Skip to content

Commit

Permalink
Improve app installation process with version checks and updates
Browse files Browse the repository at this point in the history
Revamp the APK installation flow to use a coroutine for result handling, ensuring proper version and installation state checks. Added logic to compare version codes and installation timestamps for determining success. Updated `AndroidManifest.xml` to enable Tiramisu-specific features with `enableOnBackInvokedCallback`.
  • Loading branch information
kdroidFilter committed Jan 10, 2025
1 parent 91cd2a6 commit e6bc422
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 24 deletions.
4 changes: 3 additions & 1 deletion platformtools/appmanager/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

<application>
<application
android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu">

<activity
android:name=".restartappmanager.RestartManagerActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package io.github.kdroidfilter.platformtools.appmanager

import android.app.PendingIntent
import android.app.admin.DevicePolicyManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import androidx.core.content.FileProvider
Expand All @@ -18,6 +23,7 @@ import java.io.File
import java.io.FileInputStream
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

actual fun getAppInstaller(): AppInstaller = ApkInstallerAndroid()

Expand Down Expand Up @@ -52,6 +58,8 @@ class ApkInstallerAndroid : AppInstaller {
}
}



override suspend fun installApp(
appFile: File,
onResult: (success: Boolean, message: String?) -> Unit,
Expand All @@ -60,47 +68,119 @@ class ApkInstallerAndroid : AppInstaller {
if (!canRequestInstallPackages()) {
// 2) Request permission if necessary
requestInstallPackagesPermission()

// Optional: Wait for the user to grant permission
// You can implement logic to wait or inform the user
// and return or suspend until the permission is granted.
// For simplicity, we continue here.
}

// 3) Recheck after the request: if still not allowed, exit
// 3) Re-check after the request: if still not allowed, exit
if (!canRequestInstallPackages()) {
onResult(false, "Installation from unknown sources is not allowed.")
return
}

// 4) If permission is granted, proceed with the installation
try {
val intent = Intent(Intent.ACTION_VIEW).apply {
// Open the APK installer in a new task
flags = Intent.FLAG_ACTIVITY_NEW_TASK
// Use a suspended coroutine to wait for the installation result
suspendCancellableCoroutine { continuation ->
val packageManager = context.packageManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// From Nougat (API 24), a FileProvider must be used
// and permissions must be granted to read the URI
val apkUri: Uri = FileProvider.getUriForFile(
context, "${context.packageName}.fileprovider", appFile
)
setDataAndType(apkUri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
// Before Nougat, Uri.fromFile can still be used
setDataAndType(Uri.fromFile(appFile), "application/vnd.android.package-archive")
// Extract the package name from the APK file
val packageInfo = packageManager.getPackageArchiveInfo(appFile.absolutePath, 0)
val targetPackageName = packageInfo?.packageName

if (targetPackageName == null) {
onResult(false, "Failed to extract package name from APK file.")
continuation.resume(Unit)
return@suspendCancellableCoroutine
}
}

// Launch the intent
context.startActivity(intent)
// Retrieve current package information for the target package before installation
val currentPackageInfo = try {
packageManager.getPackageInfo(targetPackageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}

// Retrieve the current versionCode or installation date
val currentVersionCode = currentPackageInfo?.versionCode ?: -1
val currentInstallTime = currentPackageInfo?.firstInstallTime ?: -1

// Create the intent to launch the APK installation
val installIntent = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// From Nougat (API 24) onwards, use FileProvider
val apkUri: Uri = FileProvider.getUriForFile(
context, "${context.packageName}.fileprovider", appFile
)
setDataAndType(apkUri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
// Before Nougat, use Uri.fromFile
setDataAndType(Uri.fromFile(appFile), "application/vnd.android.package-archive")
}
}

try {
// Launch the installation intent
context.startActivity(installIntent)

// Inform the caller that the installation has started
onResult(true, "Installation started.")
// Wait for a short moment to allow the installation to start
Handler(Looper.getMainLooper()).postDelayed({
// Check if the app has been installed or updated
val updatedPackageInfo = try {
packageManager.getPackageInfo(targetPackageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}

if (updatedPackageInfo != null) {
// Compare the versionCode or installation date
val isUpdated = when {
// If the versionCode has increased, it's a successful update
updatedPackageInfo.versionCode > currentVersionCode -> true
// If the installation date has changed, it's a successful update
updatedPackageInfo.firstInstallTime > currentInstallTime -> true
// Otherwise, the installation/update failed
else -> false
}

if (isUpdated) {
onResult(true, "Installation/update successful.")
} else {
onResult(false, "The application was not updated.")
}
} else {
onResult(false, "The application was not installed.")
}

// Resume the coroutine
continuation.resume(Unit)
}, 5000) // Wait 5 seconds before checking

} catch (e: Exception) {
// Handle errors during the installation intent launch
onResult(false, "Failed to start installation: ${e.message}")
continuation.resumeWithException(e)
}

// Handle coroutine cancellation
continuation.invokeOnCancellation {
// Clean up if necessary
}
}
} catch (e: Exception) {
// In case of an unexpected error
e.printStackTrace()
onResult(false, "Failed to start installation: ${e.message}")
onResult(false, "Installation failed: ${e.message}")
}
}




/**
* Installs an APK file silently without user interaction.
*
Expand Down

0 comments on commit e6bc422

Please sign in to comment.