AppUpdater

This commit is contained in:
sathwikcs
2025-01-16 14:53:16 +05:30
parent bdda880414
commit 1482c2b548

View File

@@ -0,0 +1,240 @@
package `in`.sminnovations.attendanceapp.main
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSource
import okio.ForwardingSource
import okio.Source
import okio.buffer
import okio.source
import java.io.File
import java.io.IOException
data class UpdateInfo(
val version: String = "",
val url: String = "",
val isNew: Boolean = false
)
object UpdateChecker {
private const val COLLECTION_NAME = "app_updates"
private const val DOCUMENT_NAME = "latest_version"
suspend fun checkForUpdate(
context: Context
): Result<UpdateInfo> = runCatching {
val currentVersion = getCurrentVersion(context)
val updateInfo = getLatestVersion()
if (isNewerVersion(currentVersion, updateInfo.version)) {
updateInfo.copy(isNew = true)
} else {
updateInfo.copy(isNew = false)
}
}
private fun getCurrentVersion(context: Context): String {
return try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
} catch (e: Exception) {
throw UpdateCheckException("Failed to get current app version", e)
}
}
private suspend fun getLatestVersion(): UpdateInfo {
return try {
val document = Firebase.firestore
.collection(COLLECTION_NAME)
.document(DOCUMENT_NAME)
.get()
.await()
if (!document.exists()) {
throw UpdateCheckException("Update document does not exist")
}
UpdateInfo(
version = document.getString("version") ?: throw UpdateCheckException("Version field missing"),
url = document.getString("url") ?: throw UpdateCheckException("URL field missing")
)
} catch (e: Exception) {
throw UpdateCheckException("Failed to fetch update info", e)
}
}
private fun isNewerVersion(currentVersion: String, latestVersion: String): Boolean {
return try {
val current = currentVersion.split(".").map { it.toIntOrNull() ?: 0 }
val latest = latestVersion.split(".").map { it.toIntOrNull() ?: 0 }
for (i in 0 until maxOf(current.size, latest.size)) {
val currentNum = current.getOrNull(i) ?: 0
val latestNum = latest.getOrNull(i) ?: 0
when {
latestNum > currentNum -> return true
latestNum < currentNum -> return false
}
}
false
} catch (e: Exception) {
false
}
}
}
class UpdateCheckException(
message: String,
cause: Throwable? = null
) : Exception(message, cause){
}
class AppUpdater(private val context: Context) {
sealed class UpdaterState {
object Idle : UpdaterState()
data class Downloading(val progress: Float) : UpdaterState()
data class Success(val file: File) : UpdaterState()
data class Error(val message: String) : UpdaterState()
}
private val _updateState = MutableStateFlow<UpdaterState>(UpdaterState.Idle)
val updateState = _updateState.asStateFlow()
fun downloadAndInstall(url: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val file = downloadApk(url) { progress ->
_updateState.value = UpdaterState.Downloading(progress)
}
_updateState.value = UpdaterState.Success(file)
installApk(file)
} catch (e: Exception) {
_updateState.value = UpdaterState.Error(e.message ?: "Unknown error occurred")
}
}
}
private suspend fun downloadApk(url: String, onProgress: (Float) -> Unit): File {
return 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")
}
originalResponse.newBuilder()
.body(originalResponse.body?.let { body ->
ProgressResponseBody(body) { progress ->
CoroutineScope(Dispatchers.Main).launch {
onProgress(progress)
}
}
})
.build()
}
.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")
response.body?.let { body ->
file.outputStream().use { fileOut ->
body.byteStream().use { bodyIn ->
bodyIn.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 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)
}
// 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")
}
} catch (e: Exception) {
_updateState.value = UpdaterState.Error("Installation failed: ${e.message}")
}
}
}
private class ProgressResponseBody(
private val responseBody: ResponseBody,
private val onProgressUpdate: (Float) -> Unit
) : ResponseBody() {
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) {
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
}
onProgressUpdate(progress)
return bytesRead
}
}
}
}