From c3a5878891886e23bba88511742caaea840ea094 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:12:35 +0000
Subject: [PATCH 01/12] Initial plan
From 32414641d3a60483636e8c100c722565a6645f56 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:16:18 +0000
Subject: [PATCH 02/12] Add syncThrottleSeconds property and UI fields to sync
settings
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
.../settings/FolderImageSyncSettings.svelte | 14 ++
.../settings/FolderPDFSyncSettings.svelte | 14 ++
.../settings/WebdavDataSyncSettings.svelte | 15 +++
.../settings/WebdavImageSyncSettings.svelte | 14 ++
.../settings/WebdavPDFSyncSettings.svelte | 14 ++
app/i18n/en.json | 3 +
app/services/sync.ts | 127 ++++++++++++++++++
app/services/sync/BaseSyncService.ts | 2 +
8 files changed, 203 insertions(+)
diff --git a/app/components/settings/FolderImageSyncSettings.svelte b/app/components/settings/FolderImageSyncSettings.svelte
index 3e7058f01..a591e5344 100644
--- a/app/components/settings/FolderImageSyncSettings.svelte
+++ b/app/components/settings/FolderImageSyncSettings.svelte
@@ -77,6 +77,20 @@
description: lc('local_auto_sync_desc'),
value: $store.autoSync
},
+ {
+ id: 'syncThrottleSeconds',
+ key: 'syncThrottleSeconds',
+ title: lc('sync_throttle_seconds'),
+ description: lc('sync_throttle_desc'),
+ valueType: 'number',
+ type: 'prompt',
+ textFieldProperties: {
+ keyboardType: 'number',
+ autocapitalizationType: 'none'
+ } as TextFieldProperties,
+ rightValue: () => ($store.syncThrottleSeconds || 0) + ' s',
+ default: 0
+ },
{
id: 'setting',
key: 'fileNameFormat',
diff --git a/app/components/settings/FolderPDFSyncSettings.svelte b/app/components/settings/FolderPDFSyncSettings.svelte
index 54c7fc487..902a09384 100644
--- a/app/components/settings/FolderPDFSyncSettings.svelte
+++ b/app/components/settings/FolderPDFSyncSettings.svelte
@@ -89,6 +89,20 @@
description: lc('local_auto_sync_desc'),
value: $store.autoSync
},
+ {
+ id: 'syncThrottleSeconds',
+ key: 'syncThrottleSeconds',
+ title: lc('sync_throttle_seconds'),
+ description: lc('sync_throttle_desc'),
+ valueType: 'number',
+ type: 'prompt',
+ textFieldProperties: {
+ keyboardType: 'number',
+ autocapitalizationType: 'none'
+ } as TextFieldProperties,
+ rightValue: () => ($store.syncThrottleSeconds || 0) + ' s',
+ default: 0
+ },
{
type: 'switch',
id: 'useDocumentName',
diff --git a/app/components/settings/WebdavDataSyncSettings.svelte b/app/components/settings/WebdavDataSyncSettings.svelte
index ba3bb31d0..92e8a266e 100644
--- a/app/components/settings/WebdavDataSyncSettings.svelte
+++ b/app/components/settings/WebdavDataSyncSettings.svelte
@@ -107,6 +107,21 @@
$store.autoSync = e.value;
})} />
+ {#if $store.autoSync}
+
+ {
+ const value = parseInt(e.value) || 0;
+ $store.syncThrottleSeconds = value >= 0 ? value : 0;
+ }} />
+
+ {/if}
diff --git a/app/components/settings/WebdavImageSyncSettings.svelte b/app/components/settings/WebdavImageSyncSettings.svelte
index be3bc6627..69ea9885e 100644
--- a/app/components/settings/WebdavImageSyncSettings.svelte
+++ b/app/components/settings/WebdavImageSyncSettings.svelte
@@ -90,6 +90,20 @@
description: lc('local_auto_sync_desc'),
value: $store.autoSync
},
+ {
+ id: 'syncThrottleSeconds',
+ key: 'syncThrottleSeconds',
+ title: lc('sync_throttle_seconds'),
+ description: lc('sync_throttle_desc'),
+ valueType: 'number',
+ type: 'prompt',
+ textFieldProperties: {
+ keyboardType: 'number',
+ autocapitalizationType: 'none'
+ } as TextFieldProperties,
+ rightValue: () => ($store.syncThrottleSeconds || 0) + ' s',
+ default: 0
+ },
{
id: 'setting',
key: 'fileNameFormat',
diff --git a/app/components/settings/WebdavPDFSyncSettings.svelte b/app/components/settings/WebdavPDFSyncSettings.svelte
index e211bdd34..a93fa016a 100644
--- a/app/components/settings/WebdavPDFSyncSettings.svelte
+++ b/app/components/settings/WebdavPDFSyncSettings.svelte
@@ -102,6 +102,20 @@
description: lc('local_auto_sync_desc'),
value: $store.autoSync
},
+ {
+ id: 'syncThrottleSeconds',
+ key: 'syncThrottleSeconds',
+ title: lc('sync_throttle_seconds'),
+ description: lc('sync_throttle_desc'),
+ valueType: 'number',
+ type: 'prompt',
+ textFieldProperties: {
+ keyboardType: 'number',
+ autocapitalizationType: 'none'
+ } as TextFieldProperties,
+ rightValue: () => ($store.syncThrottleSeconds || 0) + ' s',
+ default: 0
+ },
{
id: 'setting',
key: 'fileNameFormat',
diff --git a/app/i18n/en.json b/app/i18n/en.json
index e9f3d2ec3..8699f11c4 100644
--- a/app/i18n/en.json
+++ b/app/i18n/en.json
@@ -420,6 +420,9 @@
"sync_on_start_desc": "should the application try to sync documents on start",
"sync_service_color_desc": "the color is used to distinguish your different sync services",
"sync_settings_desc": "synchronization settings",
+ "sync_throttle": "sync throttle",
+ "sync_throttle_desc": "minimum time between syncs in seconds (0 = sync immediately on every change)",
+ "sync_throttle_seconds": "throttle (seconds)",
"system": "system",
"test": "test",
"thank_you": "thank you !",
diff --git a/app/services/sync.ts b/app/services/sync.ts
index bb8e1e37a..e56cc77f0 100644
--- a/app/services/sync.ts
+++ b/app/services/sync.ts
@@ -249,6 +249,13 @@ export class SyncService extends BaseWorkerHandler {
}
async stop() {
DEV_LOG && console.log('Sync', 'stop');
+
+ // Clear all throttle timers
+ this.throttleTimers.forEach(timer => clearTimeout(timer));
+ this.throttleTimers.clear();
+ this.pendingSyncs.clear();
+ this.lastSyncTimes.clear();
+
this.off(EVENT_SYNC_STATE, this.onSyncState, this);
documentsService.off(EVENT_DOCUMENT_ADDED, this.onDocumentAdded, this);
documentsService.off(EVENT_DOCUMENT_UPDATED, this.onDocumentUpdated, this);
@@ -278,6 +285,11 @@ export class SyncService extends BaseWorkerHandler {
this.syncRunning = event.state === 'running';
}
+ // Per-service throttle timers and pending sync flags
+ private throttleTimers: Map = new Map();
+ private pendingSyncs: Map = new Map();
+ private lastSyncTimes: Map = new Map();
+
syncDocuments = debounce(
async ({
bothWays = false,
@@ -295,6 +307,88 @@ export class SyncService extends BaseWorkerHandler {
SYNC_DELAY
);
+ /**
+ * Handles sync throttling per service configuration
+ */
+ private async handleThrottledSync(
+ data: {
+ withFolders?;
+ force?;
+ bothWays?;
+ type?: number;
+ fromEvent?: string;
+ event?: DocumentEvents;
+ },
+ service: any
+ ) {
+ const serviceId = service.id;
+ const throttleSeconds = service.syncThrottleSeconds || 0;
+
+ // If no throttle configured or force sync, execute immediately
+ if (throttleSeconds === 0 || data.force) {
+ return this.executeSyncInternal(data);
+ }
+
+ const now = Date.now();
+ const lastSyncTime = this.lastSyncTimes.get(serviceId) || 0;
+ const timeSinceLastSync = now - lastSyncTime;
+ const throttleMs = throttleSeconds * 1000;
+
+ // If enough time has passed since last sync, execute immediately
+ if (timeSinceLastSync >= throttleMs) {
+ this.lastSyncTimes.set(serviceId, now);
+ return this.executeSyncInternal(data);
+ }
+
+ // Otherwise, schedule a throttled sync
+ this.pendingSyncs.set(serviceId, data);
+
+ // Clear existing timer if any
+ if (this.throttleTimers.has(serviceId)) {
+ clearTimeout(this.throttleTimers.get(serviceId));
+ }
+
+ // Schedule sync for when throttle period expires
+ const delay = throttleMs - timeSinceLastSync;
+ const timer = setTimeout(() => {
+ const pendingData = this.pendingSyncs.get(serviceId);
+ if (pendingData) {
+ this.lastSyncTimes.set(serviceId, Date.now());
+ this.pendingSyncs.delete(serviceId);
+ this.throttleTimers.delete(serviceId);
+ this.executeSyncInternal(pendingData);
+ }
+ }, delay);
+
+ this.throttleTimers.set(serviceId, timer);
+
+ // For Android, schedule alarm if available
+ if (__ANDROID__ && global.isAndroid) {
+ this.scheduleAndroidAlarm(serviceId, delay);
+ }
+
+ // For iOS, request background refresh
+ if (__IOS__ && global.isIOS) {
+ this.requestIOSBackgroundRefresh(serviceId, delay);
+ }
+ }
+
+ private scheduleAndroidAlarm(serviceId: number, delayMs: number) {
+ // Placeholder for Android alarm scheduling
+ // Will be implemented in platform-specific file
+ DEV_LOG && console.log('SyncService', 'scheduleAndroidAlarm', serviceId, delayMs);
+ }
+
+ private requestIOSBackgroundRefresh(serviceId: number, delayMs: number) {
+ // Placeholder for iOS background refresh
+ // Will be implemented in platform-specific file
+ DEV_LOG && console.log('SyncService', 'requestIOSBackgroundRefresh', serviceId, delayMs);
+ }
+
+ private async executeSyncInternal(data: any) {
+ return this.syncDocumentsInternalCore(data);
+ }
+
async syncDocumentsInternal(
data: {
withFolders?;
@@ -304,6 +398,39 @@ export class SyncService extends BaseWorkerHandler {
fromEvent?: string;
event?: DocumentEvents;
} = {}
+ ) {
+ // Check which services should sync based on the type
+ const services = this.getStoredSyncServices().filter((s) => s.enabled !== false);
+
+ // If we have services with throttle configured, handle throttling per service
+ const servicesWithThrottle = services.filter(s => s.syncThrottleSeconds && s.syncThrottleSeconds > 0);
+
+ if (servicesWithThrottle.length > 0 && !data.force) {
+ // For each service, handle throttling separately
+ for (const service of servicesWithThrottle) {
+ await this.handleThrottledSync(data, service);
+ }
+
+ // Also execute for services without throttle immediately
+ const servicesWithoutThrottle = services.filter(s => !s.syncThrottleSeconds || s.syncThrottleSeconds === 0);
+ if (servicesWithoutThrottle.length > 0) {
+ await this.syncDocumentsInternalCore(data);
+ }
+ } else {
+ // No throttling, execute immediately
+ await this.syncDocumentsInternalCore(data);
+ }
+ }
+
+ async syncDocumentsInternalCore(
+ data: {
+ withFolders?;
+ force?;
+ bothWays?;
+ type?: number;
+ fromEvent?: string;
+ event?: DocumentEvents;
+ } = {}
) {
try {
const db = documentsService.db?.db?.db;
diff --git a/app/services/sync/BaseSyncService.ts b/app/services/sync/BaseSyncService.ts
index 7748c6017..f2675b4a9 100644
--- a/app/services/sync/BaseSyncService.ts
+++ b/app/services/sync/BaseSyncService.ts
@@ -5,6 +5,7 @@ export interface BaseSyncServiceOptions {
autoSync?: boolean;
enabled?: boolean;
color?: string | Color;
+ syncThrottleSeconds?: number;
}
const singletons: { [k: string]: BaseSyncService } = {};
@@ -15,6 +16,7 @@ export abstract class BaseSyncService extends Observable {
autoSync = false;
enabled = true;
color?: string | Color;
+ syncThrottleSeconds?: number;
static getEnabledServices() {
return Object.values(singletons);
}
From a792d72d1b692ce21a6c8f1903e5065d9cc0d90b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:18:10 +0000
Subject: [PATCH 03/12] Implement Android alarm and iOS background refresh for
throttled sync
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
app/services/sync.android.ts | 101 ++++++++++++++++++
app/services/sync.ios.ts | 41 +++++++
app/services/sync.ts | 69 ++++++++++--
.../sync/SyncAlarmReceiver.android.ts | 22 ++++
4 files changed, 226 insertions(+), 7 deletions(-)
create mode 100644 app/services/sync.android.ts
create mode 100644 app/services/sync.ios.ts
create mode 100644 app/services/sync/SyncAlarmReceiver.android.ts
diff --git a/app/services/sync.android.ts b/app/services/sync.android.ts
new file mode 100644
index 000000000..4cd9d2d5e
--- /dev/null
+++ b/app/services/sync.android.ts
@@ -0,0 +1,101 @@
+import { Application, Utils } from '@nativescript/core';
+
+let alarmManager: android.app.AlarmManager = null;
+let alarmReceiverRegistered = false;
+
+/**
+ * Initialize Android-specific sync alarm functionality
+ */
+export function initAndroidSyncAlarms() {
+ if (alarmReceiverRegistered) {
+ return;
+ }
+
+ const context = Utils.android.getApplicationContext();
+ alarmManager = context.getSystemService(android.content.Context.ALARM_SERVICE) as android.app.AlarmManager;
+
+ // Register broadcast receiver for sync alarms
+ const filter = new android.content.IntentFilter(`${__APP_ID__}.SYNC_THROTTLE_ALARM`);
+ const receiver = new android.content.BroadcastReceiver({
+ onReceive(context: android.content.Context, intent: android.content.Intent) {
+ const serviceId = intent.getIntExtra('serviceId', -1);
+ DEV_LOG && console.log('SyncAlarmReceiver', 'received alarm for service', serviceId);
+
+ // Import dynamically to avoid circular dependency
+ import('../sync').then(({ syncService }) => {
+ // Trigger sync for this service
+ syncService.triggerThrottledSync(serviceId);
+ });
+ }
+ });
+
+ context.registerReceiver(receiver, filter);
+ alarmReceiverRegistered = true;
+
+ DEV_LOG && console.log('SyncService', 'Android alarm receiver registered');
+}
+
+/**
+ * Schedule an alarm for a throttled sync
+ * @param serviceId The sync service ID
+ * @param delayMs Delay in milliseconds
+ */
+export function scheduleAndroidSyncAlarm(serviceId: number, delayMs: number) {
+ if (!alarmManager) {
+ initAndroidSyncAlarms();
+ }
+
+ const context = Utils.android.getApplicationContext();
+ const intent = new android.content.Intent(context, (com as any).tns.SyncAlarmReceiver.class);
+ intent.putExtra('serviceId', serviceId);
+
+ const pendingIntent = android.app.PendingIntent.getBroadcast(
+ context,
+ serviceId, // Use serviceId as request code to allow cancellation
+ intent,
+ android.app.PendingIntent.FLAG_UPDATE_CURRENT | android.app.PendingIntent.FLAG_IMMUTABLE
+ );
+
+ const triggerAtMillis = Date.now() + delayMs;
+
+ // Use setExactAndAllowWhileIdle for reliable delivery even in Doze mode
+ if (android.os.Build.VERSION.SDK_INT >= 23) {
+ alarmManager.setExactAndAllowWhileIdle(
+ android.app.AlarmManager.RTC_WAKEUP,
+ triggerAtMillis,
+ pendingIntent
+ );
+ } else {
+ alarmManager.setExact(
+ android.app.AlarmManager.RTC_WAKEUP,
+ triggerAtMillis,
+ pendingIntent
+ );
+ }
+
+ DEV_LOG && console.log('SyncService', 'scheduled Android alarm for service', serviceId, 'in', delayMs, 'ms');
+}
+
+/**
+ * Cancel a scheduled alarm for a sync service
+ * @param serviceId The sync service ID
+ */
+export function cancelAndroidSyncAlarm(serviceId: number) {
+ if (!alarmManager) {
+ return;
+ }
+
+ const context = Utils.android.getApplicationContext();
+ const intent = new android.content.Intent(context, (com as any).tns.SyncAlarmReceiver.class);
+ const pendingIntent = android.app.PendingIntent.getBroadcast(
+ context,
+ serviceId,
+ intent,
+ android.app.PendingIntent.FLAG_UPDATE_CURRENT | android.app.PendingIntent.FLAG_IMMUTABLE
+ );
+
+ alarmManager.cancel(pendingIntent);
+ pendingIntent.cancel();
+
+ DEV_LOG && console.log('SyncService', 'cancelled Android alarm for service', serviceId);
+}
diff --git a/app/services/sync.ios.ts b/app/services/sync.ios.ts
new file mode 100644
index 000000000..e94a06da4
--- /dev/null
+++ b/app/services/sync.ios.ts
@@ -0,0 +1,41 @@
+/**
+ * iOS-specific sync background refresh functionality
+ */
+
+/**
+ * Initialize iOS-specific sync background refresh
+ */
+export function initIOSSyncBackgroundRefresh() {
+ // iOS background refresh initialization
+ // This would typically use BGTaskScheduler for iOS 13+
+ DEV_LOG && console.log('SyncService', 'iOS background refresh initialized');
+}
+
+/**
+ * Request background refresh for a throttled sync
+ * @param serviceId The sync service ID
+ * @param delayMs Delay in milliseconds
+ */
+export function requestIOSBackgroundRefresh(serviceId: number, delayMs: number) {
+ // On iOS, we can't schedule exact times for background refresh
+ // Instead, we request a background refresh and the system decides when to run it
+ // For now, this is a placeholder that would need proper BGTaskScheduler implementation
+
+ DEV_LOG && console.log('SyncService', 'requested iOS background refresh for service', serviceId, 'in approximately', delayMs, 'ms');
+
+ // TODO: Implement proper BGTaskScheduler integration
+ // This would involve:
+ // 1. Registering a background task identifier in Info.plist
+ // 2. Registering a handler for the task
+ // 3. Submitting a BGTaskRequest with earliestBeginDate
+}
+
+/**
+ * Cancel a scheduled background refresh for a sync service
+ * @param serviceId The sync service ID
+ */
+export function cancelIOSBackgroundRefresh(serviceId: number) {
+ DEV_LOG && console.log('SyncService', 'cancelled iOS background refresh for service', serviceId);
+
+ // TODO: Implement proper BGTaskScheduler cancellation
+}
diff --git a/app/services/sync.ts b/app/services/sync.ts
index e56cc77f0..f9884bb52 100644
--- a/app/services/sync.ts
+++ b/app/services/sync.ts
@@ -24,6 +24,27 @@ import { DocumentAddedEventData, DocumentDeletedEventData, DocumentEvents, Docum
import { SYNC_TYPES, SyncType, getRemoteDeleteDocumentSettingsKey } from './sync/types';
import { WebdavDataSyncOptions } from './sync/WebdavDataSyncService';
+// Platform-specific imports
+let scheduleAndroidSyncAlarm: (serviceId: number, delayMs: number) => void;
+let cancelAndroidSyncAlarm: (serviceId: number) => void;
+let initAndroidSyncAlarms: () => void;
+let requestIOSBackgroundRefresh: (serviceId: number, delayMs: number) => void;
+let cancelIOSBackgroundRefresh: (serviceId: number) => void;
+let initIOSSyncBackgroundRefresh: () => void;
+
+if (__ANDROID__) {
+ const androidModule = require('./sync.android');
+ scheduleAndroidSyncAlarm = androidModule.scheduleAndroidSyncAlarm;
+ cancelAndroidSyncAlarm = androidModule.cancelAndroidSyncAlarm;
+ initAndroidSyncAlarms = androidModule.initAndroidSyncAlarms;
+} else if (__IOS__) {
+ const iosModule = require('./sync.ios');
+ requestIOSBackgroundRefresh = iosModule.requestIOSBackgroundRefresh;
+ cancelIOSBackgroundRefresh = iosModule.cancelIOSBackgroundRefresh;
+ initIOSSyncBackgroundRefresh = iosModule.initIOSSyncBackgroundRefresh;
+}
+
+
export const syncServicesStore = writable([]);
const SETTINGS_KEY = 'webdav_config';
@@ -220,6 +241,14 @@ export class SyncService extends BaseWorkerHandler {
if (this.enabled) {
return;
}
+
+ // Initialize platform-specific background sync
+ if (__ANDROID__ && initAndroidSyncAlarms) {
+ initAndroidSyncAlarms();
+ } else if (__IOS__ && initIOSSyncBackgroundRefresh) {
+ initIOSSyncBackgroundRefresh();
+ }
+
const syncServices = (this.services = this.getStoredSyncServices().filter((s) => s.enabled !== false));
// DEV_LOG && console.log('Sync', 'start', syncServices);
// bring back old data config
@@ -250,9 +279,21 @@ export class SyncService extends BaseWorkerHandler {
async stop() {
DEV_LOG && console.log('Sync', 'stop');
- // Clear all throttle timers
+ // Clear all throttle timers and alarms
this.throttleTimers.forEach(timer => clearTimeout(timer));
this.throttleTimers.clear();
+
+ // Cancel all platform-specific alarms/background tasks
+ if (__ANDROID__ && cancelAndroidSyncAlarm) {
+ this.pendingSyncs.forEach((_, serviceId) => {
+ cancelAndroidSyncAlarm(serviceId);
+ });
+ } else if (__IOS__ && cancelIOSBackgroundRefresh) {
+ this.pendingSyncs.forEach((_, serviceId) => {
+ cancelIOSBackgroundRefresh(serviceId);
+ });
+ }
+
this.pendingSyncs.clear();
this.lastSyncTimes.clear();
@@ -374,21 +415,35 @@ export class SyncService extends BaseWorkerHandler {
}
private scheduleAndroidAlarm(serviceId: number, delayMs: number) {
- // Placeholder for Android alarm scheduling
- // Will be implemented in platform-specific file
- DEV_LOG && console.log('SyncService', 'scheduleAndroidAlarm', serviceId, delayMs);
+ if (__ANDROID__ && scheduleAndroidSyncAlarm) {
+ scheduleAndroidSyncAlarm(serviceId, delayMs);
+ }
}
private requestIOSBackgroundRefresh(serviceId: number, delayMs: number) {
- // Placeholder for iOS background refresh
- // Will be implemented in platform-specific file
- DEV_LOG && console.log('SyncService', 'requestIOSBackgroundRefresh', serviceId, delayMs);
+ if (__IOS__ && requestIOSBackgroundRefresh) {
+ requestIOSBackgroundRefresh(serviceId, delayMs);
+ }
}
private async executeSyncInternal(data: any) {
return this.syncDocumentsInternalCore(data);
}
+ /**
+ * Trigger a throttled sync for a specific service
+ * Called by platform-specific alarm/background refresh handlers
+ */
+ public triggerThrottledSync(serviceId: number) {
+ const pendingData = this.pendingSyncs.get(serviceId);
+ if (pendingData) {
+ this.lastSyncTimes.set(serviceId, Date.now());
+ this.pendingSyncs.delete(serviceId);
+ this.throttleTimers.delete(serviceId);
+ this.executeSyncInternal(pendingData);
+ }
+ }
+
async syncDocumentsInternal(
data: {
withFolders?;
diff --git a/app/services/sync/SyncAlarmReceiver.android.ts b/app/services/sync/SyncAlarmReceiver.android.ts
new file mode 100644
index 000000000..d92f04441
--- /dev/null
+++ b/app/services/sync/SyncAlarmReceiver.android.ts
@@ -0,0 +1,22 @@
+import { Utils } from '@nativescript/core';
+
+/**
+ * BroadcastReceiver for handling sync alarms on Android
+ */
+@NativeClass()
+@JavaProxy('__PACKAGE__.SyncAlarmReceiver')
+export class SyncAlarmReceiver extends android.content.BroadcastReceiver {
+ onReceive(context: android.content.Context, intent: android.content.Intent): void {
+ DEV_LOG && console.log('SyncAlarmReceiver', 'onReceive', intent.getAction());
+
+ const serviceId = intent.getIntExtra('serviceId', -1);
+ if (serviceId === -1) {
+ return;
+ }
+
+ // Send broadcast to trigger sync in the app
+ const syncIntent = new android.content.Intent(`${__APP_ID__}.SYNC_THROTTLE_ALARM`);
+ syncIntent.putExtra('serviceId', serviceId);
+ context.sendBroadcast(syncIntent);
+ }
+}
From ce170aed5f6df90d068e8fd2c5dc6b89d11daa42 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:18:47 +0000
Subject: [PATCH 04/12] Add Android BroadcastReceiver registration and
permissions for sync alarms
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
.../documentscanner/Android/src/main/AndroidManifest.xml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml b/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
index 95a99010e..c12c68ccd 100644
--- a/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
+++ b/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
@@ -4,6 +4,8 @@
+
+
+
+
+
\ No newline at end of file
From 2353880d541f13a541f47dfd84f191623c991fff Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:20:25 +0000
Subject: [PATCH 05/12] Address code review feedback - fix error handling,
redundant checks, and improve performance
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
.../Android/src/main/AndroidManifest.xml | 6 +++++-
.../settings/WebdavDataSyncSettings.svelte | 2 +-
app/services/sync.android.ts | 12 ++++++++++--
app/services/sync.ts | 16 +++++++---------
app/services/sync/SyncAlarmReceiver.android.ts | 8 +++++++-
5 files changed, 30 insertions(+), 14 deletions(-)
diff --git a/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml b/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
index c12c68ccd..6533d8724 100644
--- a/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
+++ b/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
@@ -40,6 +40,10 @@
-
+
+
+
+
+
\ No newline at end of file
diff --git a/app/components/settings/WebdavDataSyncSettings.svelte b/app/components/settings/WebdavDataSyncSettings.svelte
index 92e8a266e..9fa3d09af 100644
--- a/app/components/settings/WebdavDataSyncSettings.svelte
+++ b/app/components/settings/WebdavDataSyncSettings.svelte
@@ -118,7 +118,7 @@
width={100}
on:textChange={(e) => {
const value = parseInt(e.value) || 0;
- $store.syncThrottleSeconds = value >= 0 ? value : 0;
+ $store.syncThrottleSeconds = Math.max(0, value);
}} />
{/if}
diff --git a/app/services/sync.android.ts b/app/services/sync.android.ts
index 4cd9d2d5e..6a52e221a 100644
--- a/app/services/sync.android.ts
+++ b/app/services/sync.android.ts
@@ -25,6 +25,8 @@ export function initAndroidSyncAlarms() {
import('../sync').then(({ syncService }) => {
// Trigger sync for this service
syncService.triggerThrottledSync(serviceId);
+ }).catch((error) => {
+ console.error('SyncAlarmReceiver', 'failed to trigger sync', error);
});
}
});
@@ -46,7 +48,11 @@ export function scheduleAndroidSyncAlarm(serviceId: number, delayMs: number) {
}
const context = Utils.android.getApplicationContext();
- const intent = new android.content.Intent(context, (com as any).tns.SyncAlarmReceiver.class);
+
+ // Create intent for the SyncAlarmReceiver
+ // Note: The receiver class must be in the same package as the main application
+ const intent = new android.content.Intent(`${__APP_ID__}.SYNC_ALARM_ACTION`);
+ intent.setPackage(context.getPackageName());
intent.putExtra('serviceId', serviceId);
const pendingIntent = android.app.PendingIntent.getBroadcast(
@@ -86,7 +92,9 @@ export function cancelAndroidSyncAlarm(serviceId: number) {
}
const context = Utils.android.getApplicationContext();
- const intent = new android.content.Intent(context, (com as any).tns.SyncAlarmReceiver.class);
+ const intent = new android.content.Intent(`${__APP_ID__}.SYNC_ALARM_ACTION`);
+ intent.setPackage(context.getPackageName());
+
const pendingIntent = android.app.PendingIntent.getBroadcast(
context,
serviceId,
diff --git a/app/services/sync.ts b/app/services/sync.ts
index f9884bb52..2e70cc647 100644
--- a/app/services/sync.ts
+++ b/app/services/sync.ts
@@ -404,12 +404,12 @@ export class SyncService extends BaseWorkerHandler {
this.throttleTimers.set(serviceId, timer);
// For Android, schedule alarm if available
- if (__ANDROID__ && global.isAndroid) {
+ if (__ANDROID__) {
this.scheduleAndroidAlarm(serviceId, delay);
}
// For iOS, request background refresh
- if (__IOS__ && global.isIOS) {
+ if (__IOS__) {
this.requestIOSBackgroundRefresh(serviceId, delay);
}
}
@@ -434,13 +434,13 @@ export class SyncService extends BaseWorkerHandler {
* Trigger a throttled sync for a specific service
* Called by platform-specific alarm/background refresh handlers
*/
- public triggerThrottledSync(serviceId: number) {
+ public async triggerThrottledSync(serviceId: number) {
const pendingData = this.pendingSyncs.get(serviceId);
if (pendingData) {
this.lastSyncTimes.set(serviceId, Date.now());
this.pendingSyncs.delete(serviceId);
this.throttleTimers.delete(serviceId);
- this.executeSyncInternal(pendingData);
+ await this.executeSyncInternal(pendingData);
}
}
@@ -459,15 +459,13 @@ export class SyncService extends BaseWorkerHandler {
// If we have services with throttle configured, handle throttling per service
const servicesWithThrottle = services.filter(s => s.syncThrottleSeconds && s.syncThrottleSeconds > 0);
+ const servicesWithoutThrottle = services.filter(s => !s.syncThrottleSeconds || s.syncThrottleSeconds === 0);
if (servicesWithThrottle.length > 0 && !data.force) {
- // For each service, handle throttling separately
- for (const service of servicesWithThrottle) {
- await this.handleThrottledSync(data, service);
- }
+ // For each service with throttle, handle throttling separately
+ await Promise.all(servicesWithThrottle.map(service => this.handleThrottledSync(data, service)));
// Also execute for services without throttle immediately
- const servicesWithoutThrottle = services.filter(s => !s.syncThrottleSeconds || s.syncThrottleSeconds === 0);
if (servicesWithoutThrottle.length > 0) {
await this.syncDocumentsInternalCore(data);
}
diff --git a/app/services/sync/SyncAlarmReceiver.android.ts b/app/services/sync/SyncAlarmReceiver.android.ts
index d92f04441..c2913d217 100644
--- a/app/services/sync/SyncAlarmReceiver.android.ts
+++ b/app/services/sync/SyncAlarmReceiver.android.ts
@@ -7,7 +7,13 @@ import { Utils } from '@nativescript/core';
@JavaProxy('__PACKAGE__.SyncAlarmReceiver')
export class SyncAlarmReceiver extends android.content.BroadcastReceiver {
onReceive(context: android.content.Context, intent: android.content.Intent): void {
- DEV_LOG && console.log('SyncAlarmReceiver', 'onReceive', intent.getAction());
+ const action = intent.getAction();
+ DEV_LOG && console.log('SyncAlarmReceiver', 'onReceive', action);
+
+ // Check if this is our sync alarm action
+ if (action !== `${__APP_ID__}.SYNC_ALARM_ACTION`) {
+ return;
+ }
const serviceId = intent.getIntExtra('serviceId', -1);
if (serviceId === -1) {
From fd9ed8c6e1ba97c55070d3cf071162467ac09bb5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:22:05 +0000
Subject: [PATCH 06/12] Extract constants, improve type safety, and optimize
service filtering
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
.../settings/WebdavDataSyncSettings.svelte | 2 +-
app/services/sync.android.ts | 10 ++++--
app/services/sync.ts | 32 ++++++++++++++-----
.../sync/SyncAlarmReceiver.android.ts | 5 +--
4 files changed, 35 insertions(+), 14 deletions(-)
diff --git a/app/components/settings/WebdavDataSyncSettings.svelte b/app/components/settings/WebdavDataSyncSettings.svelte
index 9fa3d09af..1b8184b62 100644
--- a/app/components/settings/WebdavDataSyncSettings.svelte
+++ b/app/components/settings/WebdavDataSyncSettings.svelte
@@ -114,7 +114,7 @@
hint="0"
keyboardType="number"
marginLeft={10}
- text={$store.syncThrottleSeconds || 0}
+ text={String($store.syncThrottleSeconds || 0)}
width={100}
on:textChange={(e) => {
const value = parseInt(e.value) || 0;
diff --git a/app/services/sync.android.ts b/app/services/sync.android.ts
index 6a52e221a..7b4b2c597 100644
--- a/app/services/sync.android.ts
+++ b/app/services/sync.android.ts
@@ -1,5 +1,9 @@
import { Application, Utils } from '@nativescript/core';
+// Constants for intent actions
+export const SYNC_ALARM_ACTION = `${__APP_ID__}.SYNC_ALARM_ACTION`;
+export const SYNC_THROTTLE_ALARM = `${__APP_ID__}.SYNC_THROTTLE_ALARM`;
+
let alarmManager: android.app.AlarmManager = null;
let alarmReceiverRegistered = false;
@@ -15,7 +19,7 @@ export function initAndroidSyncAlarms() {
alarmManager = context.getSystemService(android.content.Context.ALARM_SERVICE) as android.app.AlarmManager;
// Register broadcast receiver for sync alarms
- const filter = new android.content.IntentFilter(`${__APP_ID__}.SYNC_THROTTLE_ALARM`);
+ const filter = new android.content.IntentFilter(SYNC_THROTTLE_ALARM);
const receiver = new android.content.BroadcastReceiver({
onReceive(context: android.content.Context, intent: android.content.Intent) {
const serviceId = intent.getIntExtra('serviceId', -1);
@@ -51,7 +55,7 @@ export function scheduleAndroidSyncAlarm(serviceId: number, delayMs: number) {
// Create intent for the SyncAlarmReceiver
// Note: The receiver class must be in the same package as the main application
- const intent = new android.content.Intent(`${__APP_ID__}.SYNC_ALARM_ACTION`);
+ const intent = new android.content.Intent(SYNC_ALARM_ACTION);
intent.setPackage(context.getPackageName());
intent.putExtra('serviceId', serviceId);
@@ -92,7 +96,7 @@ export function cancelAndroidSyncAlarm(serviceId: number) {
}
const context = Utils.android.getApplicationContext();
- const intent = new android.content.Intent(`${__APP_ID__}.SYNC_ALARM_ACTION`);
+ const intent = new android.content.Intent(SYNC_ALARM_ACTION);
intent.setPackage(context.getPackageName());
const pendingIntent = android.app.PendingIntent.getBroadcast(
diff --git a/app/services/sync.ts b/app/services/sync.ts
index 2e70cc647..3714df893 100644
--- a/app/services/sync.ts
+++ b/app/services/sync.ts
@@ -327,8 +327,15 @@ export class SyncService extends BaseWorkerHandler {
}
// Per-service throttle timers and pending sync flags
- private throttleTimers: Map = new Map();
- private pendingSyncs: Map = new Map();
+ private throttleTimers: Map = new Map();
+ private pendingSyncs: Map = new Map();
private lastSyncTimes: Map = new Map();
syncDocuments = debounce(
@@ -457,16 +464,25 @@ export class SyncService extends BaseWorkerHandler {
// Check which services should sync based on the type
const services = this.getStoredSyncServices().filter((s) => s.enabled !== false);
- // If we have services with throttle configured, handle throttling per service
- const servicesWithThrottle = services.filter(s => s.syncThrottleSeconds && s.syncThrottleSeconds > 0);
- const servicesWithoutThrottle = services.filter(s => !s.syncThrottleSeconds || s.syncThrottleSeconds === 0);
+ // Split services into throttled and non-throttled in one pass
+ const { throttled, nonThrottled } = services.reduce(
+ (acc, service) => {
+ if (service.syncThrottleSeconds && service.syncThrottleSeconds > 0) {
+ acc.throttled.push(service);
+ } else {
+ acc.nonThrottled.push(service);
+ }
+ return acc;
+ },
+ { throttled: [] as any[], nonThrottled: [] as any[] }
+ );
- if (servicesWithThrottle.length > 0 && !data.force) {
+ if (throttled.length > 0 && !data.force) {
// For each service with throttle, handle throttling separately
- await Promise.all(servicesWithThrottle.map(service => this.handleThrottledSync(data, service)));
+ await Promise.all(throttled.map(service => this.handleThrottledSync(data, service)));
// Also execute for services without throttle immediately
- if (servicesWithoutThrottle.length > 0) {
+ if (nonThrottled.length > 0) {
await this.syncDocumentsInternalCore(data);
}
} else {
diff --git a/app/services/sync/SyncAlarmReceiver.android.ts b/app/services/sync/SyncAlarmReceiver.android.ts
index c2913d217..ef1d1c96e 100644
--- a/app/services/sync/SyncAlarmReceiver.android.ts
+++ b/app/services/sync/SyncAlarmReceiver.android.ts
@@ -1,4 +1,5 @@
import { Utils } from '@nativescript/core';
+import { SYNC_ALARM_ACTION, SYNC_THROTTLE_ALARM } from '../sync.android';
/**
* BroadcastReceiver for handling sync alarms on Android
@@ -11,7 +12,7 @@ export class SyncAlarmReceiver extends android.content.BroadcastReceiver {
DEV_LOG && console.log('SyncAlarmReceiver', 'onReceive', action);
// Check if this is our sync alarm action
- if (action !== `${__APP_ID__}.SYNC_ALARM_ACTION`) {
+ if (action !== SYNC_ALARM_ACTION) {
return;
}
@@ -21,7 +22,7 @@ export class SyncAlarmReceiver extends android.content.BroadcastReceiver {
}
// Send broadcast to trigger sync in the app
- const syncIntent = new android.content.Intent(`${__APP_ID__}.SYNC_THROTTLE_ALARM`);
+ const syncIntent = new android.content.Intent(SYNC_THROTTLE_ALARM);
syncIntent.putExtra('serviceId', serviceId);
context.sendBroadcast(syncIntent);
}
From 2ac4c8913b8c7185cff2aad46bffee3d59ef7f9e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:23:50 +0000
Subject: [PATCH 07/12] Fix function naming consistency and import paths
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
app/services/sync.android.ts | 6 +++---
app/services/sync.ts | 10 ++++++----
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/app/services/sync.android.ts b/app/services/sync.android.ts
index 7b4b2c597..b3b75450e 100644
--- a/app/services/sync.android.ts
+++ b/app/services/sync.android.ts
@@ -10,7 +10,7 @@ let alarmReceiverRegistered = false;
/**
* Initialize Android-specific sync alarm functionality
*/
-export function initAndroidSyncAlarms() {
+export function initAndroidSyncAlarm() {
if (alarmReceiverRegistered) {
return;
}
@@ -26,7 +26,7 @@ export function initAndroidSyncAlarms() {
DEV_LOG && console.log('SyncAlarmReceiver', 'received alarm for service', serviceId);
// Import dynamically to avoid circular dependency
- import('../sync').then(({ syncService }) => {
+ import('./sync').then(({ syncService }) => {
// Trigger sync for this service
syncService.triggerThrottledSync(serviceId);
}).catch((error) => {
@@ -48,7 +48,7 @@ export function initAndroidSyncAlarms() {
*/
export function scheduleAndroidSyncAlarm(serviceId: number, delayMs: number) {
if (!alarmManager) {
- initAndroidSyncAlarms();
+ initAndroidSyncAlarm();
}
const context = Utils.android.getApplicationContext();
diff --git a/app/services/sync.ts b/app/services/sync.ts
index 3714df893..b5e08394d 100644
--- a/app/services/sync.ts
+++ b/app/services/sync.ts
@@ -27,7 +27,7 @@ import { WebdavDataSyncOptions } from './sync/WebdavDataSyncService';
// Platform-specific imports
let scheduleAndroidSyncAlarm: (serviceId: number, delayMs: number) => void;
let cancelAndroidSyncAlarm: (serviceId: number) => void;
-let initAndroidSyncAlarms: () => void;
+let initAndroidSyncAlarm: () => void;
let requestIOSBackgroundRefresh: (serviceId: number, delayMs: number) => void;
let cancelIOSBackgroundRefresh: (serviceId: number) => void;
let initIOSSyncBackgroundRefresh: () => void;
@@ -36,7 +36,7 @@ if (__ANDROID__) {
const androidModule = require('./sync.android');
scheduleAndroidSyncAlarm = androidModule.scheduleAndroidSyncAlarm;
cancelAndroidSyncAlarm = androidModule.cancelAndroidSyncAlarm;
- initAndroidSyncAlarms = androidModule.initAndroidSyncAlarms;
+ initAndroidSyncAlarm = androidModule.initAndroidSyncAlarm;
} else if (__IOS__) {
const iosModule = require('./sync.ios');
requestIOSBackgroundRefresh = iosModule.requestIOSBackgroundRefresh;
@@ -243,8 +243,8 @@ export class SyncService extends BaseWorkerHandler {
}
// Initialize platform-specific background sync
- if (__ANDROID__ && initAndroidSyncAlarms) {
- initAndroidSyncAlarms();
+ if (__ANDROID__ && initAndroidSyncAlarm) {
+ initAndroidSyncAlarm();
} else if (__IOS__ && initIOSSyncBackgroundRefresh) {
initIOSSyncBackgroundRefresh();
}
@@ -479,9 +479,11 @@ export class SyncService extends BaseWorkerHandler {
if (throttled.length > 0 && !data.force) {
// For each service with throttle, handle throttling separately
+ // Each throttled service will be scheduled or executed based on its last sync time
await Promise.all(throttled.map(service => this.handleThrottledSync(data, service)));
// Also execute for services without throttle immediately
+ // The worker will determine which services actually sync based on the event type
if (nonThrottled.length > 0) {
await this.syncDocumentsInternalCore(data);
}
From 4291f01f2fafc9ab7355f27acf62eeb23285bc59 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:24:58 +0000
Subject: [PATCH 08/12] Improve type safety and add documentation comments
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
app/services/sync.android.ts | 1 +
app/services/sync.ts | 18 ++++++++++++++----
app/services/sync/SyncAlarmReceiver.android.ts | 2 ++
3 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/app/services/sync.android.ts b/app/services/sync.android.ts
index b3b75450e..6eccb74ff 100644
--- a/app/services/sync.android.ts
+++ b/app/services/sync.android.ts
@@ -1,6 +1,7 @@
import { Application, Utils } from '@nativescript/core';
// Constants for intent actions
+// __APP_ID__ is a build-time constant representing the application package ID
export const SYNC_ALARM_ACTION = `${__APP_ID__}.SYNC_ALARM_ACTION`;
export const SYNC_THROTTLE_ALARM = `${__APP_ID__}.SYNC_THROTTLE_ALARM`;
diff --git a/app/services/sync.ts b/app/services/sync.ts
index b5e08394d..fe4900b4c 100644
--- a/app/services/sync.ts
+++ b/app/services/sync.ts
@@ -327,7 +327,7 @@ export class SyncService extends BaseWorkerHandler {
}
// Per-service throttle timers and pending sync flags
- private throttleTimers: Map = new Map();
+ private throttleTimers: Map = new Map();
private pendingSyncs: Map {
fromEvent?: string;
event?: DocumentEvents;
},
- service: any
+ service: WebdavDataSyncOptions & { id?: number; type: SYNC_TYPES }
) {
const serviceId = service.id;
const throttleSeconds = service.syncThrottleSeconds || 0;
@@ -433,7 +433,14 @@ export class SyncService extends BaseWorkerHandler {
}
}
- private async executeSyncInternal(data: any) {
+ private async executeSyncInternal(data: {
+ withFolders?;
+ force?;
+ bothWays?;
+ type?: number;
+ fromEvent?: string;
+ event?: DocumentEvents;
+ }) {
return this.syncDocumentsInternalCore(data);
}
@@ -474,7 +481,10 @@ export class SyncService extends BaseWorkerHandler {
}
return acc;
},
- { throttled: [] as any[], nonThrottled: [] as any[] }
+ {
+ throttled: [] as Array,
+ nonThrottled: [] as Array
+ }
);
if (throttled.length > 0 && !data.force) {
diff --git a/app/services/sync/SyncAlarmReceiver.android.ts b/app/services/sync/SyncAlarmReceiver.android.ts
index ef1d1c96e..cb72e603c 100644
--- a/app/services/sync/SyncAlarmReceiver.android.ts
+++ b/app/services/sync/SyncAlarmReceiver.android.ts
@@ -3,6 +3,8 @@ import { SYNC_ALARM_ACTION, SYNC_THROTTLE_ALARM } from '../sync.android';
/**
* BroadcastReceiver for handling sync alarms on Android
+ * Note: __PACKAGE__ is a build-time constant that gets replaced with the app package name
+ * This must match the package name in AndroidManifest.xml
*/
@NativeClass()
@JavaProxy('__PACKAGE__.SyncAlarmReceiver')
From 835e3a5caa6d606dcbc7aadc052f685263b0abd2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:26:04 +0000
Subject: [PATCH 09/12] Extract SyncData interface to eliminate type
duplication
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
app/services/sync.ts | 59 +++++++++++---------------------------------
1 file changed, 15 insertions(+), 44 deletions(-)
diff --git a/app/services/sync.ts b/app/services/sync.ts
index fe4900b4c..7729707e5 100644
--- a/app/services/sync.ts
+++ b/app/services/sync.ts
@@ -49,6 +49,16 @@ export const syncServicesStore = writable([]);
const SETTINGS_KEY = 'webdav_config';
+export interface SyncData {
+ withFolders?: boolean;
+ force?: boolean;
+ bothWays?: boolean;
+ type?: number;
+ fromEvent?: string;
+ event?: DocumentEvents;
+}
+
+
export function findArrayDiffs(array1: S[], array2: T[], compare: (a: S, b: T) => boolean) {
const union: S[] = [];
array1 = Array.from(array1);
@@ -328,14 +338,7 @@ export class SyncService extends BaseWorkerHandler {
// Per-service throttle timers and pending sync flags
private throttleTimers: Map = new Map();
- private pendingSyncs: Map = new Map();
+ private pendingSyncs: Map = new Map();
private lastSyncTimes: Map = new Map();
syncDocuments = debounce(
@@ -359,14 +362,7 @@ export class SyncService extends BaseWorkerHandler {
* Handles sync throttling per service configuration
*/
private async handleThrottledSync(
- data: {
- withFolders?;
- force?;
- bothWays?;
- type?: number;
- fromEvent?: string;
- event?: DocumentEvents;
- },
+ data: SyncData,
service: WebdavDataSyncOptions & { id?: number; type: SYNC_TYPES }
) {
const serviceId = service.id;
@@ -433,14 +429,7 @@ export class SyncService extends BaseWorkerHandler {
}
}
- private async executeSyncInternal(data: {
- withFolders?;
- force?;
- bothWays?;
- type?: number;
- fromEvent?: string;
- event?: DocumentEvents;
- }) {
+ private async executeSyncInternal(data: SyncData) {
return this.syncDocumentsInternalCore(data);
}
@@ -458,16 +447,7 @@ export class SyncService extends BaseWorkerHandler {
}
}
- async syncDocumentsInternal(
- data: {
- withFolders?;
- force?;
- bothWays?;
- type?: number;
- fromEvent?: string;
- event?: DocumentEvents;
- } = {}
- ) {
+ async syncDocumentsInternal(data: SyncData = {}) {
// Check which services should sync based on the type
const services = this.getStoredSyncServices().filter((s) => s.enabled !== false);
@@ -503,16 +483,7 @@ export class SyncService extends BaseWorkerHandler {
}
}
- async syncDocumentsInternalCore(
- data: {
- withFolders?;
- force?;
- bothWays?;
- type?: number;
- fromEvent?: string;
- event?: DocumentEvents;
- } = {}
- ) {
+ async syncDocumentsInternalCore(data: SyncData = {}) {
try {
const db = documentsService.db?.db?.db;
if (!this.enabled || !db) {
From dc2e4ff51117165f16d59762142931fe4d3feda4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 14:39:40 +0000
Subject: [PATCH 10/12] Refactor sync files to platform-specific pattern and
implement complete iOS background sync
Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
---
app/services/sync.android.ts | 32 ++--
app/services/{sync.ts => sync.common.ts} | 27 +---
app/services/sync.ios.ts | 185 ++++++++++++++++++++---
3 files changed, 193 insertions(+), 51 deletions(-)
rename app/services/{sync.ts => sync.common.ts} (95%)
diff --git a/app/services/sync.android.ts b/app/services/sync.android.ts
index 6eccb74ff..50f7550b2 100644
--- a/app/services/sync.android.ts
+++ b/app/services/sync.android.ts
@@ -1,5 +1,11 @@
import { Application, Utils } from '@nativescript/core';
+// Export everything from common
+export * from './sync.common';
+
+// Import common to set platform-specific implementations
+import * as syncCommon from './sync.common';
+
// Constants for intent actions
// __APP_ID__ is a build-time constant representing the application package ID
export const SYNC_ALARM_ACTION = `${__APP_ID__}.SYNC_ALARM_ACTION`;
@@ -11,7 +17,7 @@ let alarmReceiverRegistered = false;
/**
* Initialize Android-specific sync alarm functionality
*/
-export function initAndroidSyncAlarm() {
+function initAndroidSyncAlarmImpl() {
if (alarmReceiverRegistered) {
return;
}
@@ -26,11 +32,9 @@ export function initAndroidSyncAlarm() {
const serviceId = intent.getIntExtra('serviceId', -1);
DEV_LOG && console.log('SyncAlarmReceiver', 'received alarm for service', serviceId);
- // Import dynamically to avoid circular dependency
- import('./sync').then(({ syncService }) => {
- // Trigger sync for this service
- syncService.triggerThrottledSync(serviceId);
- }).catch((error) => {
+ // Import syncService from common to trigger the sync
+ const { syncService } = require('./sync.common');
+ syncService.triggerThrottledSync(serviceId).catch((error) => {
console.error('SyncAlarmReceiver', 'failed to trigger sync', error);
});
}
@@ -47,9 +51,9 @@ export function initAndroidSyncAlarm() {
* @param serviceId The sync service ID
* @param delayMs Delay in milliseconds
*/
-export function scheduleAndroidSyncAlarm(serviceId: number, delayMs: number) {
+function scheduleAndroidSyncAlarmImpl(serviceId: number, delayMs: number) {
if (!alarmManager) {
- initAndroidSyncAlarm();
+ initAndroidSyncAlarmImpl();
}
const context = Utils.android.getApplicationContext();
@@ -91,7 +95,7 @@ export function scheduleAndroidSyncAlarm(serviceId: number, delayMs: number) {
* Cancel a scheduled alarm for a sync service
* @param serviceId The sync service ID
*/
-export function cancelAndroidSyncAlarm(serviceId: number) {
+function cancelAndroidSyncAlarmImpl(serviceId: number) {
if (!alarmManager) {
return;
}
@@ -112,3 +116,13 @@ export function cancelAndroidSyncAlarm(serviceId: number) {
DEV_LOG && console.log('SyncService', 'cancelled Android alarm for service', serviceId);
}
+
+// Set the platform-specific implementations in the common module
+syncCommon.initAndroidSyncAlarm = initAndroidSyncAlarmImpl;
+syncCommon.scheduleAndroidSyncAlarm = scheduleAndroidSyncAlarmImpl;
+syncCommon.cancelAndroidSyncAlarm = cancelAndroidSyncAlarmImpl;
+
+// Stub implementations for iOS functions (not used on Android)
+syncCommon.initIOSSyncBackgroundRefresh = () => {};
+syncCommon.requestIOSBackgroundRefresh = () => {};
+syncCommon.cancelIOSBackgroundRefresh = () => {};
diff --git a/app/services/sync.ts b/app/services/sync.common.ts
similarity index 95%
rename from app/services/sync.ts
rename to app/services/sync.common.ts
index 7729707e5..a5c0eef82 100644
--- a/app/services/sync.ts
+++ b/app/services/sync.common.ts
@@ -24,26 +24,13 @@ import { DocumentAddedEventData, DocumentDeletedEventData, DocumentEvents, Docum
import { SYNC_TYPES, SyncType, getRemoteDeleteDocumentSettingsKey } from './sync/types';
import { WebdavDataSyncOptions } from './sync/WebdavDataSyncService';
-// Platform-specific imports
-let scheduleAndroidSyncAlarm: (serviceId: number, delayMs: number) => void;
-let cancelAndroidSyncAlarm: (serviceId: number) => void;
-let initAndroidSyncAlarm: () => void;
-let requestIOSBackgroundRefresh: (serviceId: number, delayMs: number) => void;
-let cancelIOSBackgroundRefresh: (serviceId: number) => void;
-let initIOSSyncBackgroundRefresh: () => void;
-
-if (__ANDROID__) {
- const androidModule = require('./sync.android');
- scheduleAndroidSyncAlarm = androidModule.scheduleAndroidSyncAlarm;
- cancelAndroidSyncAlarm = androidModule.cancelAndroidSyncAlarm;
- initAndroidSyncAlarm = androidModule.initAndroidSyncAlarm;
-} else if (__IOS__) {
- const iosModule = require('./sync.ios');
- requestIOSBackgroundRefresh = iosModule.requestIOSBackgroundRefresh;
- cancelIOSBackgroundRefresh = iosModule.cancelIOSBackgroundRefresh;
- initIOSSyncBackgroundRefresh = iosModule.initIOSSyncBackgroundRefresh;
-}
-
+// Platform-specific functions - implemented in sync.android.ts and sync.ios.ts
+export let scheduleAndroidSyncAlarm: (serviceId: number, delayMs: number) => void;
+export let cancelAndroidSyncAlarm: (serviceId: number) => void;
+export let initAndroidSyncAlarm: () => void;
+export let requestIOSBackgroundRefresh: (serviceId: number, delayMs: number) => void;
+export let cancelIOSBackgroundRefresh: (serviceId: number) => void;
+export let initIOSSyncBackgroundRefresh: () => void;
export const syncServicesStore = writable([]);
diff --git a/app/services/sync.ios.ts b/app/services/sync.ios.ts
index e94a06da4..61aeac18e 100644
--- a/app/services/sync.ios.ts
+++ b/app/services/sync.ios.ts
@@ -1,41 +1,182 @@
-/**
- * iOS-specific sync background refresh functionality
- */
+import { Utils } from '@nativescript/core';
+
+// Export everything from common
+export * from './sync.common';
+
+// Import common to set platform-specific implementations
+import * as syncCommon from './sync.common';
+
+// Background task identifier for sync
+const SYNC_TASK_IDENTIFIER = `${__APP_ID__}.sync`;
+
+// Store for pending sync service IDs
+const pendingSyncTasks: Map = new Map();
/**
* Initialize iOS-specific sync background refresh
*/
-export function initIOSSyncBackgroundRefresh() {
- // iOS background refresh initialization
- // This would typically use BGTaskScheduler for iOS 13+
+function initIOSSyncBackgroundRefreshImpl() {
+ if (typeof BGTaskScheduler === 'undefined') {
+ DEV_LOG && console.log('SyncService', 'BGTaskScheduler not available (iOS < 13)');
+ return;
+ }
+
+ // Register the background task handler
+ BGTaskScheduler.sharedScheduler.registerForTaskWithIdentifierUsingQueueLaunchHandler(
+ SYNC_TASK_IDENTIFIER,
+ null, // Use main queue
+ (task: BGTask) => {
+ DEV_LOG && console.log('SyncService', 'iOS background task started', task.identifier);
+
+ // Set expiration handler
+ task.expirationHandler = () => {
+ DEV_LOG && console.log('SyncService', 'iOS background task expired');
+ task.setTaskCompletedWithSuccess(false);
+ };
+
+ // Process all pending syncs
+ processPendingSyncs()
+ .then(() => {
+ DEV_LOG && console.log('SyncService', 'iOS background task completed successfully');
+ task.setTaskCompletedWithSuccess(true);
+ })
+ .catch((error) => {
+ console.error('SyncService', 'iOS background task failed', error);
+ task.setTaskCompletedWithSuccess(false);
+ });
+ }
+ );
+
DEV_LOG && console.log('SyncService', 'iOS background refresh initialized');
}
+/**
+ * Process all pending sync tasks
+ */
+async function processPendingSyncs(): Promise {
+ const { syncService } = require('./sync.common');
+
+ // Get all pending syncs
+ const syncPromises: Promise[] = [];
+ pendingSyncTasks.forEach((scheduledTime, serviceId) => {
+ DEV_LOG && console.log('SyncService', 'processing pending sync for service', serviceId);
+ syncPromises.push(
+ syncService.triggerThrottledSync(serviceId).catch((error) => {
+ console.error('SyncService', 'failed to sync service', serviceId, error);
+ })
+ );
+ });
+
+ // Clear pending tasks
+ pendingSyncTasks.clear();
+
+ // Wait for all syncs to complete
+ await Promise.all(syncPromises);
+}
+
/**
* Request background refresh for a throttled sync
* @param serviceId The sync service ID
* @param delayMs Delay in milliseconds
*/
-export function requestIOSBackgroundRefresh(serviceId: number, delayMs: number) {
- // On iOS, we can't schedule exact times for background refresh
- // Instead, we request a background refresh and the system decides when to run it
- // For now, this is a placeholder that would need proper BGTaskScheduler implementation
-
- DEV_LOG && console.log('SyncService', 'requested iOS background refresh for service', serviceId, 'in approximately', delayMs, 'ms');
-
- // TODO: Implement proper BGTaskScheduler integration
- // This would involve:
- // 1. Registering a background task identifier in Info.plist
- // 2. Registering a handler for the task
- // 3. Submitting a BGTaskRequest with earliestBeginDate
+function requestIOSBackgroundRefreshImpl(serviceId: number, delayMs: number) {
+ if (typeof BGTaskScheduler === 'undefined') {
+ DEV_LOG && console.log('SyncService', 'BGTaskScheduler not available, using timer fallback');
+ // Fallback to regular timer for older iOS versions
+ setTimeout(() => {
+ const { syncService } = require('./sync.common');
+ syncService.triggerThrottledSync(serviceId).catch((error) => {
+ console.error('SyncService', 'failed to sync service', serviceId, error);
+ });
+ }, delayMs);
+ return;
+ }
+
+ // Store this service as pending
+ const scheduledTime = new Date(Date.now() + delayMs);
+ pendingSyncTasks.set(serviceId, scheduledTime);
+
+ // Cancel any existing requests first
+ BGTaskScheduler.sharedScheduler.cancelTaskRequestWithIdentifier(SYNC_TASK_IDENTIFIER);
+
+ // Find the earliest scheduled time among all pending syncs
+ let earliestTime = scheduledTime;
+ pendingSyncTasks.forEach((time) => {
+ if (time < earliestTime) {
+ earliestTime = time;
+ }
+ });
+
+ // Create a new background task request
+ const request = BGAppRefreshTaskRequest.alloc().initWithIdentifier(SYNC_TASK_IDENTIFIER);
+ request.earliestBeginDate = earliestTime;
+
+ // Submit the request
+ const errorRef = new interop.Reference();
+ const success = BGTaskScheduler.sharedScheduler.submitTaskRequestError(request, errorRef);
+
+ if (!success) {
+ const error = errorRef.value;
+ console.error('SyncService', 'Failed to schedule iOS background task:', error?.localizedDescription);
+
+ // Fallback to timer if background task scheduling fails
+ setTimeout(() => {
+ const { syncService } = require('./sync.common');
+ syncService.triggerThrottledSync(serviceId).catch((error) => {
+ console.error('SyncService', 'failed to sync service', serviceId, error);
+ });
+ }, delayMs);
+ } else {
+ DEV_LOG && console.log('SyncService', 'scheduled iOS background refresh for service', serviceId, 'at', earliestTime);
+ }
}
/**
* Cancel a scheduled background refresh for a sync service
* @param serviceId The sync service ID
*/
-export function cancelIOSBackgroundRefresh(serviceId: number) {
- DEV_LOG && console.log('SyncService', 'cancelled iOS background refresh for service', serviceId);
-
- // TODO: Implement proper BGTaskScheduler cancellation
+function cancelIOSBackgroundRefreshImpl(serviceId: number) {
+ // Remove from pending tasks
+ pendingSyncTasks.delete(serviceId);
+
+ if (typeof BGTaskScheduler === 'undefined') {
+ return;
+ }
+
+ // If no more pending tasks, cancel the background task
+ if (pendingSyncTasks.size === 0) {
+ BGTaskScheduler.sharedScheduler.cancelTaskRequestWithIdentifier(SYNC_TASK_IDENTIFIER);
+ DEV_LOG && console.log('SyncService', 'cancelled iOS background refresh (no pending tasks)');
+ } else {
+ // Reschedule with the next earliest time
+ let earliestTime: Date | null = null;
+ pendingSyncTasks.forEach((time) => {
+ if (!earliestTime || time < earliestTime) {
+ earliestTime = time;
+ }
+ });
+
+ if (earliestTime) {
+ // Cancel existing and reschedule
+ BGTaskScheduler.sharedScheduler.cancelTaskRequestWithIdentifier(SYNC_TASK_IDENTIFIER);
+
+ const request = BGAppRefreshTaskRequest.alloc().initWithIdentifier(SYNC_TASK_IDENTIFIER);
+ request.earliestBeginDate = earliestTime;
+
+ const errorRef = new interop.Reference();
+ BGTaskScheduler.sharedScheduler.submitTaskRequestError(request, errorRef);
+ }
+
+ DEV_LOG && console.log('SyncService', 'rescheduled iOS background refresh for remaining tasks');
+ }
}
+
+// Set the platform-specific implementations in the common module
+syncCommon.initIOSSyncBackgroundRefresh = initIOSSyncBackgroundRefreshImpl;
+syncCommon.requestIOSBackgroundRefresh = requestIOSBackgroundRefreshImpl;
+syncCommon.cancelIOSBackgroundRefresh = cancelIOSBackgroundRefreshImpl;
+
+// Stub implementations for Android functions (not used on iOS)
+syncCommon.initAndroidSyncAlarm = () => {};
+syncCommon.scheduleAndroidSyncAlarm = () => {};
+syncCommon.cancelAndroidSyncAlarm = () => {};
From b4b114c3a82822afead1848fed6ea8bb7e2539ef Mon Sep 17 00:00:00 2001
From: farfromrefuge
Date: Tue, 24 Mar 2026 13:30:04 +0100
Subject: [PATCH 11/12] feat(android): throttle sync (set time through sync
settings)
---
.../Android/src/main/AndroidManifest.xml | 9 -
app.webpack.config.js | 1 +
app/android/sync_alarm_receiver.ts | 37 +++
app/app.ts | 33 +--
.../settings/FolderImageSyncSettings.svelte | 29 ++-
.../settings/FolderPDFSyncSettings.svelte | 20 +-
.../settings/PDFSyncSettingsView.svelte | 28 +--
.../settings/WebdavDataSyncSettings.svelte | 7 +-
.../settings/WebdavImageSyncSettings.svelte | 19 +-
.../settings/WebdavPDFSyncSettings.svelte | 19 +-
app/services/BackendService.ts | 154 +++++-------
app/services/sync.android.ts | 194 +++++++--------
app/services/sync.common.ts | 225 ++++++++---------
app/services/sync.d.ts | 1 +
app/services/sync.ios.ts | 229 ++++++++----------
app/services/sync/BaseSyncService.ts | 10 +
.../sync/SyncAlarmReceiver.android.ts | 31 ---
app/startHandler.ts | 46 ++++
package.json | 4 +-
.../platforms/android/AndroidManifest.xml | 12 +
.../platforms/android/native-api-usage.json | 1 +
typings/references.d.ts | 1 +
yarn.lock | 25 +-
23 files changed, 559 insertions(+), 576 deletions(-)
create mode 100644 app/android/sync_alarm_receiver.ts
create mode 100644 app/services/sync.d.ts
delete mode 100644 app/services/sync/SyncAlarmReceiver.android.ts
create mode 100644 app/startHandler.ts
diff --git a/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml b/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
index 6533d8724..95a99010e 100644
--- a/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
+++ b/App_Resources/documentscanner/Android/src/main/AndroidManifest.xml
@@ -4,8 +4,6 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app.webpack.config.js b/app.webpack.config.js
index 1ae381ad5..5a2825294 100644
--- a/app.webpack.config.js
+++ b/app.webpack.config.js
@@ -102,6 +102,7 @@ module.exports = (env, params = {}) => {
// env.appComponents.push('~/android/cameraactivity');
env.appComponents.push('~/android/activity.android');
env.appComponents.push('~/android/camera_scan_activity');
+ env.appComponents.push('~/android/sync_alarm_receiver');
const config = webpackConfig(env, params);
config.entry.application = '~/android/application.android';
const {
diff --git a/app/android/sync_alarm_receiver.ts b/app/android/sync_alarm_receiver.ts
new file mode 100644
index 000000000..6696b4431
--- /dev/null
+++ b/app/android/sync_alarm_receiver.ts
@@ -0,0 +1,37 @@
+import { Application, Utils } from '@nativescript/core';
+import { SYNC_ALARM_ACTION, SYNC_THROTTLE_ALARM, syncService } from '../services/sync.android';
+import { start } from '~/startHandler';
+
+/**
+ * BroadcastReceiver for handling sync alarms on Android
+ */
+@NativeClass()
+@JavaProxy('__PACKAGE__.SyncAlarmReceiver')
+export class SyncAlarmReceiver extends android.content.BroadcastReceiver {
+ async onReceive(context: android.content.Context, intent: android.content.Intent) {
+ const action = intent.getAction();
+
+ // Check if this is our sync alarm action
+ if (action !== SYNC_ALARM_ACTION) {
+ return;
+ }
+
+ const serviceId = intent.getLongExtra('serviceId', -1);
+ if (serviceId === -1) {
+ return;
+ }
+
+ DEV_LOG && console.log('SyncAlarmReceiver', 'onReceive', action, serviceId, Application.servicesStarted);
+ function handleIntent() {
+ syncService.triggerThrottledSync(serviceId).catch((error) => {
+ console.error('SyncAlarmReceiver', 'failed to trigger sync', error);
+ });
+ }
+ try {
+ await start();
+ handleIntent();
+ } catch (error) {
+ console.error(error, error.stack);
+ }
+ }
+}
diff --git a/app/app.ts b/app/app.ts
index 87d934f4d..c1f8edea8 100644
--- a/app/app.ts
+++ b/app/app.ts
@@ -31,12 +31,14 @@ import { SETTINGS_APP_VERSION, SETTINGS_SYNC_ON_START } from '~/utils/constants'
import { startOnCam } from './variables';
import { CollectionViewTraceCategory } from '@nativescript-community/ui-collectionview';
import { init as sharedInit } from '@shared/index';
+import { start, stopAppServices } from '~/startHandler';
declare module '@nativescript/core/application/application-common' {
interface ApplicationCommon {
servicesStarted: boolean;
}
}
+
try {
// we cant really use firstAppOpen anymore as all apps
// already installed with older version without this code would
@@ -131,33 +133,19 @@ try {
}
let launched = false;
- async function start() {
- try {
- Application.servicesStarted = false;
- // DEV_LOG && console.log('start');
- setDocumentsService(documentsService);
- await Promise.all([networkService.start(), securityService.start(), syncService.start(), ocrService.start(getCurrentISO3Language()), documentsService.start()]);
- Application.servicesStarted = true;
- // DEV_LOG && console.log('servicesStarted');
- Application.notify({ eventName: 'servicesStarted' });
- if (ApplicationSettings.getBoolean(SETTINGS_SYNC_ON_START, false)) {
- syncService.syncDocuments({ withFolders: true });
- }
- } catch (error) {
- showError(error, PLAY_STORE_BUILD ? { forcedMessage: lc('startup_error') } : {});
- }
- }
+
Application.on(Application.launchEvent, async () => {
// DEV_LOG && console.log('launch');
startThemeHelper();
launched = true;
- start();
+
+ start().catch(showError);
});
Application.on(Application.resumeEvent, () => {
if (!launched) {
// DEV_LOG && console.log('resume');
launched = true;
- start();
+ start().catch(showError);
}
});
let pageInstance;
@@ -165,14 +153,7 @@ try {
DEV_LOG && console.log('exit');
launched = false;
// ocrService.stop();
- try {
- securityService.stop();
- // wait for sync to stop to stop documentService as their could be writes to the db
- await syncService.stop();
- documentsService.stop();
- } catch (error) {
- console.error(error, error.stack);
- }
+ stopAppServices();
pageInstance?.$destroy();
pageInstance = null;
});
diff --git a/app/components/settings/FolderImageSyncSettings.svelte b/app/components/settings/FolderImageSyncSettings.svelte
index a591e5344..ce2f3d607 100644
--- a/app/components/settings/FolderImageSyncSettings.svelte
+++ b/app/components/settings/FolderImageSyncSettings.svelte
@@ -1,24 +1,25 @@