Skip to content

Commit c783fd4

Browse files
authored
Patch background notification (#730)
1 parent 5a787b8 commit c783fd4

File tree

13 files changed

+584
-630
lines changed

13 files changed

+584
-630
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,34 @@
66
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
77
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
88
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
9-
<uses-permission
10-
android:name="android.permission.READ_EXTERNAL_STORAGE"
11-
android:maxSdkVersion="32" />
12-
<uses-permission
13-
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
14-
android:maxSdkVersion="32" />
9+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
10+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
1511

16-
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
17-
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
18-
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
12+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
13+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
14+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
15+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
16+
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
17+
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
18+
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
1919

20-
<application
21-
android:label="White Noise"
22-
android:name="${applicationName}"
23-
android:icon="@mipmap/ic_launcher">
24-
<activity
25-
android:name=".MainActivity"
26-
android:exported="true"
27-
android:launchMode="singleTop"
28-
android:taskAffinity=""
29-
android:theme="@style/LaunchTheme"
30-
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
31-
android:hardwareAccelerated="true"
32-
android:windowSoftInputMode="adjustResize">
20+
<application android:label="White Noise" android:name="${applicationName}" android:icon="@mipmap/ic_launcher">
21+
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
3322
<!-- Specifies an Android theme to apply to this Activity as soon as
3423
the Android process has started. This theme is visible to the user
3524
while the Flutter UI initializes. After that, this theme continues
3625
to determine the Window background behind the Flutter UI. -->
37-
<meta-data
38-
android:name="io.flutter.embedding.android.NormalTheme"
39-
android:resource="@style/NormalTheme"
40-
/>
26+
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
4127
<intent-filter>
4228
<action android:name="android.intent.action.MAIN"/>
4329
<category android:name="android.intent.category.LAUNCHER"/>
4430
</intent-filter>
4531
</activity>
32+
<!-- Foreground service -->
33+
<service android:name="com.pravera.flutter_foreground_task.service.ForegroundService" android:foregroundServiceType="dataSync|remoteMessaging" android:exported="false" />
4634
<!-- Don't delete the meta-data below.
4735
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
48-
<meta-data
49-
android:name="flutterEmbedding"
50-
android:value="2" />
36+
<meta-data android:name="flutterEmbedding" android:value="2" />
5137
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
5238
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
5339
<intent-filter>

ios/Podfile.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ PODS:
44
- emoji_picker_flutter (0.0.1):
55
- Flutter
66
- Flutter (1.0.0)
7+
- flutter_foreground_task (0.0.1):
8+
- Flutter
79
- flutter_local_notifications (0.0.1):
810
- Flutter
911
- flutter_native_splash (2.4.3):
@@ -44,6 +46,7 @@ DEPENDENCIES:
4446
- audio_session (from `.symlinks/plugins/audio_session/ios`)
4547
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
4648
- Flutter (from `Flutter`)
49+
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
4750
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
4851
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
4952
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
@@ -67,6 +70,8 @@ EXTERNAL SOURCES:
6770
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
6871
Flutter:
6972
:path: Flutter
73+
flutter_foreground_task:
74+
:path: ".symlinks/plugins/flutter_foreground_task/ios"
7075
flutter_local_notifications:
7176
:path: ".symlinks/plugins/flutter_local_notifications/ios"
7277
flutter_native_splash:

ios/Runner/AppDelegate.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ private let metadataRefreshTaskId = "com.whitenoise.metadata_refresh"
1717
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
1818
GeneratedPluginRegistrant.register(with: registry)
1919
}
20-
20+
SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback { registry in
21+
GeneratedPluginRegistrant.register(with: registry)
22+
}
23+
2124
if #available(iOS 10.0, *) {
2225
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
2326
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#import "GeneratedPluginRegistrant.h"
2+
#import <flutter_foreground_task/FlutterForegroundTaskPlugin.h>
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
2+
import 'package:logging/logging.dart';
3+
import 'package:whitenoise/domain/services/message_sync_service.dart';
4+
import 'package:whitenoise/domain/services/notification_service.dart';
5+
import 'package:whitenoise/src/rust/api/accounts.dart';
6+
import 'package:whitenoise/src/rust/api/groups.dart';
7+
import 'package:whitenoise/src/rust/api/messages.dart' show fetchAggregatedMessagesForGroup;
8+
import 'package:whitenoise/src/rust/api/welcomes.dart' show pendingWelcomes;
9+
import 'package:whitenoise/src/rust/frb_generated.dart';
10+
11+
class BackgroundSyncHandler extends TaskHandler {
12+
final _log = Logger('BackgroundSyncHandler');
13+
14+
@override
15+
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
16+
try {
17+
await NotificationService.initialize();
18+
await RustLib.init();
19+
_log.info('BackgroundSyncHandler initialized at $timestamp');
20+
} catch (e, stackTrace) {
21+
_log.severe('Error initializing BackgroundSyncHandler: $e', e, stackTrace);
22+
}
23+
}
24+
25+
@override
26+
Future<void> onRepeatEvent(DateTime timestamp) async {
27+
try {
28+
_log.fine('Background sync started at $timestamp');
29+
await _syncMessagesForAllAccounts();
30+
await _syncInvitesForAllAccounts();
31+
_log.fine('Background sync completed at $timestamp');
32+
} catch (e, stackTrace) {
33+
_log.severe('Error in onRepeatEvent: $e', e, stackTrace);
34+
}
35+
FlutterForegroundTask.sendDataToMain(timestamp.millisecondsSinceEpoch);
36+
}
37+
38+
Future<void> _syncMessagesForAllAccounts() async {
39+
try {
40+
final accounts = await getAccounts();
41+
if (accounts.isEmpty) {
42+
_log.fine('No accounts found, skipping message sync');
43+
return;
44+
}
45+
_log.fine('Syncing messages for ${accounts.length} account(s)');
46+
for (final account in accounts) {
47+
try {
48+
await _syncMessagesForAccount(account.pubkey);
49+
} catch (e, stackTrace) {
50+
_log.warning('Message sync failed for ${account.pubkey}: $e', e, stackTrace);
51+
}
52+
}
53+
} catch (e, stackTrace) {
54+
_log.warning('Error syncing messages for all accounts: $e', e, stackTrace);
55+
}
56+
}
57+
58+
Future<void> _syncMessagesForAccount(String accountPubkey) async {
59+
try {
60+
final groups = await activeGroups(pubkey: accountPubkey);
61+
_log.fine('Found ${groups.length} active group(s) for account $accountPubkey');
62+
for (final group in groups) {
63+
await _syncMessagesForGroup(
64+
accountPubkey: accountPubkey,
65+
groupId: group.mlsGroupId,
66+
);
67+
}
68+
} catch (e, stackTrace) {
69+
_log.warning('Error syncing messages for account $accountPubkey: $e', e, stackTrace);
70+
}
71+
}
72+
73+
Future<void> _syncMessagesForGroup({
74+
required String accountPubkey,
75+
required String groupId,
76+
}) async {
77+
try {
78+
final lastSyncTime = await MessageSyncService.getLastMessageSyncTime(
79+
activePubkey: accountPubkey,
80+
groupId: groupId,
81+
);
82+
final messages = await fetchAggregatedMessagesForGroup(
83+
pubkey: accountPubkey,
84+
groupId: groupId,
85+
);
86+
87+
final newMessages = await MessageSyncService.filterNewMessages(
88+
messages,
89+
accountPubkey,
90+
groupId,
91+
lastSyncTime,
92+
);
93+
if (newMessages.isNotEmpty) {
94+
_log.info('Found ${newMessages.length} new message(s) in group $groupId');
95+
await MessageSyncService.notifyNewMessages(
96+
groupId: groupId,
97+
activePubkey: accountPubkey,
98+
newMessages: newMessages,
99+
);
100+
try {
101+
await MessageSyncService.setLastMessageSyncTime(
102+
activePubkey: accountPubkey,
103+
groupId: groupId,
104+
time: DateTime.now(),
105+
);
106+
} catch (e, stackTrace) {
107+
_log.warning(
108+
'Failed to update sync time for group $groupId after notification. '
109+
'This may cause duplicate notifications on next sync: $e',
110+
e,
111+
stackTrace,
112+
);
113+
}
114+
}
115+
} catch (e, stackTrace) {
116+
_log.warning('Error syncing messages for group $groupId: $e', e, stackTrace);
117+
}
118+
}
119+
120+
Future<void> _syncInvitesForAllAccounts() async {
121+
try {
122+
final accounts = await getAccounts();
123+
if (accounts.isEmpty) {
124+
_log.fine('No accounts found, skipping invite sync');
125+
return;
126+
}
127+
_log.fine('Syncing invites for ${accounts.length} account(s)');
128+
for (final account in accounts) {
129+
await _syncInvitesForAccount(account.pubkey);
130+
}
131+
} catch (e, stackTrace) {
132+
_log.warning('Error syncing invites for all accounts: $e', e, stackTrace);
133+
}
134+
}
135+
136+
Future<void> _syncInvitesForAccount(String accountPubkey) async {
137+
try {
138+
final lastSyncTime = await MessageSyncService.getLastInviteSyncTime(
139+
activePubkey: accountPubkey,
140+
);
141+
final welcomes = await pendingWelcomes(pubkey: accountPubkey);
142+
if (welcomes.isEmpty) {
143+
_log.fine('No pending invites found for account $accountPubkey, skipping invite sync');
144+
return;
145+
}
146+
147+
_log.fine('Found ${welcomes.length} pending welcome(s) for account $accountPubkey');
148+
149+
final newWelcomes = await MessageSyncService.filterNewInvites(
150+
welcomes: welcomes,
151+
currentUserPubkey: accountPubkey,
152+
lastSyncTime: lastSyncTime,
153+
);
154+
155+
if (newWelcomes.isNotEmpty) {
156+
_log.info(
157+
'Found ${newWelcomes.length} new invite(s) for account $accountPubkey (${welcomes.length} total pending)',
158+
);
159+
160+
await MessageSyncService.notifyNewInvites(
161+
newWelcomes: newWelcomes,
162+
);
163+
164+
await MessageSyncService.setLastInviteSyncTime(
165+
activePubkey: accountPubkey,
166+
time: DateTime.now(),
167+
);
168+
}
169+
} catch (e, stackTrace) {
170+
_log.warning('Error syncing invites for account $accountPubkey: $e', e, stackTrace);
171+
}
172+
}
173+
174+
@override
175+
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
176+
_log.info('Foreground task destroyed at $timestamp, isTimeout: $isTimeout');
177+
}
178+
}
179+
180+
@pragma('vm:entry-point')
181+
void startCallback() {
182+
FlutterForegroundTask.setTaskHandler(BackgroundSyncHandler());
183+
}

0 commit comments

Comments
 (0)