AppUpdator.kt
This commit is contained in:
@@ -2,6 +2,7 @@ package `in`.sminnovations.attendanceapp.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -30,7 +31,7 @@ data class UpdateInfo(
|
|||||||
|
|
||||||
class AppUpdater(private val context: Context) {
|
class AppUpdater(private val context: Context) {
|
||||||
sealed class UpdaterState {
|
sealed class UpdaterState {
|
||||||
object Idle : UpdaterState()
|
data object Idle : UpdaterState()
|
||||||
data class Downloading(val progress: Float) : UpdaterState()
|
data class Downloading(val progress: Float) : UpdaterState()
|
||||||
data class Success(val file: File) : UpdaterState()
|
data class Success(val file: File) : UpdaterState()
|
||||||
data class Error(val message: String) : UpdaterState()
|
data class Error(val message: String) : UpdaterState()
|
||||||
@@ -42,86 +43,78 @@ class AppUpdater(private val context: Context) {
|
|||||||
fun downloadAndInstall(url: String) {
|
fun downloadAndInstall(url: String) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val file = downloadApk(url) { progress ->
|
//Added for future use case
|
||||||
_updateState.value = UpdaterState.Downloading(progress)
|
// 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)
|
_updateState.value = UpdaterState.Success(file)
|
||||||
installApk(file)
|
installApk(file)
|
||||||
} catch (e: Exception) {
|
} 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 {
|
private fun resolveDownloadUrl(url: String): String {
|
||||||
return withContext(Dispatchers.IO) {
|
// 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()
|
val client = OkHttpClient.Builder()
|
||||||
.addNetworkInterceptor { chain ->
|
.addNetworkInterceptor { chain ->
|
||||||
val originalResponse = chain.proceed(chain.request())
|
val originalResponse = chain.proceed(chain.request())
|
||||||
// Verify content type
|
val contentLength = originalResponse.body?.contentLength() ?: -1L
|
||||||
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")
|
|
||||||
}
|
|
||||||
originalResponse.newBuilder()
|
originalResponse.newBuilder()
|
||||||
.body(originalResponse.body?.let { body ->
|
.body(originalResponse.body?.let { body ->
|
||||||
ProgressResponseBody(body) { progress ->
|
ProgressResponseBody(body) { progress ->
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
onProgress(progress)
|
||||||
onProgress(progress)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder().url(url).build()
|
||||||
.url(url)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val file = File(context.cacheDir, "update.apk")
|
val file = File(context.cacheDir, "update.apk")
|
||||||
|
|
||||||
client.newCall(request).execute().use { response ->
|
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 ->
|
response.body?.let { body ->
|
||||||
file.outputStream().use { fileOut ->
|
file.outputStream().use { fileOut ->
|
||||||
body.byteStream().use { bodyIn ->
|
body.byteStream().use { it.copyTo(fileOut) }
|
||||||
bodyIn.copyTo(fileOut)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} ?: throw IOException("Empty response")
|
} ?: throw IOException("Empty response")
|
||||||
}
|
}
|
||||||
|
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun installApk(file: File) {
|
private fun installApk(file: File) {
|
||||||
try {
|
try {
|
||||||
val uri = FileProvider.getUriForFile(
|
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
|
||||||
context,
|
if (file.length() == 0L) throw IOException("Downloaded file is empty")
|
||||||
"${context.packageName}.provider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify the file is actually an APK
|
|
||||||
if (file.length() == 0L) {
|
|
||||||
throw IOException("Downloaded file is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that there's an app to handle the install intent
|
|
||||||
if (intent.resolveActivity(context.packageManager) != null) {
|
if (intent.resolveActivity(context.packageManager) != null) {
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
throw IOException("No app found to handle APK installation")
|
throw IOException("No app found to handle installation")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_updateState.value = UpdaterState.Error("Installation failed: ${e.message}")
|
_updateState.value = UpdaterState.Error("Installation failed: ${e.message}")
|
||||||
@@ -136,28 +129,19 @@ private class ProgressResponseBody(
|
|||||||
private val bufferedSource: BufferedSource by lazy {
|
private val bufferedSource: BufferedSource by lazy {
|
||||||
source(responseBody.source()).buffer()
|
source(responseBody.source()).buffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentType() = responseBody.contentType()
|
override fun contentType() = responseBody.contentType()
|
||||||
override fun contentLength() = responseBody.contentLength()
|
override fun contentLength() = responseBody.contentLength()
|
||||||
override fun source(): BufferedSource = bufferedSource
|
override fun source(): BufferedSource = bufferedSource
|
||||||
|
|
||||||
private fun source(source: Source): Source {
|
private fun source(source: Source): Source =
|
||||||
return object : ForwardingSource(source) {
|
object : ForwardingSource(source) {
|
||||||
var totalBytesRead = 0L
|
var totalBytesRead = 0L
|
||||||
|
|
||||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||||
val bytesRead = super.read(sink, byteCount)
|
val bytesRead = super.read(sink, byteCount)
|
||||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
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)
|
onProgressUpdate(progress)
|
||||||
|
|
||||||
return bytesRead
|
return bytesRead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user