[fix]: fixed auto-install flow & add install monitor #78

This commit is contained in:
nahnah
2026-03-13 13:26:03 +00:00
parent 9a391a4496
commit 0dc462d2b4
2 changed files with 57 additions and 176 deletions
+2 -2
View File
@@ -254,7 +254,7 @@ class DownloadProvider extends ChangeNotifier {
/// Downloads an APK file
Future<String?> downloadApk(FDroidApp app, {bool? skipAutoInstall}) async {
final version = await _selectBestVersionForDevice(app);
skipAutoInstall = _settingsProvider.autoInstallApk;
skipAutoInstall ??= false;
if (version == null) {
throw Exception('No version available for download');
}
@@ -387,7 +387,7 @@ class DownloadProvider extends ChangeNotifier {
);
// Auto-install after download completes if enabled
if (_settingsProvider.autoInstallApk) {
if (_settingsProvider.autoInstallApk && !skipAutoInstall) {
debugPrint(
'[DownloadProvider] Auto-install queued for ${app.packageName} ${version.versionName} (method: ${_settingsProvider.installMethod})',
);
+55 -174
View File
@@ -34,11 +34,14 @@ class AppDetailsScreen extends StatefulWidget {
State<AppDetailsScreen> createState() => _AppDetailsScreenState();
}
class _AppDetailsScreenState extends State<AppDetailsScreen> {
class _AppDetailsScreenState extends State<AppDetailsScreen>
with WidgetsBindingObserver {
late Future<List<String>> _screenshotsFuture;
late Future<IzzyStats> _statsFuture;
late Future<FDroidApp> _enrichedAppFuture;
bool _isInstalling = false;
bool _pendingInstallStateRefresh = false;
bool _isInstallMonitorRunning = false;
bool _isCollapsed = false;
late ScrollController _scrollController;
final double expandedBarHeight = 300;
@@ -47,6 +50,7 @@ class _AppDetailsScreenState extends State<AppDetailsScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_scrollController = ScrollController();
// Start with lightweight futures to keep route transition smooth.
_screenshotsFuture = Future.value(const <String>[]);
@@ -79,11 +83,14 @@ class _AppDetailsScreenState extends State<AppDetailsScreen> {
DownloadProvider downloadProvider,
SettingsProvider settings,
) async {
// Poll for installation completion in background
for (int i = 0; i < 15; i++) {
// Poll for installation completion in background.
// Keep this window long enough for slow/manual install confirmation flows.
final deadline = DateTime.now().add(const Duration(minutes: 2));
while (DateTime.now().isBefore(deadline)) {
await Future.delayed(const Duration(milliseconds: 800));
await appProvider.fetchInstalledApps();
if (appProvider.isAppInstalled(widget.app.packageName)) {
_pendingInstallStateRefresh = false;
// App installed, clean up if auto-delete enabled
if (settings.autoDeleteApk) {
final latestVersion = await appProvider.getLatestVersion(widget.app);
@@ -108,6 +115,30 @@ class _AppDetailsScreenState extends State<AppDetailsScreen> {
}
}
Future<void> _ensureInstallStateRefresh() async {
if (!mounted || _isInstallMonitorRunning) return;
_isInstallMonitorRunning = true;
try {
await _startBackgroundCleanup(
context.read<AppProvider>(),
context.read<DownloadProvider>(),
context.read<SettingsProvider>(),
);
} finally {
_isInstallMonitorRunning = false;
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!mounted || state != AppLifecycleState.resumed) return;
context.read<AppProvider>().fetchInstalledApps();
if (_pendingInstallStateRefresh) {
_ensureInstallStateRefresh();
}
}
Widget _buildInstallButton(
BuildContext context,
DownloadProvider downloadProvider,
@@ -252,6 +283,7 @@ class _AppDetailsScreenState extends State<AppDetailsScreen> {
setState(() {
_isInstalling = true;
_pendingInstallStateRefresh = true;
});
try {
await downloadProvider.installApk(
@@ -340,184 +372,32 @@ class _AppDetailsScreenState extends State<AppDetailsScreen> {
final appWithVersion = widget.app
.copyWithVersion(version)
.copyWith(repositoryUrl: repositoryUrl);
// Skip DownloadProvider's auto-install since we handle it here with proper UI updates
await downloadProvider.downloadApk(
appWithVersion,
skipAutoInstall: settingsProvider.autoInstallApk,
);
await downloadProvider.downloadApk(appWithVersion);
if (context.mounted) {
final settings = context.read<SettingsProvider>();
// Auto-install if enabled
// Provider owns auto-install. Here we only reflect state and refresh UI.
if (settings.autoInstallApk) {
final downloadInfo = downloadProvider.getDownloadInfo(
widget.app.packageName,
version.versionName,
);
if (downloadInfo?.filePath != null &&
File(downloadInfo!.filePath!).existsSync()) {
debugPrint(
'[AppDetails] Auto-install start ${widget.app.packageName} ${version.versionName}',
);
try {
// Ensure the chosen installer is available (system permission or Shizuku binder)
final hasPermission = await downloadProvider
.requestInstallPermission();
debugPrint(
'[AppDetails] Auto-install permission result: $hasPermission',
);
if (!hasPermission) {
if (!context.mounted) return;
if (settings.installMethod == InstallMethod.shizuku) {
await _handleShizukuUnavailable(context, settings);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Install permission required for auto-install',
),
),
);
}
return;
}
if (mounted) {
setState(() {
_isInstalling = true;
});
}
debugPrint(_isInstalling as String?);
// Trigger install without waiting (non-blocking). For Shizuku,
// run fire-and-forget to avoid UI stalls; for system, await as before.
if (settings.installMethod == InstallMethod.shizuku) {
Future<void>(() async {
try {
await downloadProvider.installApk(
downloadInfo.filePath!,
widget.app.packageName,
downloadInfo.versionName,
widget.app.name,
antiFeatures: widget.app.antiFeatures,
);
await Future.delayed(const Duration(seconds: 1));
await appProvider.fetchInstalledApps();
_startBackgroundCleanup(
appProvider,
downloadProvider,
settings,
);
} catch (e) {
debugPrint(
'[AppDetails] Shizuku auto-install failed: $e',
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Auto-install failed: ${e.toString()}',
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isInstalling = false;
});
}
}
});
} else {
downloadProvider
.installApk(
downloadInfo.filePath!,
widget.app.packageName,
downloadInfo.versionName,
widget.app.name,
antiFeatures: widget.app.antiFeatures,
)
.then((_) async {
debugPrint('[AppDetails] Auto-install completed');
try {
await appProvider.waitForInstalled(
widget.app.packageName,
);
} catch (_) {
// Best-effort; UI will refresh on next poll
}
await Future.delayed(const Duration(seconds: 2));
await appProvider.fetchInstalledApps();
_startBackgroundCleanup(
appProvider,
downloadProvider,
settings,
);
})
.catchError((e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.auto_install_failed_with_error(e.toString()),
),
),
);
}
})
.whenComplete(() async {
if (mounted) {
setState(() {
_isInstalling = false;
});
}
await Future.delayed(const Duration(milliseconds: 500));
await appProvider.fetchInstalledApps();
});
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.installing_app(widget.app.name),
),
),
);
}
// Best-effort refresh a moment later so the UI can flip to Installed
Future.delayed(const Duration(seconds: 2), () async {
await appProvider.fetchInstalledApps();
});
// Return early - don't wait for installation
return;
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.auto_install_failed_with_error(e.toString()),
),
),
);
}
return;
}
} else {
debugPrint(
'[AppDetails] Auto-install skipped: downloadInfo missing or file not found',
if (mounted) {
setState(() {
_isInstalling = true;
_pendingInstallStateRefresh = true;
});
}
_ensureInstallStateRefresh();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.installing_app(widget.app.name),
),
),
);
}
return;
}
// Only run this polling loop when NOT auto-installing
@@ -731,6 +611,7 @@ class _AppDetailsScreenState extends State<AppDetailsScreen> {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_scrollController.dispose();
super.dispose();
}