Fix: #262 =-Enhance _ensureUpdateNotificationChannel method for better notification management. Update package manager references to BackgroundPackageManager for improved clarity and functionality. BackgroundUpdateService: Refactor file handling and notification channel setup. Introduce

This commit is contained in:
Henrique Velloso
2026-01-16 20:40:25 -03:00
parent 332ed3506d
commit e262fc927d
2 changed files with 73 additions and 43 deletions
+71 -43
View File
@@ -1,7 +1,9 @@
import 'dart:io' show Directory, Platform;
import 'dart:io' show Directory, File, Platform;
import 'dart:ui' as ui;
import 'package:background_downloader/background_downloader.dart';
import 'package:background_downloader/background_downloader.dart' hide Request;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:path/path.dart' as path;
@@ -9,7 +11,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:purplebase/purplebase.dart';
import 'package:workmanager/workmanager.dart';
import 'package:zapstore/services/package_manager/android_package_manager.dart';
import 'package:zapstore/services/package_manager/background_package_manager.dart';
import 'package:zapstore/services/package_manager/dummy_package_manager.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import 'package:zapstore/utils/extensions.dart';
@@ -42,6 +44,8 @@ const kAppCatalogRelaysKey = 'appCatalogRelays';
/// This MUST be a top-level function (not a class method).
@pragma('vm:entry-point')
void callbackDispatcher() {
WidgetsFlutterBinding.ensureInitialized();
ui.DartPluginRegistrant.ensureInitialized();
Workmanager().executeTask((task, inputData) async {
switch (task) {
case kBackgroundUpdateTaskName:
@@ -81,11 +85,8 @@ Future<bool> _performWeeklyCleanup() async {
try {
// Delete the file if it exists
final filePath = await task.filePath();
final file = Directory(filePath).parent.listSync().firstWhere(
(f) => f.path.endsWith(task.filename),
orElse: () => Directory(''),
);
if (file.path.isNotEmpty) {
final file = File(filePath);
if (await file.exists()) {
await file.delete();
}
} catch (_) {}
@@ -105,14 +106,14 @@ Future<bool> _performWeeklyCleanup() async {
);
if (await downloadDir.exists()) {
final files = downloadDir.listSync();
final entities = downloadDir.listSync();
final cutoff = DateTime.now().subtract(_staleDownloadThreshold);
for (final file in files) {
if (file is! Directory) {
final stat = await file.stat();
for (final entity in entities) {
if (entity is File) {
final stat = await entity.stat();
if (stat.modified.isBefore(cutoff)) {
await file.delete();
await entity.delete();
}
}
}
@@ -145,7 +146,7 @@ Future<bool> _checkForUpdatesInBackground(Set<String>? appCatalogRelays) async {
storageNotifierProvider.overrideWith(PurplebaseStorageNotifier.new),
packageManagerProvider.overrideWith(
(ref) => Platform.isAndroid
? AndroidPackageManager(ref)
? BackgroundPackageManager(ref)
: DummyPackageManager(ref),
),
],
@@ -153,7 +154,7 @@ Future<bool> _checkForUpdatesInBackground(Set<String>? appCatalogRelays) async {
try {
// Initialize Purplebase with same DB path as main app
final dir = await getApplicationDocumentsDirectory();
final dir = await getApplicationSupportDirectory();
final dbPath = path.join(dir.path, 'zapstore.db');
await container.read(
@@ -188,18 +189,40 @@ Future<bool> _checkForUpdatesInBackground(Set<String>? appCatalogRelays) async {
source: const RemoteSource(relays: 'AppCatalog', stream: false),
);
// Load releases for all apps in a single query
// Load releases and their metadata/assets (required for hasUpdate)
if (apps.isNotEmpty) {
final addressableIds = apps
.map((app) => app.event.addressableId)
.toSet();
await storage.query(
RequestFilter<Release>(tags: {'#a': addressableIds}).toRequest(),
source: const LocalAndRemoteSource(
relays: 'AppCatalog',
stream: false,
),
);
final releaseFilters = apps
.map((app) => app.latestRelease.req?.filters.firstOrNull)
.nonNulls
.toList();
if (releaseFilters.isNotEmpty) {
final List<Release> releases = await storage.query(
Request<Release>(releaseFilters),
source: const RemoteSource(relays: 'AppCatalog', stream: false),
);
final metadataFilters = releases
.map((r) => r.latestMetadata.req?.filters.firstOrNull)
.nonNulls
.toList();
if (metadataFilters.isNotEmpty) {
await storage.query(
Request<FileMetadata>(metadataFilters),
source: const RemoteSource(relays: 'AppCatalog', stream: false),
);
}
final assetFilters = releases
.map((r) => r.latestAsset.req?.filters.firstOrNull)
.nonNulls
.toList();
if (assetFilters.isNotEmpty) {
await storage.query(
Request<SoftwareAsset>(assetFilters),
source: const RemoteSource(relays: 'AppCatalog', stream: false),
);
}
}
}
// Re-query apps from local to ensure relationships are loaded
@@ -255,6 +278,7 @@ Future<void> _showUpdateNotification(
android: initializationSettingsAndroid,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
await _ensureUpdateNotificationChannel(flutterLocalNotificationsPlugin);
// Build notification content
final title = updateCount == 1
@@ -366,23 +390,7 @@ class BackgroundUpdateService {
// Navigation is handled by the app's normal launch flow
},
);
// Create notification channel on Android
final androidPlugin = flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidPlugin != null) {
await androidPlugin.createNotificationChannel(
const AndroidNotificationChannel(
kUpdateNotificationChannelId,
kUpdateNotificationChannelName,
description: kUpdateNotificationChannelDescription,
importance: Importance.defaultImportance,
),
);
}
await _ensureUpdateNotificationChannel(flutterLocalNotificationsPlugin);
}
/// Cancel background update checks
@@ -405,6 +413,26 @@ class BackgroundUpdateService {
}
}
Future<void> _ensureUpdateNotificationChannel(
FlutterLocalNotificationsPlugin plugin,
) async {
final androidPlugin = plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidPlugin != null) {
await androidPlugin.createNotificationChannel(
const AndroidNotificationChannel(
kUpdateNotificationChannelId,
kUpdateNotificationChannelName,
description: kUpdateNotificationChannelDescription,
importance: Importance.defaultImportance,
),
);
}
}
/// Provider for the background update service
final backgroundUpdateServiceProvider = Provider<BackgroundUpdateService>(
BackgroundUpdateService.new,
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:models/models.dart';
import 'package:zapstore/services/package_manager/installed_packages_snapshot.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
/// Install status values from native side.
@@ -587,6 +588,7 @@ final class AndroidPackageManager extends PackageManager {
}
state = state.copyWith(installed: {...packages, ...preserved});
await InstalledPackagesSnapshot.save(state.installed);
// Clear operations for apps where the installed version matches the target version
// This catches installs that succeeded but we missed the event