feat : apk installer

This commit is contained in:
2025-07-19 15:35:07 +03:30
parent 69945a29cf
commit 3683e8a9e6
15 changed files with 641 additions and 79 deletions

View File

@@ -7,6 +7,14 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application
@@ -19,8 +27,8 @@
android:enableOnBackInvokedCallback="true"
android:exported="true"
android:hardwareAccelerated="true"
android:requestLegacyExternalStorage="true"
android:launchMode="singleTop"
android:requestLegacyExternalStorage="true"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">

View File

@@ -0,0 +1,294 @@
package ir.mnpc.rasadyar
import android.app.Activity
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.core.content.FileProvider
import java.io.File
import androidx.core.net.toUri
class ApkInstaller(private val context: Context) {
companion object {
const val INSTALL_REQUEST_CODE = 1001
private const val TAG = "ApkInstaller"
}
/**
* Install APK with compatibility for Android 5-15
*/
fun installApk(apkFile: File) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
// Android 8+ (API 26+) - Use PackageInstaller API
installWithPackageInstaller(apkFile)
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
// Android 7+ (API 24+) - Use FileProvider with Intent
installWithFileProvider(apkFile)
}
else -> {
// Android 5-6 (API 21-23) - Use file URI with Intent
installWithFileUri(apkFile)
}
}
}
/**
* Android 8+ (API 26+) - PackageInstaller API
*/
private fun installWithPackageInstaller(apkFile: File) {
try {
val packageInstaller = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
)
// Set installer package name for better tracking
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
params.setInstallReason(PackageManager.INSTALL_REASON_USER)
}
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)
session.use { activeSession ->
apkFile.inputStream().use { inputStream ->
activeSession.openWrite("package", 0, apkFile.length()).use { outputStream ->
inputStream.copyTo(outputStream)
activeSession.fsync(outputStream)
}
}
val intent = Intent(context, InstallResultReceiver::class.java).apply {
action = "com.yourpackage.INSTALL_RESULT"
}
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent, flags
)
activeSession.commit(pendingIntent.intentSender)
}
} catch (e: Exception) {
Log.e(TAG, "Error installing with PackageInstaller", e)
// Fallback to intent method
installWithFileProvider(apkFile)
}
}
/**
* Android 7+ (API 24+) - FileProvider with Intent
*/
private fun installWithFileProvider(apkFile: File) {
try {
val apkUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
apkFile
)
val intent = createInstallIntent(apkUri).apply {
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
// Additional flags for better compatibility
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.packageName)
}
}
if (context is Activity) {
context.startActivityForResult(intent, INSTALL_REQUEST_CODE)
} else {
context.startActivity(intent)
}
} catch (e: Exception) {
Log.e(TAG, "Error installing with FileProvider", e)
// Final fallback for Android 7+
installWithFileUri(apkFile)
}
}
/**
* Android 5-6 (API 21-23) - File URI with Intent
*/
private fun installWithFileUri(apkFile: File) {
try {
val apkUri = Uri.fromFile(apkFile)
val intent = createInstallIntent(apkUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
if (context is Activity) {
context.startActivityForResult(intent, INSTALL_REQUEST_CODE)
} else {
context.startActivity(intent)
}
} catch (e: Exception) {
Log.e(TAG, "Error installing with file URI", e)
}
}
/**
* Create appropriate install intent based on Android version
*/
private fun createInstallIntent(uri: Uri): Intent {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
data = uri
}
} else {
Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
}
}
}
/**
* Check if installation from unknown sources is allowed
*/
fun canInstallPackages(): Boolean {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
context.packageManager.canRequestPackageInstalls()
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> {
try {
Settings.Secure.getInt(
context.contentResolver,
Settings.Secure.INSTALL_NON_MARKET_APPS
) == 1
} catch (e: Settings.SettingNotFoundException) {
false
}
}
else -> {
// For older versions, assume it's allowed
true
}
}
}
/**
* Request permission to install packages
*/
fun requestInstallPermission() {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
if (!context.packageManager.canRequestPackageInstalls()) {
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
data = "package:${context.packageName}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
}
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> {
val intent = Intent(Settings.ACTION_SECURITY_SETTINGS).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
}
}
}
/**
* Check if APK file is valid
*/
fun isValidApkFile(apkFile: File): Boolean {
if (!apkFile.exists() || !apkFile.canRead()) {
return false
}
return try {
val packageInfo = context.packageManager.getPackageArchiveInfo(
apkFile.absolutePath,
PackageManager.GET_ACTIVITIES
)
packageInfo != null
} catch (e: Exception) {
Log.e(TAG, "Error validating APK file", e)
false
}
}
}
class InstallResultReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_SUCCESS -> {
Log.d("InstallResult", "Installation successful")
// Handle successful installation
Toast.makeText(context, "Installation successful", Toast.LENGTH_SHORT).show()
}
PackageInstaller.STATUS_FAILURE -> {
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
Log.e("InstallResult", "Installation failed: $message")
Toast.makeText(context, "Installation failed: $message", Toast.LENGTH_LONG).show()
}
PackageInstaller.STATUS_FAILURE_BLOCKED -> {
Log.e("InstallResult", "Installation blocked")
Toast.makeText(context, "Installation blocked by system", Toast.LENGTH_LONG).show()
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
Log.e("InstallResult", "Installation aborted")
Toast.makeText(context, "Installation was cancelled", Toast.LENGTH_SHORT).show()
}
PackageInstaller.STATUS_FAILURE_INVALID -> {
Log.e("InstallResult", "Invalid APK")
Toast.makeText(context, "Invalid APK file", Toast.LENGTH_LONG).show()
}
PackageInstaller.STATUS_FAILURE_CONFLICT -> {
Log.e("InstallResult", "Installation conflict")
Toast.makeText(
context,
"Installation conflict with existing app",
Toast.LENGTH_LONG
).show()
}
PackageInstaller.STATUS_FAILURE_STORAGE -> {
Log.e("InstallResult", "Insufficient storage")
Toast.makeText(context, "Insufficient storage space", Toast.LENGTH_LONG).show()
}
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> {
Log.e("InstallResult", "Incompatible app")
Toast.makeText(context, "App is incompatible with device", Toast.LENGTH_LONG).show()
}
else -> {
Log.w("InstallResult", "Unknown status: $status")
}
}
}
}

View File

@@ -3,16 +3,22 @@ package ir.mnpc.rasadyar
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import androidx.core.net.toUri
class MainActivity : FlutterActivity() {
private val CHANNEL = "apk_installer"
private val INSTALL_PACKAGES_REQUEST_CODE = 1001
val installer = ApkInstaller(this)
private val TAG = "cj"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@@ -21,9 +27,18 @@ class MainActivity : FlutterActivity() {
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
if (call.method == "installApk") {
if (call.method == "apk_installer") {
val apkPath = call.argument<String>("appPath") ?: ""
installApk(apkPath)
Log.i(TAG, "configureFlutterEngine: $apkPath")
val apkFile = File(getExternalFilesDir(null), apkPath)
Log.i(TAG, "apkFile: $apkFile")
Log.i(TAG, "externalStorageDirectory: ${getExternalFilesDir(null)}")
/*
if (!installer.canInstallPackages()) {
installer.requestInstallPermission()
} else {
installer.installApk(apkFile)
}*/
result.success(null)
}
}
@@ -34,24 +49,51 @@ class MainActivity : FlutterActivity() {
private fun installApk(path: String) {
val file = File(path)
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
val apkUri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val uri = FileProvider.getUriForFile(
applicationContext,
"${applicationContext.packageName}.fileprovider",
file
)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
uri
} else {
Uri.fromFile(file)
if (!file.exists()) {
Log.e("jojo", "APK file does not exist: $path")
return
}
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
applicationContext.startActivity(intent)
// Check if we can install unknown apps (Android 8.0+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!packageManager.canRequestPackageInstalls()) {
requestInstallPermission()
return
}
}
val intent = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
val apkUri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
).also {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
} else {
Uri.fromFile(file)
}
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
context.startActivity(intent)
} catch (e: Exception) {
Log.e("jojo", "installApk error: ${e.message}", e)
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun requestInstallPermission() {
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
data = "package:$packageName".toUri()
}
startActivityForResult(intent, INSTALL_PACKAGES_REQUEST_CODE)
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external_files"
path="." />
<files-path name="app_flutter" path="app_flutter/*" />
</paths>