feat : apk installer
This commit is contained in:
@@ -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">
|
||||
|
||||
294
android/app/src/main/kotlin/ir/mnpc/rasadyar/ApkInstaller.kt
Normal file
294
android/app/src/main/kotlin/ir/mnpc/rasadyar/ApkInstaller.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user