From 0490a3459c63017eadb153f339ea3cf82e925844 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Fri, 17 Jul 2026 16:24:56 -0300 Subject: [PATCH] Remove unused notifications, reintroduce later --- android/app/src/main/AndroidManifest.xml | 1 - .../main/res/drawable-v21/ic_notification.xml | 11 - .../src/main/res/drawable/ic_notification.xml | 11 - lib/main.dart | 5 +- .../background_auto_update_executor.dart | 3 +- .../background_pending_install_store.dart | 2 +- lib/services/background_update_service.dart | 418 +----------------- pubspec.lock | 88 ---- pubspec.yaml | 2 - .../WORK-028-remove-android-notifications.md | 43 ++ 10 files changed, 70 insertions(+), 514 deletions(-) delete mode 100644 android/app/src/main/res/drawable-v21/ic_notification.xml delete mode 100644 android/app/src/main/res/drawable/ic_notification.xml create mode 100644 spec/work/WORK-028-remove-android-notifications.md diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4c97e0b..ba7ee40 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ - diff --git a/android/app/src/main/res/drawable-v21/ic_notification.xml b/android/app/src/main/res/drawable-v21/ic_notification.xml deleted file mode 100644 index 151daea..0000000 --- a/android/app/src/main/res/drawable-v21/ic_notification.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_notification.xml b/android/app/src/main/res/drawable/ic_notification.xml deleted file mode 100644 index 151daea..0000000 --- a/android/app/src/main/res/drawable/ic_notification.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/lib/main.dart b/lib/main.dart index 179b290..117ebce 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -431,7 +431,7 @@ final appInitializationProvider = FutureProvider((ref) async { // Initialize device capabilities (used for dynamic download concurrency) await DeviceCapabilitiesCache.initialize(); - // Record app open time for background notification throttling + // Record app open time (retained for settings / device-key restore) await ref .read(settingsServiceProvider) .update((s) => s.copyWith(lastAppOpened: DateTime.now())); @@ -574,7 +574,7 @@ class _AppLifecycleObserver with WidgetsBindingObserver { final packageManager = _ref.read(packageManagerProvider.notifier); if (state == AppLifecycleState.resumed) { - // Record app open time for background notification throttling + // Record app open time (retained for settings / device-key restore) unawaited(_recordAppOpened()); // Sync installed packages to detect installs that completed while backgrounded @@ -606,7 +606,6 @@ class _AppLifecycleObserver with WidgetsBindingObserver { } /// Record that the user opened the app. - /// This is used to check inactivity for background notifications. Future _recordAppOpened() async { await SettingsService().update( (s) => s.copyWith(lastAppOpened: DateTime.now()), diff --git a/lib/services/background_auto_update_executor.dart b/lib/services/background_auto_update_executor.dart index d3083e1..d9b4772 100644 --- a/lib/services/background_auto_update_executor.dart +++ b/lib/services/background_auto_update_executor.dart @@ -52,8 +52,7 @@ class BackgroundAutoUpdateExecutor { final pending = pendingInstalls[appId]; if (!pkg.canInstallSilently && pending?.hash == target.hash) { - // This exact manual update is already downloaded and verified. It was - // reported when first staged, so avoid downloading and notifying again. + // This exact manual update is already downloaded and verified. continue; } diff --git a/lib/services/background_pending_install_store.dart b/lib/services/background_pending_install_store.dart index 8642262..2f64070 100644 --- a/lib/services/background_pending_install_store.dart +++ b/lib/services/background_pending_install_store.dart @@ -79,7 +79,7 @@ class PendingBackgroundInstall { } } -/// Persists manual background installs until the user taps the notification. +/// Persists manual background installs until the user installs from Updates. class BackgroundPendingInstallStore { static const _fileName = 'background_pending_installs.json'; diff --git a/lib/services/background_update_service.dart b/lib/services/background_update_service.dart index 150c17b..b4cf448 100644 --- a/lib/services/background_update_service.dart +++ b/lib/services/background_update_service.dart @@ -5,30 +5,24 @@ import 'dart:ui' as ui; import 'dart:ui' show PlatformDispatcher; import 'package:background_downloader/background_downloader.dart' hide Request; -import 'package:flutter/foundation.dart' show FlutterError, kDebugMode; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter/foundation.dart' show FlutterError; import 'package:flutter/widgets.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:go_router/go_router.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/router.dart'; import 'package:zapstore/services/background_auto_update_executor.dart'; import 'package:zapstore/services/background_native_installer.dart'; -import 'package:zapstore/services/background_pending_install_store.dart'; import 'package:zapstore/services/log_service.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/services/catalog_fetcher.dart'; import 'package:zapstore/services/settings_service.dart'; -import 'package:zapstore/utils/extensions.dart'; -/// Unique task name for background update checking +/// Legacy notification-only task name (cancelled on init; no longer registered). const kBackgroundUpdateTaskName = 'dev.zapstore.backgroundUpdateCheck'; /// Unique task name for unmetered background auto-updates @@ -37,7 +31,7 @@ const kBackgroundAutoUpdateTaskName = 'dev.zapstore.backgroundAutoUpdate'; /// Unique task name for weekly cleanup const kWeeklyCleanupTaskName = 'dev.zapstore.weeklyCleanup'; -/// Unique task identifier +/// Legacy notification-only task id (cancelled on init; no longer registered). const kBackgroundUpdateTaskId = 'backgroundUpdateCheck'; /// Unique task identifier for unmetered background auto-updates @@ -49,28 +43,9 @@ const kBackgroundAutoUpdateImmediateTaskId = 'backgroundAutoUpdateImmediate'; /// Unique task identifier for weekly cleanup const kWeeklyCleanupTaskId = 'weeklyCleanup'; -/// Notification channel for update notifications -const kUpdateNotificationChannelId = 'zapstore_updates'; -const kUpdateNotificationChannelName = 'App Updates'; -const kUpdateNotificationChannelDescription = - 'Notifications for available app updates'; - /// Stale download threshold for cleanup const _staleDownloadThreshold = Duration(days: 7); -/// How long user must be inactive before showing background notification -const _inactivityThreshold = Duration(hours: 24); - -/// Notification payload for deep linking to updates screen -const _kNotificationPayload = 'updates'; - -/// Notification payload prefix for ready-to-install manual background updates. -/// Format: `background-ready:appId1,appId2` -const _kNotificationPayloadReadyPrefix = 'background-ready:'; - -/// Notification payload when auto-update ran but needs no user install action. -const _kNotificationPayloadResults = 'background-results'; - /// Input data key for AppCatalog relay URLs const kAppCatalogRelaysKey = 'appCatalogRelays'; @@ -141,32 +116,15 @@ void callbackDispatcher() { try { switch (task) { case kBackgroundUpdateTaskName: - final relayUrls = - (inputData?[kAppCatalogRelaysKey] as List?) - ?.cast() - .toSet(); - return await _checkForUpdatesInBackground( - relayUrls, - autoUpdateWorker: false, - ); + // Legacy notification-only worker — no longer does work. + return true; case kBackgroundAutoUpdateTaskName: - final relayUrls = - (inputData?[kAppCatalogRelaysKey] as List?) - ?.cast() - .toSet(); - return await _checkForUpdatesInBackground( - relayUrls, - autoUpdateWorker: true, - ); case kBackgroundAutoUpdateImmediateTaskId: final relayUrls = (inputData?[kAppCatalogRelaysKey] as List?) ?.cast() .toSet(); - return await _checkForUpdatesInBackground( - relayUrls, - autoUpdateWorker: true, - ); + return await _checkForUpdatesInBackground(relayUrls); case kWeeklyCleanupTaskName: return await _performWeeklyCleanup(); default: @@ -269,22 +227,16 @@ Future _performWeeklyCleanup() async { } } -/// Background update check logic - runs in a separate isolate via WorkManager. +/// Background auto-update check — runs in a separate isolate via WorkManager. /// /// [appCatalogRelays] - Relay URLs resolved from main isolate. Falls back to /// default relay if not provided. -Future _checkForUpdatesInBackground( - Set? appCatalogRelays, { - required bool autoUpdateWorker, -}) async { +Future _checkForUpdatesInBackground(Set? appCatalogRelays) async { try { final relays = appCatalogRelays ?? {'wss://relay.zapstore.dev'}; final settings = await SettingsService().load(); - // Notification-only checks keep their existing connected-network schedule. - // Auto-update work runs in a separate worker constrained to unmetered - // networks. Exactly one worker proceeds for the current setting. - if (settings.backgroundAutoUpdatesEnabled != autoUpdateWorker) { + if (!settings.backgroundAutoUpdatesEnabled) { return true; } @@ -346,28 +298,18 @@ Future _checkForUpdatesInBackground( source: const LocalSource(), ); - if (autoUpdateWorker) { - final displayNames = { - for (final app in updatableApps) - app.identifier: app.name ?? app.identifier, - }; - final result = await BackgroundAutoUpdateExecutor.run( - updatableInstallables: updatableInstallables, - installed: pmState.installed, - displayNames: displayNames, - ); - await BackgroundNativeInstaller.notifyBackgroundUpdatesCompleted( - result.updatedAppIds, - ); - if (result.hasWork) { - await _showAutoUpdateResultNotification(result, updatableApps); - } - } else { - await _showUpdateNotificationIfNeeded( - updatableApps, - updatableInstallables, - ); - } + final displayNames = { + for (final app in updatableApps) + app.identifier: app.name ?? app.identifier, + }; + final result = await BackgroundAutoUpdateExecutor.run( + updatableInstallables: updatableInstallables, + installed: pmState.installed, + displayNames: displayNames, + ); + await BackgroundNativeInstaller.notifyBackgroundUpdatesCompleted( + result.updatedAppIds, + ); } return true; @@ -379,184 +321,6 @@ Future _checkForUpdatesInBackground( } } -/// Show a local notification for available updates. -/// Only notifies if: -/// 1. User hasn't opened app in 24+ hours -/// 2. There are updates with installable.createdAt > seenUntil AND > lastOpened -/// (new since both last notification AND last time user saw the app) -/// -/// [installables] maps app identifier → the installable (SoftwareAsset or -/// FileMetadata) that represents the available update. Its `createdAt` is used -/// for freshness checks instead of `app.latestRelease.value` which is not -/// populated in the background isolate context. -Future _showUpdateNotificationIfNeeded( - List updates, - Map installables, -) async { - final settingsService = SettingsService(); - final settings = await settingsService.load(); - - // Skip if user recently opened the app - if (settings.lastAppOpened != null && - DateTime.now().difference(settings.lastAppOpened!) < - _inactivityThreshold) { - return; - } - - // Get the "seen until" timestamp - updates with createdAt > this are new - final seenUntil = settings.seenUntil; - - // Filter to only updates that are genuinely new: - // - installable.createdAt > seenUntil (not already notified via background) - // - installable.createdAt > lastOpened (not already seen when user opened app) - // This prevents nagging about updates user saw in the app but chose to ignore - final newUpdates = updates.where((app) { - final installable = installables[app.identifier]; - if (installable == null) return false; - final releaseTime = installable.createdAt; - - // Must be newer than last notification (if any) - if (seenUntil != null && !releaseTime.isAfter(seenUntil)) { - return false; - } - - // Must be newer than last app open (if any) - user may have seen it in UI - if (settings.lastAppOpened != null && - !releaseTime.isAfter(settings.lastAppOpened!)) { - return false; - } - - return true; - }).toList(); - - if (newUpdates.isEmpty) { - return; // No new updates to notify about - } - - // Show the notification - final plugin = FlutterLocalNotificationsPlugin(); - const initSettings = InitializationSettings( - android: AndroidInitializationSettings('@drawable/ic_notification'), - ); - await plugin.initialize(initSettings); - await _ensureUpdateNotificationChannel(plugin); - - final appNames = newUpdates.map((a) => a.name ?? a.identifier).toList(); - final title = newUpdates.length == 1 - ? '1 app update available' - : '${newUpdates.length} app updates available'; - final body = appNames.length <= 3 - ? appNames.join(', ') - : '${appNames.take(3).join(', ')} and ${appNames.length - 3} more'; - - await plugin.show( - 0, - title, - body, - const NotificationDetails( - android: AndroidNotificationDetails( - kUpdateNotificationChannelId, - kUpdateNotificationChannelName, - channelDescription: kUpdateNotificationChannelDescription, - importance: Importance.defaultImportance, - priority: Priority.defaultPriority, - showWhen: true, - autoCancel: true, - ), - ), - payload: _kNotificationPayload, - ); - - // Update seenUntil to now - future checks will only notify about releases after this - await settingsService.update((s) => s.copyWith(seenUntil: DateTime.now())); -} - -/// Show a notification summarizing background auto-update results. -Future _showAutoUpdateResultNotification( - BackgroundAutoUpdateResult result, - List updatableApps, -) async { - final plugin = FlutterLocalNotificationsPlugin(); - const initSettings = InitializationSettings( - android: AndroidInitializationSettings('@drawable/ic_notification'), - ); - await plugin.initialize(initSettings); - await _ensureUpdateNotificationChannel(plugin); - - final nameById = { - for (final app in updatableApps) app.identifier: app.name ?? app.identifier, - }; - - String name(String appId) => nameById[appId] ?? appId; - - final parts = []; - if (result.updatedAppIds.isNotEmpty) { - final names = result.updatedAppIds.map(name).toList(); - parts.add( - result.updatedAppIds.length == 1 - ? 'Updated ${names.first}' - : 'Updated ${names.length} apps: ${_formatNameList(names)}', - ); - } - if (result.readyAppIds.isNotEmpty) { - final names = result.readyAppIds.map(name).toList(); - parts.add( - result.readyAppIds.length == 1 - ? '${names.first} is ready to update — tap to install' - : '${names.length} apps ready to update — tap to install', - ); - } - if (result.failedAppIds.isNotEmpty) { - parts.add( - result.failedAppIds.length == 1 - ? 'Failed to update ${name(result.failedAppIds.first)}' - : 'Failed to update ${result.failedAppIds.length} apps', - ); - } - - final body = parts.join('\n'); - - final title = switch (( - result.updatedAppIds.isNotEmpty, - result.readyAppIds.isNotEmpty, - )) { - (true, true) => 'Updates applied', - (true, false) => - result.updatedAppIds.length == 1 ? 'App updated' : 'Apps updated', - (false, true) => - result.readyAppIds.length == 1 ? 'Update ready' : 'Updates ready', - _ => 'Update check finished', - }; - - final payload = result.readyAppIds.isNotEmpty - ? '$_kNotificationPayloadReadyPrefix${result.readyAppIds.join(',')}' - : _kNotificationPayloadResults; - - await plugin.show( - 0, - title, - body, - NotificationDetails( - android: AndroidNotificationDetails( - kUpdateNotificationChannelId, - kUpdateNotificationChannelName, - channelDescription: kUpdateNotificationChannelDescription, - importance: Importance.defaultImportance, - priority: Priority.defaultPriority, - showWhen: true, - autoCancel: true, - styleInformation: BigTextStyleInformation(body), - ), - ), - payload: payload, - ); -} - -String _formatNameList(List names) { - if (names.length <= 3) return names.join(', '); - return '${names.take(3).join(', ')} and ${names.length - 3} more'; -} - /// Service for managing background update checks class BackgroundUpdateService { BackgroundUpdateService(this.ref); @@ -576,32 +340,14 @@ class BackgroundUpdateService { // Initialize WorkManager await Workmanager().initialize(callbackDispatcher); - // Initialize local notifications - await _initializeNotifications(); + // Drop any previously scheduled notification-only checks. + await Workmanager().cancelByUniqueName(kBackgroundUpdateTaskId); // Resolve AppCatalog relays from main isolate to pass to background task final appCatalogRelays = await ref .read(storageNotifierProvider.notifier) .resolveRelays('AppCatalog'); - // Register periodic task (minimum 15 minutes on Android) - // We use 24 hours to match the inactivity threshold for notifications. - // More frequent checks would be wasted since we only notify users - // who haven't opened the app in 24+ hours. - await Workmanager().registerPeriodicTask( - kBackgroundUpdateTaskId, - kBackgroundUpdateTaskName, - frequency: const Duration(hours: 24), - constraints: Constraints( - networkType: NetworkType.connected, - requiresBatteryNotLow: true, - ), - existingWorkPolicy: ExistingPeriodicWorkPolicy.keep, - backoffPolicy: BackoffPolicy.exponential, - initialDelay: const Duration(hours: 1), - inputData: {kAppCatalogRelaysKey: appCatalogRelays.toList()}, - ); - await _registerAutoUpdatePeriodicTask( appCatalogRelays, initialDelay: const Duration(hours: 1), @@ -680,104 +426,6 @@ class BackgroundUpdateService { ); } - /// Initialize local notifications plugin and request permission (release/profile only). - Future _initializeNotifications() async { - // Request notification permission on Android 13+ (API 33+); skip in debug / dev runs. - if (Platform.isAndroid && !kDebugMode) { - final status = await Permission.notification.status; - if (!status.isGranted) { - await Permission.notification.request(); - } - } - - final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - - const initializationSettingsAndroid = AndroidInitializationSettings( - '@drawable/ic_notification', - ); - const initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - ); - - await flutterLocalNotificationsPlugin.initialize( - initializationSettings, - onDidReceiveNotificationResponse: _handleNotificationTap, - ); - - // Check if app was launched from a notification (terminated state) - final launchDetails = await flutterLocalNotificationsPlugin - .getNotificationAppLaunchDetails(); - if (launchDetails?.didNotificationLaunchApp == true) { - final payload = launchDetails?.notificationResponse?.payload; - if (payload != null) { - _handleNotificationPayload(payload); - } - } - - await _ensureUpdateNotificationChannel(flutterLocalNotificationsPlugin); - } - - /// Handle notification tap - navigate or launch pending installs - static void _handleNotificationTap(NotificationResponse response) { - final payload = response.payload; - if (payload == null || payload.isEmpty) return; - _handleNotificationPayload(payload); - } - - static void _handleNotificationPayload(String payload) { - if (payload.startsWith(_kNotificationPayloadReadyPrefix)) { - final raw = payload.substring(_kNotificationPayloadReadyPrefix.length); - final appIds = raw.split(',').where((id) => id.isNotEmpty).toList(); - // A notification can cold-start the app before its Activity, method - // channels, and router have finished attaching. Defer to the next frame - // so the prepared install can safely launch Android system UI. - WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(launchPendingBackgroundInstalls(appIds)); - }); - return; - } - - if (payload == _kNotificationPayload || - payload == _kNotificationPayloadResults) { - _navigateToUpdates(); - } - } - - /// Launch Android install dialogs for manual updates prepared in the background. - static Future launchPendingBackgroundInstalls( - List appIds, - ) async { - final pending = await BackgroundPendingInstallStore.loadAll(); - final targets = appIds.isEmpty ? pending.keys.toList() : appIds; - - for (final appId in targets) { - final install = pending[appId]; - if (install == null) continue; - - await BackgroundNativeInstaller.launchPreparedInstall( - appId: install.appId, - filePath: install.filePath, - expectedHash: install.hash, - expectedSize: install.size, - expectedCertHashes: install.certificateHashes, - ); - } - - final context = rootNavigatorKey.currentContext; - if (context != null && context.mounted) { - GoRouter.of(context).go('/updates'); - } - } - - /// Navigate to the updates screen - static void _navigateToUpdates() { - // Use the root navigator key to navigate - final context = rootNavigatorKey.currentContext; - if (context != null) { - GoRouter.of(context).go('/updates'); - } - } - /// Cancel background update checks Future cancelBackgroundChecks() async { await Future.wait([ @@ -787,26 +435,6 @@ class BackgroundUpdateService { } } -Future _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.new, diff --git a/pubspec.lock b/pubspec.lock index 5ebbad5..2951be0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -383,38 +383,6 @@ 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: @@ -791,54 +759,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.5" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 - url: "https://pub.dev" - source: hosted - version: "12.0.1" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.dev" - source: hosted - version: "13.0.1" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.dev" - source: hosted - version: "9.4.7" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" - source: hosted - version: "0.1.3+5" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.dev" - source: hosted - version: "4.3.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" petitparser: dependency: transitive description: @@ -1163,14 +1083,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" - timezone: - dependency: transitive - description: - name: timezone - sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 - url: "https://pub.dev" - source: hosted - version: "0.10.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c9b3570..27affc0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,6 @@ dependencies: auto_size_text: ^3.0.0 percent_indicator: ^4.2.5 flutter_layout_grid: ^2.0.8 - permission_handler: ^12.0.1 equatable: ^2.0.7 state_notifier: ^1.0.0 gap: ^3.0.1 @@ -56,7 +55,6 @@ dependencies: flutter_secure_storage: ^9.2.4 # DO NOT UPDATE TO BROKEN ^10.0.0 markdown: ^7.3.0 workmanager: ^0.9.0+3 - flutter_local_notifications: ^19.5.0 device_info_plus: ^12.0.0 archive: ^4.0.7 flutter_slidable: ^4.0.3 diff --git a/spec/work/WORK-028-remove-android-notifications.md b/spec/work/WORK-028-remove-android-notifications.md new file mode 100644 index 0000000..7918462 --- /dev/null +++ b/spec/work/WORK-028-remove-android-notifications.md @@ -0,0 +1,43 @@ +# WORK-028 — Remove Android System Notifications + +**Feature:** FEAT-002-background-notifications.md, FEAT-007-background-auto-updates.md +**Status:** Complete + +## Tasks + +- [x] 1. Remove `POST_NOTIFICATIONS` from AndroidManifest + - Files: `android/app/src/main/AndroidManifest.xml` +- [x] 2. Remove runtime notification permission request and local-notification plumbing + - Files: `lib/services/background_update_service.dart` +- [x] 3. Drop unused packages and notification icons + - Files: `pubspec.yaml`, `android/app/src/main/res/drawable*/ic_notification.xml` +- [x] 4. Stop registering the notification-only background check worker + - Notes: With no notifications, the FEAT-002 worker has no user-visible effect +- [x] 5. Self-review against INVARIANTS.md + +## Test Coverage + +| Scenario | Expected | Status | +|----------|----------|--------| +| Cold start (release) | No Android notification permission dialog | [ ] | +| Background auto-updates ON | Silent installs still run; no result notification | [ ] | +| Background auto-updates OFF | No availability notification | [ ] | + +## Decisions + +### 2026-07-17 — Remove system notifications entirely + +**Context:** Product request to remove the Android “Allow notifications?” prompt. +**Options:** (A) Stop requesting only, keep permission + show paths; (B) Remove permission + request + show paths. +**Decision:** B — remove both the runtime request and `POST_NOTIFICATIONS`, and delete show/tap plumbing so failures are not silent. +**Rationale:** Without the permission, posts cannot succeed on Android 13+; leaving show paths would violate UX Safety (silent failures). + +## Spec Issues + +- **Issue:** FEAT-002 and FEAT-007 require system notifications (availability alerts, result summaries, tap-to-install for staged manual updates). +- **Question:** Product chose removal anyway. Feature specs remain human-owned and still describe the old behavior until updated. +- **Mitigation in code:** Keep WorkManager auto-update execution and pending-install staging; drop notification-only checks and all `flutter_local_notifications` usage. Staged manual updates remain on disk for the Updates screen / next user-driven install (per FEAT-007 edge case when notification is never tapped). + +## Progress Notes + +**2026-07-17:** Removed `POST_NOTIFICATIONS`, runtime request, `flutter_local_notifications` / `permission_handler`, notification show/tap paths, and notification-only WorkManager registration. Background auto-update execution kept.