feat: implement cross-platform features and UI integration
- iOS: Add BackgroundSyncService, SyncScheduler, SyncWorker, BookmarkViewModel, FeedViewModel - iOS: Add BackgroundSyncService, SyncScheduler, SyncWorker services - Linux: Add settings-store.vala, State.vala signals, view widgets (FeedList, FeedDetail, AddFeed, Search, Settings, Bookmark) - Linux: Add bookmark-store.vala, bookmark vala model, search-service.vala - Android: Add NotificationService, NotificationManager, NotificationPreferencesStore - Android: Add BookmarkDao, BookmarkRepository, SettingsStore - Add unit tests for iOS, Android, Linux - Add integration tests - Add performance benchmarks - Update tasks and documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
25
native-route/linux/gsettings/org.rssuper.sync.gschema.xml
Normal file
25
native-route/linux/gsettings/org.rssuper.sync.gschema.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist>
|
||||
<schema id="org.rssuper.sync" path="/org/rssuper/sync/">
|
||||
<key type="t" name="last-sync-timestamp">
|
||||
<default>0</default>
|
||||
<summary>Last sync timestamp</summary>
|
||||
<description>The Unix timestamp of the last successful sync</description>
|
||||
</key>
|
||||
<key type="i" name="preferred-sync-interval">
|
||||
<default>21600</default>
|
||||
<summary>Preferred sync interval in seconds</summary>
|
||||
<description>The preferred interval between sync operations (default: 6 hours)</description>
|
||||
</key>
|
||||
<key type="b" name="auto-sync-enabled">
|
||||
<default>true</default>
|
||||
<summary>Auto-sync enabled</summary>
|
||||
<description>Whether automatic background sync is enabled</description>
|
||||
</key>
|
||||
<key type="i" name="sync-on-wifi-only">
|
||||
<default>0</default>
|
||||
<summary>Sync on Wi-Fi only</summary>
|
||||
<description>0=always, 1=Wi-Fi only, 2=never</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
10
native-route/linux/org.rssuper.sync.desktop
Normal file
10
native-route/linux/org.rssuper.sync.desktop
Normal file
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Name=RSSuper Background Sync
|
||||
Comment=Background feed synchronization for RSSuper
|
||||
Exec=/opt/rssuper/bin/rssuper-sync-daemon
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;Network;
|
||||
StartupNotify=false
|
||||
Hidden=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
23
native-route/linux/rssuper-sync.service
Normal file
23
native-route/linux/rssuper-sync.service
Normal file
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=RSSuper Background Sync Service
|
||||
Documentation=man:rssuper(1)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/opt/rssuper/bin/rssuper-sync
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=yes
|
||||
|
||||
# Timeout (5 minutes)
|
||||
TimeoutStartSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
23
native-route/linux/rssuper-sync.timer
Normal file
23
native-route/linux/rssuper-sync.timer
Normal file
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=RSSuper Background Sync Timer
|
||||
Documentation=man:rssuper(1)
|
||||
|
||||
[Timer]
|
||||
# On-boot delay (randomized between 1-5 minutes)
|
||||
OnBootSec=1min
|
||||
RandomizedDelaySec=4min
|
||||
|
||||
# On-unit-active delay (6 hours after service starts)
|
||||
OnUnitActiveSec=6h
|
||||
|
||||
# Accuracy (allow ±15 minutes)
|
||||
AccuracySec=15min
|
||||
|
||||
# Persist timer across reboots
|
||||
Persistent=true
|
||||
|
||||
# Wake system if sleeping to run timer
|
||||
WakeSystem=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
503
native-route/linux/src/background-sync.vala
Normal file
503
native-route/linux/src/background-sync.vala
Normal file
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
* background-sync.vala
|
||||
*
|
||||
* Main background sync service for RSSuper on Linux.
|
||||
* Orchestrates background feed synchronization using GTimeout and systemd timer.
|
||||
*/
|
||||
|
||||
using Gio;
|
||||
using GLib;
|
||||
|
||||
namespace RSSuper {
|
||||
|
||||
/**
|
||||
* BackgroundSyncService - Main background sync service coordinator
|
||||
*
|
||||
* Orchestrates background feed synchronization using:
|
||||
* - GTimeout for in-app scheduling
|
||||
* - systemd timer for system-level scheduling
|
||||
*/
|
||||
public class BackgroundSyncService : Object {
|
||||
|
||||
// Singleton instance
|
||||
private static BackgroundSyncService? _instance;
|
||||
|
||||
// Sync scheduler
|
||||
private SyncScheduler? _sync_scheduler;
|
||||
|
||||
// Sync worker
|
||||
private SyncWorker? _sync_worker;
|
||||
|
||||
// Current sync state
|
||||
private bool _is_syncing = false;
|
||||
|
||||
// Sync configuration
|
||||
public const string BACKGROUND_REFRESH_IDENTIFIER = "org.rssuper.background-refresh";
|
||||
public const string PERIODIC_SYNC_IDENTIFIER = "org.rssuper.periodic-sync";
|
||||
|
||||
// Settings
|
||||
private Settings? _settings;
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
public static BackgroundSyncService? get_instance() {
|
||||
if (_instance == null) {
|
||||
_instance = new BackgroundSyncService();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance (for singleton pattern)
|
||||
*/
|
||||
private BackgroundSyncService() {
|
||||
_sync_scheduler = SyncScheduler.get_instance();
|
||||
_sync_worker = new SyncWorker();
|
||||
|
||||
try {
|
||||
_settings = new Settings("org.rssuper.sync");
|
||||
} catch (Error e) {
|
||||
warning("Failed to create settings: %s", e.message);
|
||||
}
|
||||
|
||||
// Connect to sync due signal
|
||||
if (_sync_scheduler != null) {
|
||||
_sync_scheduler.sync_due.connect(on_sync_due);
|
||||
}
|
||||
|
||||
info("BackgroundSyncService initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the sync service
|
||||
*/
|
||||
public void initialize() {
|
||||
info("Initializing background sync service");
|
||||
|
||||
// Schedule initial sync
|
||||
schedule_next_sync();
|
||||
|
||||
info("Background sync service initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the next sync
|
||||
*/
|
||||
public bool schedule_next_sync() {
|
||||
if (_is_syncing) {
|
||||
warning("Sync already in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_sync_scheduler != null) {
|
||||
return _sync_scheduler.schedule_next_sync();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all pending sync operations
|
||||
*/
|
||||
public void cancel_all_pending() {
|
||||
if (_sync_scheduler != null) {
|
||||
_sync_scheduler.cancel_sync_timeout();
|
||||
}
|
||||
info("All pending sync operations cancelled");
|
||||
}
|
||||
|
||||
/**
|
||||
* Force immediate sync (for testing or user-initiated)
|
||||
*/
|
||||
public async void force_sync() {
|
||||
if (_is_syncing) {
|
||||
warning("Sync already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
_is_syncing = true;
|
||||
|
||||
try {
|
||||
var result = yield _sync_worker.perform_sync();
|
||||
|
||||
// Update last sync timestamp
|
||||
if (_sync_scheduler != null) {
|
||||
_sync_scheduler.set_last_sync_timestamp();
|
||||
}
|
||||
|
||||
info("Force sync completed: %d feeds, %d articles",
|
||||
result.feeds_synced, result.articles_fetched);
|
||||
|
||||
// Schedule next sync
|
||||
schedule_next_sync();
|
||||
|
||||
} catch (Error e) {
|
||||
warning("Force sync failed: %s", e.message);
|
||||
}
|
||||
|
||||
_is_syncing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if background sync is enabled
|
||||
*/
|
||||
public bool are_background_tasks_enabled() {
|
||||
// Check if systemd timer is active
|
||||
try {
|
||||
var result = subprocess_helper_command_str(
|
||||
"systemctl", "is-enabled", "rssuper-sync.timer");
|
||||
return result.strip() == "enabled";
|
||||
} catch (Error e) {
|
||||
// Timer might not be installed
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last sync date
|
||||
*/
|
||||
public DateTime? get_last_sync_date() {
|
||||
return _sync_scheduler != null ? _sync_scheduler.get_last_sync_date() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending feeds count
|
||||
*/
|
||||
public int get_pending_feeds_count() {
|
||||
// TODO: Implement
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently syncing
|
||||
*/
|
||||
public bool is_syncing() {
|
||||
return _is_syncing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync due callback
|
||||
*/
|
||||
private void on_sync_due() {
|
||||
if (_is_syncing) {
|
||||
warning("Sync already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
info("Sync due, starting background sync");
|
||||
|
||||
_is_syncing = true;
|
||||
|
||||
// Run sync in background
|
||||
GLib.Thread.new<void?>(null, () => {
|
||||
try {
|
||||
var result = _sync_worker.perform_sync();
|
||||
|
||||
// Update last sync timestamp
|
||||
if (_sync_scheduler != null) {
|
||||
_sync_scheduler.set_last_sync_timestamp();
|
||||
}
|
||||
|
||||
info("Background sync completed: %d feeds, %d articles",
|
||||
result.feeds_synced, result.articles_fetched);
|
||||
|
||||
// Schedule next sync
|
||||
schedule_next_sync();
|
||||
|
||||
} catch (Error e) {
|
||||
warning("Background sync failed: %s", e.message);
|
||||
}
|
||||
|
||||
_is_syncing = false;
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the sync service
|
||||
*/
|
||||
public void shutdown() {
|
||||
cancel_all_pending();
|
||||
info("Background sync service shut down");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncWorker - Performs the actual sync work
|
||||
*/
|
||||
public class SyncWorker : Object {
|
||||
|
||||
// Maximum number of feeds to sync per batch
|
||||
public const int MAX_FEEDS_PER_BATCH = 20;
|
||||
|
||||
// Timeout for individual feed fetch (in seconds)
|
||||
public const int FEED_FETCH_TIMEOUT = 30;
|
||||
|
||||
// Maximum concurrent feed fetches
|
||||
public const int MAX_CONCURRENT_FETCHES = 3;
|
||||
|
||||
/**
|
||||
* Perform a full sync operation
|
||||
*/
|
||||
public SyncResult perform_sync() {
|
||||
int feeds_synced = 0;
|
||||
int articles_fetched = 0;
|
||||
var errors = new List<Error>();
|
||||
|
||||
info("Starting sync");
|
||||
|
||||
// Get all subscriptions that need syncing
|
||||
var subscriptions = fetch_subscriptions_needing_sync();
|
||||
|
||||
info("Syncing %d subscriptions", subscriptions.length());
|
||||
|
||||
if (subscriptions.length() == 0) {
|
||||
info("No subscriptions to sync");
|
||||
return new SyncResult(feeds_synced, articles_fetched, errors);
|
||||
}
|
||||
|
||||
// Process subscriptions in batches
|
||||
var batches = chunk_list(subscriptions, MAX_FEEDS_PER_BATCH);
|
||||
|
||||
foreach (var batch in batches) {
|
||||
var batch_result = sync_batch(batch);
|
||||
feeds_synced += batch_result.feeds_synced;
|
||||
articles_fetched += batch_result.articles_fetched;
|
||||
errors.append_list(batch_result.errors);
|
||||
|
||||
// Small delay between batches to be battery-friendly
|
||||
try {
|
||||
Thread.sleep(500); // 500ms
|
||||
} catch (Error e) {
|
||||
warning("Failed to sleep: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
info("Sync completed: %d feeds, %d articles, %d errors",
|
||||
feeds_synced, articles_fetched, errors.length());
|
||||
|
||||
return new SyncResult(feeds_synced, articles_fetched, errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a partial sync for specific subscriptions
|
||||
*/
|
||||
public SyncResult perform_partial_sync(List<string> subscription_ids) {
|
||||
// TODO: Implement partial sync
|
||||
return new SyncResult(0, 0, new List<Error>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel ongoing sync operations
|
||||
*/
|
||||
public void cancel_sync() {
|
||||
info("Sync cancelled");
|
||||
// TODO: Cancel ongoing network requests
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch subscriptions that need syncing
|
||||
*/
|
||||
private List<Subscription> fetch_subscriptions_needing_sync() {
|
||||
// TODO: Replace with actual database query
|
||||
// For now, return empty list as placeholder
|
||||
return new List<Subscription>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a batch of subscriptions
|
||||
*/
|
||||
private SyncResult sync_batch(List<Subscription> subscriptions) {
|
||||
var feeds_synced = 0;
|
||||
var articles_fetched = 0;
|
||||
var errors = new List<Error>();
|
||||
|
||||
foreach (var subscription in subscriptions) {
|
||||
try {
|
||||
var feed_data = fetch_feed_data(subscription);
|
||||
|
||||
if (feed_data != null) {
|
||||
process_feed_data(feed_data, subscription.id);
|
||||
feeds_synced++;
|
||||
articles_fetched += feed_data.articles.length();
|
||||
|
||||
info("Synced %s: %d articles", subscription.title,
|
||||
feed_data.articles.length());
|
||||
}
|
||||
|
||||
} catch (Error e) {
|
||||
errors.append(e);
|
||||
warning("Error syncing %s: %s", subscription.title, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return new SyncResult(feeds_synced, articles_fetched, errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch feed data for a subscription
|
||||
*/
|
||||
private FeedData? fetch_feed_data(Subscription subscription) {
|
||||
// TODO: Implement actual feed fetching
|
||||
// This is a placeholder implementation
|
||||
|
||||
// Example implementation:
|
||||
// var uri = new Uri(subscription.url);
|
||||
// var client = new HttpClient();
|
||||
// var data = client.get(uri);
|
||||
// var feed_data = rss_parser.parse(data);
|
||||
// return feed_data;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process fetched feed data
|
||||
*/
|
||||
private void process_feed_data(FeedData feed_data, string subscription_id) {
|
||||
// TODO: Implement actual feed data processing
|
||||
// - Store new articles
|
||||
// - Update feed metadata
|
||||
// - Handle duplicates
|
||||
|
||||
info("Processing %d articles for %s", feed_data.articles.length(),
|
||||
feed_data.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk a list into batches
|
||||
*/
|
||||
private List<List<Subscription>> chunk_list(List<Subscription> list, int size) {
|
||||
var batches = new List<List<Subscription>>();
|
||||
var current_batch = new List<Subscription>();
|
||||
|
||||
foreach (var item in list) {
|
||||
current_batch.append(item);
|
||||
if (current_batch.length() >= size) {
|
||||
batches.append(current_batch);
|
||||
current_batch = new List<Subscription>();
|
||||
}
|
||||
}
|
||||
|
||||
if (current_batch.length() > 0) {
|
||||
batches.append(current_batch);
|
||||
}
|
||||
|
||||
return batches;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncResult - Result of a sync operation
|
||||
*/
|
||||
public class SyncResult : Object {
|
||||
public int feeds_synced {
|
||||
get { return _feeds_synced; }
|
||||
}
|
||||
public int articles_fetched {
|
||||
get { return _articles_fetched; }
|
||||
}
|
||||
public List<Error> errors {
|
||||
get { return _errors; }
|
||||
}
|
||||
|
||||
private int _feeds_synced;
|
||||
private int _articles_fetched;
|
||||
private List<Error> _errors;
|
||||
|
||||
public SyncResult(int feeds_synced, int articles_fetched, List<Error> errors) {
|
||||
_feeds_synced = feeds_synced;
|
||||
_articles_fetched = articles_fetched;
|
||||
_errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription - Model for a feed subscription
|
||||
*/
|
||||
public class Subscription : Object {
|
||||
public string id {
|
||||
get { return _id; }
|
||||
}
|
||||
public string title {
|
||||
get { return _title; }
|
||||
}
|
||||
public string url {
|
||||
get { return _url; }
|
||||
}
|
||||
public uint64 last_sync_date {
|
||||
get { return _last_sync_date; }
|
||||
}
|
||||
|
||||
private string _id;
|
||||
private string _title;
|
||||
private string _url;
|
||||
private uint64 _last_sync_date;
|
||||
|
||||
public Subscription(string id, string title, string url, uint64 last_sync_date = 0) {
|
||||
_id = id;
|
||||
_title = title;
|
||||
_url = url;
|
||||
_last_sync_date = last_sync_date;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FeedData - Parsed feed data
|
||||
*/
|
||||
public class FeedData : Object {
|
||||
public string title {
|
||||
get { return _title; }
|
||||
}
|
||||
public List<Article> articles {
|
||||
get { return _articles; }
|
||||
}
|
||||
|
||||
private string _title;
|
||||
private List<Article> _articles;
|
||||
|
||||
public FeedData(string title, List<Article> articles) {
|
||||
_title = title;
|
||||
_articles = articles;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Article - Model for a feed article
|
||||
*/
|
||||
public class Article : Object {
|
||||
public string id {
|
||||
get { return _id; }
|
||||
}
|
||||
public string title {
|
||||
get { return _title; }
|
||||
}
|
||||
public string? link {
|
||||
get { return _link; }
|
||||
}
|
||||
public uint64 published {
|
||||
get { return _published; }
|
||||
}
|
||||
public string? content {
|
||||
get { return _content; }
|
||||
}
|
||||
|
||||
private string _id;
|
||||
private string _title;
|
||||
private string? _link;
|
||||
private uint64 _published;
|
||||
private string? _content;
|
||||
|
||||
public Article(string id, string title, string? link = null,
|
||||
uint64 published = 0, string? content = null) {
|
||||
_id = id;
|
||||
_title = title;
|
||||
_link = link;
|
||||
_published = published;
|
||||
_content = content;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
325
native-route/linux/src/sync-scheduler.vala
Normal file
325
native-route/linux/src/sync-scheduler.vala
Normal file
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* sync-scheduler.vala
|
||||
*
|
||||
* Manages background sync scheduling for RSSuper on Linux.
|
||||
* Uses GTimeout for in-app scheduling and integrates with systemd timer.
|
||||
*/
|
||||
|
||||
using Gio;
|
||||
using GLib;
|
||||
|
||||
namespace RSSuper {
|
||||
|
||||
/**
|
||||
* SyncScheduler - Manages background sync scheduling
|
||||
*
|
||||
* Handles intelligent scheduling based on user behavior and system conditions.
|
||||
* Uses GTimeout for in-app scheduling and can trigger systemd timer.
|
||||
*/
|
||||
public class SyncScheduler : Object {
|
||||
|
||||
// Default sync interval (6 hours in seconds)
|
||||
public const int DEFAULT_SYNC_INTERVAL = 6 * 3600;
|
||||
|
||||
// Minimum sync interval (15 minutes in seconds)
|
||||
public const int MINIMUM_SYNC_INTERVAL = 15 * 60;
|
||||
|
||||
// Maximum sync interval (24 hours in seconds)
|
||||
public const int MAXIMUM_SYNC_INTERVAL = 24 * 3600;
|
||||
|
||||
// Singleton instance
|
||||
private static SyncScheduler? _instance;
|
||||
|
||||
// Settings for persisting sync state
|
||||
private Settings? _settings;
|
||||
|
||||
// GTimeout source for scheduling
|
||||
private uint _timeout_source_id = 0;
|
||||
|
||||
// Last sync timestamp
|
||||
private uint64 _last_sync_timestamp = 0;
|
||||
|
||||
// Preferred sync interval
|
||||
private int _preferred_sync_interval = DEFAULT_SYNC_INTERVAL;
|
||||
|
||||
// Sync callback
|
||||
public signal void sync_due();
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
public static SyncScheduler? get_instance() {
|
||||
if (_instance == null) {
|
||||
_instance = new SyncScheduler();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance (for singleton pattern)
|
||||
*/
|
||||
private SyncScheduler() {
|
||||
// Initialize settings for persisting sync state
|
||||
try {
|
||||
_settings = new Settings("org.rssuper.sync");
|
||||
} catch (Error e) {
|
||||
warning("Failed to create settings: %s", e.message);
|
||||
}
|
||||
|
||||
// Load last sync timestamp
|
||||
if (_settings != null) {
|
||||
_last_sync_timestamp = _settings.get_uint64("last-sync-timestamp");
|
||||
_preferred_sync_interval = _settings.get_int("preferred-sync-interval");
|
||||
}
|
||||
|
||||
info("SyncScheduler initialized: last_sync=%lu, interval=%d",
|
||||
_last_sync_timestamp, _preferred_sync_interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last sync date as DateTime
|
||||
*/
|
||||
public DateTime? get_last_sync_date() {
|
||||
if (_last_sync_timestamp == 0) {
|
||||
return null;
|
||||
}
|
||||
return new DateTime.from_unix_local((int64)_last_sync_timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preferred sync interval in hours
|
||||
*/
|
||||
public int get_preferred_sync_interval_hours() {
|
||||
return _preferred_sync_interval / 3600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set preferred sync interval in hours
|
||||
*/
|
||||
public void set_preferred_sync_interval_hours(int hours) {
|
||||
int clamped = hours.clamp(MINIMUM_SYNC_INTERVAL / 3600, MAXIMUM_SYNC_INTERVAL / 3600);
|
||||
_preferred_sync_interval = clamped * 3600;
|
||||
|
||||
if (_settings != null) {
|
||||
_settings.set_int("preferred-sync-interval", _preferred_sync_interval);
|
||||
}
|
||||
|
||||
info("Preferred sync interval updated to %d hours", clamped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time since last sync in seconds
|
||||
*/
|
||||
public uint64 get_time_since_last_sync() {
|
||||
if (_last_sync_timestamp == 0) {
|
||||
return uint64.MAX;
|
||||
}
|
||||
var now = get_monotonic_time() / 1000000; // Convert to seconds
|
||||
return now - _last_sync_timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sync is due
|
||||
*/
|
||||
public bool is_sync_due() {
|
||||
var time_since = get_time_since_last_sync();
|
||||
return time_since >= (uint64)_preferred_sync_interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the next sync based on current conditions
|
||||
*/
|
||||
public bool schedule_next_sync() {
|
||||
// Cancel any existing timeout
|
||||
cancel_sync_timeout();
|
||||
|
||||
// Check if we should sync immediately
|
||||
if (is_sync_due() && get_time_since_last_sync() >= (uint64)(_preferred_sync_interval * 2)) {
|
||||
info("Sync is significantly overdue, scheduling immediate sync");
|
||||
schedule_immediate_sync();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate next sync time
|
||||
var next_sync_in = calculate_next_sync_time();
|
||||
|
||||
info("Next sync scheduled in %d seconds (%.1f hours)",
|
||||
next_sync_in, next_sync_in / 3600.0);
|
||||
|
||||
// Schedule timeout
|
||||
_timeout_source_id = Timeout.add_seconds(next_sync_in, on_sync_timeout);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preferred sync interval based on user behavior
|
||||
*/
|
||||
public void update_sync_interval(int number_of_feeds, UserActivityLevel activity_level) {
|
||||
int base_interval;
|
||||
|
||||
// Adjust base interval based on number of feeds
|
||||
if (number_of_feeds < 10) {
|
||||
base_interval = 4 * 3600; // 4 hours for small feed lists
|
||||
} else if (number_of_feeds < 50) {
|
||||
base_interval = 6 * 3600; // 6 hours for medium feed lists
|
||||
} else if (number_of_feeds < 200) {
|
||||
base_interval = 12 * 3600; // 12 hours for large feed lists
|
||||
} else {
|
||||
base_interval = 24 * 3600; // 24 hours for very large feed lists
|
||||
}
|
||||
|
||||
// Adjust based on user activity
|
||||
switch (activity_level) {
|
||||
case UserActivityLevel.HIGH:
|
||||
_preferred_sync_interval = base_interval / 2; // Sync more frequently
|
||||
break;
|
||||
case UserActivityLevel.MEDIUM:
|
||||
_preferred_sync_interval = base_interval;
|
||||
break;
|
||||
case UserActivityLevel.LOW:
|
||||
_preferred_sync_interval = base_interval * 2; // Sync less frequently
|
||||
break;
|
||||
}
|
||||
|
||||
// Clamp to valid range
|
||||
_preferred_sync_interval = _preferred_sync_interval.clamp(
|
||||
MINIMUM_SYNC_INTERVAL, MAXIMUM_SYNC_INTERVAL);
|
||||
|
||||
// Persist
|
||||
if (_settings != null) {
|
||||
_settings.set_int("preferred-sync-interval", _preferred_sync_interval);
|
||||
}
|
||||
|
||||
info("Sync interval updated to %d hours (feeds: %d, activity: %s)",
|
||||
_preferred_sync_interval / 3600, number_of_feeds,
|
||||
activity_level.to_string());
|
||||
|
||||
// Re-schedule
|
||||
schedule_next_sync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended sync interval based on current conditions
|
||||
*/
|
||||
public int recommended_sync_interval() {
|
||||
return _preferred_sync_interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset sync schedule
|
||||
*/
|
||||
public void reset_sync_schedule() {
|
||||
cancel_sync_timeout();
|
||||
_last_sync_timestamp = 0;
|
||||
_preferred_sync_interval = DEFAULT_SYNC_INTERVAL;
|
||||
|
||||
if (_settings != null) {
|
||||
_settings.set_uint64("last-sync-timestamp", 0);
|
||||
_settings.set_int("preferred-sync-interval", DEFAULT_SYNC_INTERVAL);
|
||||
}
|
||||
|
||||
info("Sync schedule reset");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel any pending sync timeout
|
||||
*/
|
||||
public void cancel_sync_timeout() {
|
||||
if (_timeout_source_id > 0) {
|
||||
Source.remove(_timeout_source_id);
|
||||
_timeout_source_id = 0;
|
||||
info("Sync timeout cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set last sync timestamp (called after sync completes)
|
||||
*/
|
||||
public void set_last_sync_timestamp() {
|
||||
_last_sync_timestamp = get_monotonic_time() / 1000000;
|
||||
|
||||
if (_settings != null) {
|
||||
_settings.set_uint64("last-sync-timestamp", _last_sync_timestamp);
|
||||
}
|
||||
|
||||
info("Last sync timestamp updated to %lu", _last_sync_timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger sync now (for testing or user-initiated)
|
||||
*/
|
||||
public void trigger_sync_now() {
|
||||
info("Triggering sync now");
|
||||
sync_due();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload state from settings
|
||||
*/
|
||||
public void reload_state() {
|
||||
if (_settings != null) {
|
||||
_last_sync_timestamp = _settings.get_uint64("last-sync-timestamp");
|
||||
_preferred_sync_interval = _settings.get_int("preferred-sync-interval");
|
||||
}
|
||||
info("State reloaded: last_sync=%lu, interval=%d",
|
||||
_last_sync_timestamp, _preferred_sync_interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync timeout callback
|
||||
*/
|
||||
private bool on_sync_timeout() {
|
||||
info("Sync timeout triggered");
|
||||
sync_due();
|
||||
return false; // Don't repeat
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule immediate sync
|
||||
*/
|
||||
private void schedule_immediate_sync() {
|
||||
// Schedule for 1 minute from now
|
||||
_timeout_source_id = Timeout.add_seconds(60, () => {
|
||||
info("Immediate sync triggered");
|
||||
sync_due();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate next sync time in seconds
|
||||
*/
|
||||
private int calculate_next_sync_time() {
|
||||
var time_since = get_time_since_last_sync();
|
||||
if (time_since >= (uint64)_preferred_sync_interval) {
|
||||
return 60; // Sync soon
|
||||
}
|
||||
return _preferred_sync_interval - (int)time_since;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UserActivityLevel - User activity level for adaptive sync scheduling
|
||||
*/
|
||||
public enum UserActivityLevel {
|
||||
HIGH, // User actively reading, sync more frequently
|
||||
MEDIUM, // Normal usage
|
||||
LOW // Inactive user, sync less frequently
|
||||
|
||||
public static UserActivityLevel calculate(int daily_open_count, uint64 last_opened_ago_seconds) {
|
||||
// High activity: opened 5+ times today OR opened within last hour
|
||||
if (daily_open_count >= 5 || last_opened_ago_seconds < 3600) {
|
||||
return UserActivityLevel.HIGH;
|
||||
}
|
||||
|
||||
// Medium activity: opened 2+ times today OR opened within last day
|
||||
if (daily_open_count >= 2 || last_opened_ago_seconds < 86400) {
|
||||
return UserActivityLevel.MEDIUM;
|
||||
}
|
||||
|
||||
// Low activity: otherwise
|
||||
return UserActivityLevel.LOW;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user