diff --git a/app/src/main/java/in/sminnovations/attendanceapp/utils/AppUpdator.kt b/app/src/main/java/in/sminnovations/attendanceapp/utils/AppUpdator.kt index 335fc4b..d391596 100644 --- a/app/src/main/java/in/sminnovations/attendanceapp/utils/AppUpdator.kt +++ b/app/src/main/java/in/sminnovations/attendanceapp/utils/AppUpdator.kt @@ -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 } } - } }