Create SilentInstallPlugin

This commit is contained in:
franzap
2025-03-07 14:22:29 -03:00
parent 11af4e870e
commit 94f1601061
9 changed files with 257 additions and 17 deletions
+10
View File
@@ -63,11 +63,21 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
<!-- Receiver for the SilentInstallPlugin installation results -->
<receiver
android:name=".plugins.silent_install.InstallResultReceiver"
android:exported="false">
<intent-filter>
<action android:name="dev.zapstore.app.plugins.silent_install.SESSION_API_PACKAGE_INSTALLED" />
</intent-filter>
</receiver>
</application>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- Required to query activities that can process text, see:
@@ -1,5 +1,13 @@
package dev.zapstore.app
import dev.zapstore.app.plugins.silent_install.SilentInstallPlugin
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: FlutterActivity()
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Register our silent installation plugin
flutterEngine.plugins.add(SilentInstallPlugin())
}
}
@@ -0,0 +1,49 @@
package dev.zapstore.app.plugins.silent_install
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.util.Log
private const val TAG = "InstallResultReceiver"
/**
* BroadcastReceiver to handle APK installation results
*/
class InstallResultReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
"dev.zapstore.app.plugins.silent_install.SESSION_API_PACKAGE_INSTALLED" -> {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
// Log installation results
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
// This should not happen with USER_ACTION_NOT_REQUIRED, but handle just in case
Log.d(TAG, "User action required: $message")
// Try to handle the user confirmation dialog if needed
val confirmIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (confirmIntent != null) {
confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(confirmIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to start confirmation activity", e)
}
}
}
PackageInstaller.STATUS_SUCCESS -> {
Log.d(TAG, "Installation succeeded for $packageName")
}
else -> {
Log.e(TAG, "Installation failed: $status - $message")
}
}
}
}
}
}
@@ -0,0 +1,150 @@
package dev.zapstore.app.plugins.silent_install
import android.app.PendingIntent
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.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.io.File
import java.io.FileInputStream
import java.io.IOException
private const val TAG = "SilentInstallPlugin"
/** Plugin that handles silent app installation */
class SilentInstallPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "silent_install_plugin")
channel.setMethodCallHandler(this)
context = flutterPluginBinding.applicationContext
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"install" -> {
val filePath = call.argument<String>("filePath")
if (filePath == null) {
result.error("MISSING_ARGUMENT", "File path is required", null)
return
}
installApk(filePath, result)
}
"canInstallSilently" -> {
result.success(canInstallSilently())
}
else -> {
result.notImplemented()
}
}
}
private fun canInstallSilently(): Boolean {
// Check if we are running on Android 11 (API 30) or higher
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
}
private fun installApk(filePath: String, result: Result) {
val file = File(filePath)
if (!file.exists()) {
result.error("FILE_NOT_FOUND", "The APK file was not found", null)
return
}
try {
val packageInstaller = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
// Set user action not required if running on Android 12 (API 31) or higher
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
// For Android 13 (API 33) and above, set package source to STORE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
params.setPackageSource(PackageInstaller.PACKAGE_SOURCE_STORE)
}
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)
val inputStream = FileInputStream(file)
val outputStream = session.openWrite("package", 0, file.length())
val buffer = ByteArray(65536)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
session.fsync(outputStream)
outputStream.close()
inputStream.close()
// Create a broadcast intent for installation status
val intent = Intent(context, InstallResultReceiver::class.java)
intent.action = "dev.zapstore.app.plugins.silent_install.SESSION_API_PACKAGE_INSTALLED"
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(
context,
sessionId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
} else {
PendingIntent.getBroadcast(
context,
sessionId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
// Commit the session
session.commit(pendingIntent.intentSender)
// Since we don't know if the installation was successful yet,
// we return a success result for now. The actual installation result
// will be handled by the broadcast receiver.
result.success(mapOf(
"isSuccess" to true,
"errorMessage" to ""
))
} catch (e: IOException) {
Log.e(TAG, "Installation error: ", e)
result.success(mapOf(
"isSuccess" to false,
"errorMessage" to "Installation failed: ${e.message}"
))
} catch (e: SecurityException) {
Log.e(TAG, "Security error: ", e)
result.success(mapOf(
"isSuccess" to false,
"errorMessage" to "Security exception: ${e.message}"
))
} catch (e: Exception) {
Log.e(TAG, "Unexpected error: ", e)
result.success(mapOf(
"isSuccess" to false,
"errorMessage" to "Unexpected error: ${e.message}"
))
}
}
}
+4 -7
View File
@@ -8,7 +8,6 @@ import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter_data/flutter_data.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -20,6 +19,7 @@ import 'package:zapstore/models/nostr_adapter.dart';
import 'package:zapstore/models/release.dart';
import 'package:zapstore/models/user.dart';
import 'package:path/path.dart' as path;
import 'package:zapstore/plugins/silent_install/silent_install_plugin.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/utils/system_info.dart';
@@ -118,13 +118,10 @@ class App extends base.App with DataModelMixin<App> {
notifier.state = RequestInstallProgress();
// NOTE: Using https://github.com/hui-z/flutter_install_plugin
// Tried https://github.com/zapstore/android_package_installer
// but it is giving users problems (and for me it's slower)
// Probably need to fork hui-z's with support for user-canceled action
// NOTE: Must use mutex: https://github.com/hui-z/flutter_install_plugin/issues/68
// NOTE: Using our custom SilentInstallPlugin for better APK installation handling
// Tries to install silently if possible, or with user confirmation if required
final result = await mutex.protect(() async {
return InstallPlugin.install(file.path);
return SilentInstallPlugin.install(file.path);
});
if (result['isSuccess']) {
+1
View File
@@ -0,0 +1 @@
export 'silent_install_plugin.dart';
@@ -0,0 +1,34 @@
import 'dart:async';
import 'package:flutter/services.dart';
class SilentInstallPlugin {
static const MethodChannel _channel = MethodChannel('silent_install_plugin');
/// Install an APK file silently if possible, or with user confirmation if required.
///
/// Returns a map with:
/// - 'isSuccess': boolean indicating if installation was successful
/// - 'errorMessage': String containing error message if any
static Future<Map<String, dynamic>> install(String filePath) async {
try {
final result = await _channel.invokeMethod('install', {'filePath': filePath});
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {
'isSuccess': false,
'errorMessage': e.message ?? 'Unknown error occurred'
};
}
}
/// Check if the app can install packages silently without user interaction
static Future<bool> canInstallSilently() async {
try {
final result = await _channel.invokeMethod('canInstallSilently');
return result;
} on PlatformException catch (_) {
return false;
}
}
}
-8
View File
@@ -654,14 +654,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.3+2"
install_plugin:
dependency: "direct main"
description:
name: install_plugin
sha256: "6fb67ba0781e75de4f2f2266ed25e835bfd277c5bfc2ed034af52774355857c6"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
intl:
dependency: "direct main"
description:
-1
View File
@@ -47,7 +47,6 @@ dependencies:
app_links: ^6.3.2
dart_emoji: ^0.2.0+2
remove_markdown: ^0.0.2
install_plugin: ^2.1.0
shared_preferences: ^2.3.2
background_downloader: ^8.8.0
mutex: ^3.1.0