mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Remove unused notifications, reintroduce later
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
|
||||
<uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
|
||||
<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" />
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="19"
|
||||
android:viewportHeight="32">
|
||||
<!-- Zapstore icon for notifications -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M18.8379,13.9711L8.84956,0.356086C8.30464,-0.386684 7.10438,0.128479 7.30103,1.02073L9.04686,8.94232C9.16268,9.46783 8.74887,9.96266 8.19641,9.9593L0.871032,9.91477C0.194934,9.91066 -0.223975,10.6293 0.126748,11.1916L7.69743,23.3297C7.99957,23.8141 7.73264,24.4447 7.16744,24.5816L5.40958,25.0076C4.70199,25.179 4.51727,26.0734 5.10186,26.4974L12.4572,31.8326C12.9554,32.194 13.6711,31.9411 13.8147,31.3529L15.8505,23.0152C16.0137,22.3465 15.3281,21.7801 14.6762,22.0452L13.0661,22.7001C12.5619,22.9052 11.991,22.6092 11.8849,22.0877L10.7521,16.5224C10.6486,16.014 11.038,15.5365 11.5704,15.5188L18.1639,15.2998C18.8529,15.2769 19.2383,14.517 18.8379,13.9711Z"/>
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="19"
|
||||
android:viewportHeight="32">
|
||||
<!-- Zapstore icon for notifications -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M18.8379,13.9711L8.84956,0.356086C8.30464,-0.386684 7.10438,0.128479 7.30103,1.02073L9.04686,8.94232C9.16268,9.46783 8.74887,9.96266 8.19641,9.9593L0.871032,9.91477C0.194934,9.91066 -0.223975,10.6293 0.126748,11.1916L7.69743,23.3297C7.99957,23.8141 7.73264,24.4447 7.16744,24.5816L5.40958,25.0076C4.70199,25.179 4.51727,26.0734 5.10186,26.4974L12.4572,31.8326C12.9554,32.194 13.6711,31.9411 13.8147,31.3529L15.8505,23.0152C16.0137,22.3465 15.3281,21.7801 14.6762,22.0452L13.0661,22.7001C12.5619,22.9052 11.991,22.6092 11.8849,22.0877L10.7521,16.5224C10.6486,16.014 11.038,15.5365 11.5704,15.5188L18.1639,15.2998C18.8529,15.2769 19.2383,14.517 18.8379,13.9711Z"/>
|
||||
</vector>
|
||||
+2
-3
@@ -431,7 +431,7 @@ final appInitializationProvider = FutureProvider<void>((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<void> _recordAppOpened() async {
|
||||
await SettingsService().update(
|
||||
(s) => s.copyWith(lastAppOpened: DateTime.now()),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<dynamic>?)
|
||||
?.cast<String>()
|
||||
.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<dynamic>?)
|
||||
?.cast<String>()
|
||||
.toSet();
|
||||
return await _checkForUpdatesInBackground(
|
||||
relayUrls,
|
||||
autoUpdateWorker: true,
|
||||
);
|
||||
case kBackgroundAutoUpdateImmediateTaskId:
|
||||
final relayUrls =
|
||||
(inputData?[kAppCatalogRelaysKey] as List<dynamic>?)
|
||||
?.cast<String>()
|
||||
.toSet();
|
||||
return await _checkForUpdatesInBackground(
|
||||
relayUrls,
|
||||
autoUpdateWorker: true,
|
||||
);
|
||||
return await _checkForUpdatesInBackground(relayUrls);
|
||||
case kWeeklyCleanupTaskName:
|
||||
return await _performWeeklyCleanup();
|
||||
default:
|
||||
@@ -269,22 +227,16 @@ Future<bool> _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<bool> _checkForUpdatesInBackground(
|
||||
Set<String>? appCatalogRelays, {
|
||||
required bool autoUpdateWorker,
|
||||
}) async {
|
||||
Future<bool> _checkForUpdatesInBackground(Set<String>? 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<bool> _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<bool> _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<void> _showUpdateNotificationIfNeeded(
|
||||
List<App> updates,
|
||||
Map<String, Installable> 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<void> _showAutoUpdateResultNotification(
|
||||
BackgroundAutoUpdateResult result,
|
||||
List<App> 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 = <String>[];
|
||||
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<String> 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<void> _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<void> launchPendingBackgroundInstalls(
|
||||
List<String> 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<void> cancelBackgroundChecks() async {
|
||||
await Future.wait([
|
||||
@@ -787,26 +435,6 @@ 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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user