mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Add background checker for updates
This commit is contained in:
@@ -9,9 +9,11 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<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.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:label="Zapstore Alpha"
|
||||
|
||||
+8
-1
@@ -11,6 +11,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:purplebase/purplebase.dart';
|
||||
import 'package:amber_signer/amber_signer.dart';
|
||||
import 'package:zapstore/services/app_restart_service.dart';
|
||||
import 'package:zapstore/services/background_update_service.dart';
|
||||
import 'package:zapstore/services/download_service.dart';
|
||||
import 'package:zapstore/router.dart';
|
||||
import 'package:zapstore/services/error_reporting_service.dart';
|
||||
@@ -238,6 +239,10 @@ final appInitializationProvider = FutureProvider<void>((ref) async {
|
||||
final packageManager = ref.read(packageManagerProvider.notifier);
|
||||
await packageManager.syncInstalledPackages();
|
||||
|
||||
// Initialize background update service (WorkManager + notifications)
|
||||
final backgroundService = ref.read(backgroundUpdateServiceProvider);
|
||||
await backgroundService.initialize();
|
||||
|
||||
// Initialize market intent handling (for market:// URIs)
|
||||
await ref.read(marketIntentServiceProvider).initialize();
|
||||
|
||||
@@ -265,13 +270,15 @@ Future<void> onSignInSuccess(dynamic ref) async {
|
||||
final storage =
|
||||
ref.read(storageNotifierProvider.notifier) as PurplebaseStorageNotifier;
|
||||
|
||||
await storage.query(
|
||||
unawaited(
|
||||
storage.query(
|
||||
RequestFilter<AppCatalogRelayList>(authors: {pubkey}).toRequest(),
|
||||
source: const RemoteSource(
|
||||
relays: 'bootstrap',
|
||||
background: false,
|
||||
stream: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:models/models.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.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/dummy_package_manager.dart';
|
||||
import 'package:zapstore/services/package_manager/package_manager.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
|
||||
/// Unique task name for background update checking
|
||||
const kBackgroundUpdateTaskName = 'dev.zapstore.backgroundUpdateCheck';
|
||||
|
||||
/// Unique task identifier
|
||||
const kBackgroundUpdateTaskId = 'backgroundUpdateCheck';
|
||||
|
||||
/// Notification channel for update notifications
|
||||
const kUpdateNotificationChannelId = 'zapstore_updates';
|
||||
const kUpdateNotificationChannelName = 'App Updates';
|
||||
const kUpdateNotificationChannelDescription =
|
||||
'Notifications for available app updates';
|
||||
|
||||
/// The entry point for WorkManager background tasks.
|
||||
/// This MUST be a top-level function (not a class method).
|
||||
@pragma('vm:entry-point')
|
||||
void callbackDispatcher() {
|
||||
Workmanager().executeTask((task, inputData) async {
|
||||
switch (task) {
|
||||
case kBackgroundUpdateTaskName:
|
||||
return await _checkForUpdatesInBackground();
|
||||
default:
|
||||
return Future.value(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Background update check logic - runs in a separate isolate
|
||||
Future<bool> _checkForUpdatesInBackground() async {
|
||||
try {
|
||||
// Create a fresh provider container for background work
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
storageNotifierProvider.overrideWith(PurplebaseStorageNotifier.new),
|
||||
packageManagerProvider.overrideWith(
|
||||
(ref) => Platform.isAndroid
|
||||
? AndroidPackageManager(ref)
|
||||
: DummyPackageManager(ref),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
try {
|
||||
// Initialize Purplebase with same DB path as main app
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dbPath = path.join(dir.path, 'zapstore.db');
|
||||
|
||||
await container.read(
|
||||
initializationProvider(
|
||||
StorageConfiguration(
|
||||
databasePath: dbPath,
|
||||
defaultRelays: {
|
||||
'AppCatalog': {'wss://relay.zapstore.dev'},
|
||||
},
|
||||
),
|
||||
).future,
|
||||
);
|
||||
|
||||
// Get installed packages directly from Android system
|
||||
final packageManager = container.read(packageManagerProvider.notifier);
|
||||
await packageManager.syncInstalledPackages();
|
||||
|
||||
final packages = container.read(packageManagerProvider);
|
||||
if (packages.isEmpty) {
|
||||
return true; // No installed apps to check
|
||||
}
|
||||
|
||||
final installedIds = packages.map((p) => p.appId).toSet();
|
||||
|
||||
// Query for apps with updates from relay
|
||||
final storage = container.read(storageNotifierProvider.notifier);
|
||||
final apps = await storage.query(
|
||||
RequestFilter<App>(
|
||||
tags: {
|
||||
'#d': installedIds,
|
||||
'#f': {packageManager.platform},
|
||||
},
|
||||
).toRequest(),
|
||||
source: const RemoteSource(
|
||||
relays: 'AppCatalog',
|
||||
stream: false,
|
||||
background: false,
|
||||
),
|
||||
);
|
||||
|
||||
// Load releases for hasUpdate check
|
||||
for (final app in apps) {
|
||||
await storage.query(
|
||||
RequestFilter<Release>(
|
||||
tags: {
|
||||
'#a': {app.event.addressableId},
|
||||
},
|
||||
).toRequest(),
|
||||
source: const LocalAndRemoteSource(
|
||||
relays: 'AppCatalog',
|
||||
stream: false,
|
||||
background: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Re-query apps from local to ensure relationships are loaded
|
||||
final appsWithRelations = await storage.query(
|
||||
RequestFilter<App>(
|
||||
tags: {
|
||||
'#d': installedIds,
|
||||
'#f': {packageManager.platform},
|
||||
},
|
||||
).toRequest(),
|
||||
source: const LocalSource(),
|
||||
);
|
||||
|
||||
// Count updates
|
||||
int updateCount = 0;
|
||||
final updatableAppNames = <String>[];
|
||||
|
||||
for (final app in appsWithRelations) {
|
||||
if (app.hasUpdate) {
|
||||
updateCount++;
|
||||
updatableAppNames.add(app.name ?? app.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification if updates found
|
||||
if (updateCount > 0) {
|
||||
await _showUpdateNotification(updateCount, updatableAppNames);
|
||||
}
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
container.dispose();
|
||||
}
|
||||
} catch (e) {
|
||||
// Background task failed - return false to indicate failure
|
||||
// WorkManager may retry based on configuration
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a local notification for available updates
|
||||
Future<void> _showUpdateNotification(
|
||||
int updateCount,
|
||||
List<String> appNames,
|
||||
) async {
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
// Initialize notifications (needed in background isolate)
|
||||
const initializationSettingsAndroid = AndroidInitializationSettings(
|
||||
'@mipmap/ic_launcher',
|
||||
);
|
||||
const initializationSettings = InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
);
|
||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||
|
||||
// Build notification content
|
||||
final title = updateCount == 1
|
||||
? '1 app update available'
|
||||
: '$updateCount app updates available';
|
||||
|
||||
final body = appNames.length <= 3
|
||||
? appNames.join(', ')
|
||||
: '${appNames.take(3).join(', ')} and ${appNames.length - 3} more';
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
kUpdateNotificationChannelId,
|
||||
kUpdateNotificationChannelName,
|
||||
channelDescription: kUpdateNotificationChannelDescription,
|
||||
importance: Importance.defaultImportance,
|
||||
priority: Priority.defaultPriority,
|
||||
showWhen: true,
|
||||
autoCancel: true,
|
||||
);
|
||||
|
||||
const notificationDetails = NotificationDetails(android: androidDetails);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0, // Notification ID
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
);
|
||||
}
|
||||
|
||||
/// Service for managing background update checks
|
||||
class BackgroundUpdateService {
|
||||
BackgroundUpdateService(this.ref);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
/// Initialize WorkManager and register periodic task
|
||||
Future<void> initialize() async {
|
||||
if (!Platform.isAndroid) {
|
||||
// WorkManager only works on Android/iOS, skip on other platforms
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize WorkManager
|
||||
await Workmanager().initialize(callbackDispatcher);
|
||||
|
||||
// Initialize local notifications
|
||||
await _initializeNotifications();
|
||||
|
||||
// Register periodic task (minimum 15 minutes on Android)
|
||||
// We use 6 hours for battery efficiency
|
||||
await Workmanager().registerPeriodicTask(
|
||||
kBackgroundUpdateTaskId,
|
||||
kBackgroundUpdateTaskName,
|
||||
frequency: const Duration(hours: 6),
|
||||
constraints: Constraints(
|
||||
networkType: NetworkType.connected,
|
||||
requiresBatteryNotLow: true,
|
||||
),
|
||||
existingWorkPolicy: ExistingPeriodicWorkPolicy.keep,
|
||||
backoffPolicy: BackoffPolicy.exponential,
|
||||
initialDelay: const Duration(minutes: 15),
|
||||
);
|
||||
}
|
||||
|
||||
/// Initialize local notifications plugin
|
||||
Future<void> _initializeNotifications() async {
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
const initializationSettingsAndroid = AndroidInitializationSettings(
|
||||
'@mipmap/ic_launcher',
|
||||
);
|
||||
const initializationSettings = InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (response) {
|
||||
// Notification tapped - app will open to main screen
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel background update checks
|
||||
Future<void> cancelBackgroundChecks() async {
|
||||
await Workmanager().cancelByUniqueName(kBackgroundUpdateTaskId);
|
||||
}
|
||||
|
||||
/// Trigger an immediate background check (for testing)
|
||||
Future<void> triggerImmediateCheck() async {
|
||||
await Workmanager().registerOneOffTask(
|
||||
'${kBackgroundUpdateTaskId}_immediate',
|
||||
kBackgroundUpdateTaskName,
|
||||
constraints: Constraints(networkType: NetworkType.connected),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for the background update service
|
||||
final backgroundUpdateServiceProvider = Provider<BackgroundUpdateService>(
|
||||
BackgroundUpdateService.new,
|
||||
);
|
||||
@@ -172,8 +172,8 @@ final profileServiceProvider = Provider((ref) => ProfileService(ref));
|
||||
/// - Returns local data immediately if available
|
||||
/// - Reacts to profile updates from batch fetches via trigger mechanism
|
||||
/// - Streams updates when profile data changes
|
||||
final profileProvider =
|
||||
AutoDisposeProvider.family<AsyncValue<Profile?>, String>((ref, pubkey) {
|
||||
final profileProvider = AutoDisposeProvider.family<AsyncValue<Profile?>, String>(
|
||||
(ref, pubkey) {
|
||||
// Watch update trigger to react when profiles are saved by fetchProfiles()
|
||||
ref.watch(_profileUpdateTriggerProvider.select((state) => state[pubkey]));
|
||||
|
||||
@@ -199,4 +199,5 @@ final profileProvider =
|
||||
models.isNotEmpty ? models.first : null,
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import Foundation
|
||||
|
||||
import app_links
|
||||
import connectivity_plus
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_darwin
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
@@ -18,6 +19,7 @@ import url_launcher_macos
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
|
||||
@@ -375,6 +375,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "19.5.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.0"
|
||||
flutter_local_notifications_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
flutter_markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1149,6 +1181,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
toastification:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1309,6 +1349,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
workmanager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: workmanager
|
||||
sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.0+3"
|
||||
workmanager_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: workmanager_android
|
||||
sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.0+2"
|
||||
workmanager_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: workmanager_apple
|
||||
sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1+2"
|
||||
workmanager_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: workmanager_platform_interface
|
||||
sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1+1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -54,6 +54,8 @@ dependencies:
|
||||
app_links: ^6.4.1
|
||||
flutter_secure_storage: ^10.0.0
|
||||
markdown: ^7.3.0
|
||||
workmanager: ^0.9.0+3
|
||||
flutter_local_notifications: ^19.5.0
|
||||
|
||||
dependency_overrides:
|
||||
models:
|
||||
|
||||
Reference in New Issue
Block a user