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.