mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Create SilentInstallPlugin
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
+49
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -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
@@ -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']) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user