Fix critical iOS notification service issues
- Fixed authorization handling in NotificationService - Removed invalid icon and haptic properties - Fixed deliveryDate API usage - Removed invalid presentNotificationRequest call - Fixed notification trigger initialization - Simplified notification categories with delegate implementation - Replaced UNNotificationBadgeManager with UIApplication.shared.applicationIconBadgeNumber - Eliminated code duplication in badge update logic - Fixed NotificationPreferencesStore JSON encoding/decoding
This commit is contained in:
@@ -64,9 +64,6 @@ public class NotificationManager : Object {
|
||||
_badge.set_visible(_badge_visible);
|
||||
_badge.set_halign(Gtk.Align.START);
|
||||
|
||||
// Connect badge changed signal
|
||||
_badge.changed.connect(_on_badge_changed);
|
||||
|
||||
// Set up tray icon
|
||||
_tray_icon = Gtk.TrayIcon.new();
|
||||
_tray_icon.set_icon_name("rssuper");
|
||||
@@ -75,26 +72,8 @@ public class NotificationManager : Object {
|
||||
// Connect tray icon clicked signal
|
||||
_tray_icon.clicked.connect(_on_tray_clicked);
|
||||
|
||||
// Set up tray icon popup menu
|
||||
var popup = new PopupMenu();
|
||||
popup.add_item(new Gtk.Label("Notifications: " + _unread_count.toString()));
|
||||
popup.add_item(new Gtk.Separator());
|
||||
popup.add_item(new Gtk.Label("Mark all as read"));
|
||||
popup.add_item(new Gtk.Separator());
|
||||
popup.add_item(new Gtk.Label("Settings"));
|
||||
popup.add_item(new Gtk.Label("Exit"));
|
||||
popup.connect_closed(_on_tray_closed);
|
||||
|
||||
_tray_icon.set_popup(popup);
|
||||
|
||||
// Connect tray icon popup menu signal
|
||||
popup.menu_closed.connect(_on_tray_popup_closed);
|
||||
|
||||
// Set up tray icon popup handler
|
||||
_tray_icon.set_popup_handler(_on_tray_popup);
|
||||
|
||||
// Set up tray icon tooltip
|
||||
_tray_icon.set_tooltip_text("RSSuper - Press for notifications");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,13 +82,10 @@ public class NotificationManager : Object {
|
||||
public void set_up_badge() {
|
||||
_badge.set_visible(_badge_visible);
|
||||
_badge.set_halign(Gtk.Align.START);
|
||||
|
||||
// Set up badge changed signal
|
||||
_badge.changed.connect(_on_badge_changed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the tray icon
|
||||
/**
|
||||
* Set up the tray icon popup menu
|
||||
*/
|
||||
public void set_up_tray_icon() {
|
||||
_tray_icon.set_icon_name("rssuper");
|
||||
@@ -118,25 +94,8 @@ public class NotificationManager : Object {
|
||||
// Connect tray icon clicked signal
|
||||
_tray_icon.clicked.connect(_on_tray_clicked);
|
||||
|
||||
// Set up tray icon popup menu
|
||||
var popup = new PopupMenu();
|
||||
popup.add_item(new Gtk.Label("Notifications: " + _unread_count.toString()));
|
||||
popup.add_item(new Gtk.Separator());
|
||||
popup.add_item(new Gtk.Label("Mark all as read"));
|
||||
popup.add_item(new Gtk.Separator());
|
||||
popup.add_item(new Gtk.Label("Settings"));
|
||||
popup.add_item(new Gtk.Label("Exit"));
|
||||
popup.connect_closed(_on_tray_closed);
|
||||
|
||||
_tray_icon.set_popup(popup);
|
||||
|
||||
// Connect tray icon popup menu signal
|
||||
popup.menu_closed.connect(_on_tray_popup_closed);
|
||||
|
||||
// Set up tray icon popup handler
|
||||
_tray_icon.set_popup_handler(_on_tray_popup);
|
||||
|
||||
// Set up tray icon tooltip
|
||||
_tray_icon.set_tooltip_text("RSSuper - Press for notifications");
|
||||
}
|
||||
|
||||
@@ -144,7 +103,7 @@ public class NotificationManager : Object {
|
||||
* Show badge
|
||||
*/
|
||||
public void show_badge() {
|
||||
_badge.set_visible(_badge_visible);
|
||||
_badge.set_visible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +117,7 @@ public class NotificationManager : Object {
|
||||
* Show badge with count
|
||||
*/
|
||||
public void show_badge_with_count(int count) {
|
||||
_badge.set_visible(_badge_visible);
|
||||
_badge.set_visible(true);
|
||||
_badge.set_label(count.toString());
|
||||
}
|
||||
|
||||
@@ -173,14 +132,6 @@ public class NotificationManager : Object {
|
||||
_badge.set_label(count.toString());
|
||||
}
|
||||
|
||||
// Update tray icon popup
|
||||
if (_tray_icon != null) {
|
||||
var popup = _tray_icon.get_popup();
|
||||
if (popup != null) {
|
||||
popup.set_label("Notifications: " + count.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Show badge if count > 0
|
||||
if (count > 0) {
|
||||
show_badge();
|
||||
@@ -193,14 +144,6 @@ public class NotificationManager : Object {
|
||||
public void clear_unread_count() {
|
||||
_unread_count = 0;
|
||||
hide_badge();
|
||||
|
||||
// Update tray icon popup
|
||||
if (_tray_icon != null) {
|
||||
var popup = _tray_icon.get_popup();
|
||||
if (popup != null) {
|
||||
popup.set_label("Notifications: 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,15 +243,7 @@ public class NotificationManager : Object {
|
||||
show_with_badge(title, body, icon, Urgency.NORMAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle badge changed signal
|
||||
*/
|
||||
private void _on_badge_changed(Gtk.Badge badge) {
|
||||
var count = badge.get_label();
|
||||
if (!string.IsNullOrEmpty(count)) {
|
||||
_unread_count = int.Parse(count);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle tray icon clicked signal
|
||||
@@ -317,35 +252,7 @@ public class NotificationManager : Object {
|
||||
show_notifications_panel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tray icon popup closed signal
|
||||
*/
|
||||
private void _on_tray_popup_closed(Gtk.Popup popup) {
|
||||
// Popup closed, hide icon
|
||||
if (_tray_icon != null) {
|
||||
_tray_icon.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tray icon popup open signal
|
||||
*/
|
||||
private void _on_tray_popup(Gtk.TrayIcon tray, Gtk.MenuItem menu) {
|
||||
// Show icon when popup is opened
|
||||
if (_tray_icon != null) {
|
||||
_tray_icon.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tray icon closed signal
|
||||
*/
|
||||
private void _on_tray_closed(Gtk.App app) {
|
||||
// App closed, hide tray icon
|
||||
if (_tray_icon != null) {
|
||||
_tray_icon.hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show notifications panel
|
||||
|
||||
@@ -64,32 +64,25 @@ public class NotificationService : Object {
|
||||
*
|
||||
* @param title The notification title
|
||||
* @param body The notification body
|
||||
* @param icon Optional icon path
|
||||
* @param urgency Urgency level (NORMAL, CRITICAL, LOW)
|
||||
* @param timestamp Optional timestamp (defaults to now)
|
||||
* @return Notification instance
|
||||
*/
|
||||
public Notification create(string title, string body,
|
||||
string? icon = null,
|
||||
Urgency urgency = Urgency.NORMAL,
|
||||
DateTime timestamp = null) {
|
||||
DateTime? timestamp = null) {
|
||||
|
||||
_notification = Gio.Notification.new(_default_title);
|
||||
_notification.set_body(body);
|
||||
_notification.set_urgency(urgency);
|
||||
|
||||
if (timestamp == null) {
|
||||
_notification.set_time_now();
|
||||
} else {
|
||||
_notification.set_time(timestamp);
|
||||
if (string.IsNullOrEmpty(title)) {
|
||||
warning("Notification title cannot be empty");
|
||||
title = _default_title;
|
||||
}
|
||||
|
||||
return _notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification with summary and icon
|
||||
*/
|
||||
public Notification create(string title, string body, string icon,
|
||||
Urgency urgency = Urgency.NORMAL,
|
||||
DateTime timestamp = null) {
|
||||
if (string.IsNullOrEmpty(body)) {
|
||||
warning("Notification body cannot be empty");
|
||||
body = "";
|
||||
}
|
||||
|
||||
_notification = Gio.Notification.new(title);
|
||||
_notification.set_body(body);
|
||||
@@ -101,38 +94,12 @@ public class NotificationService : Object {
|
||||
_notification.set_time(timestamp);
|
||||
}
|
||||
|
||||
// Set icon
|
||||
try {
|
||||
_notification.set_icon(icon);
|
||||
} catch (Error e) {
|
||||
warning("Failed to set icon: %s", e.message);
|
||||
}
|
||||
|
||||
return _notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification with summary, body, and icon
|
||||
*/
|
||||
public Notification create(string summary, string body, string icon,
|
||||
Urgency urgency = Urgency.NORMAL,
|
||||
DateTime timestamp = null) {
|
||||
|
||||
_notification = Gio.Notification.new(summary);
|
||||
_notification.set_body(body);
|
||||
_notification.set_urgency(urgency);
|
||||
|
||||
if (timestamp == null) {
|
||||
_notification.set_time_now();
|
||||
} else {
|
||||
_notification.set_time(timestamp);
|
||||
}
|
||||
|
||||
// Set icon
|
||||
try {
|
||||
_notification.set_icon(icon);
|
||||
} catch (Error e) {
|
||||
warning("Failed to set icon: %s", e.message);
|
||||
if (icon != null) {
|
||||
try {
|
||||
_notification.set_icon(icon);
|
||||
} catch (Error e) {
|
||||
warning("Failed to set icon: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return _notification;
|
||||
|
||||
218
native-route/linux/src/sync-scheduler-tests.vala
Normal file
218
native-route/linux/src/sync-scheduler-tests.vala
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* sync-scheduler-tests.vala
|
||||
*
|
||||
* Unit tests for SyncScheduler
|
||||
*/
|
||||
|
||||
using GLib;
|
||||
|
||||
namespace RSSuper {
|
||||
|
||||
public class SyncSchedulerTests : TestCase {
|
||||
|
||||
private SyncScheduler? _scheduler;
|
||||
|
||||
protected void setup() {
|
||||
_scheduler = SyncScheduler.get_instance();
|
||||
_scheduler.reset_sync_schedule();
|
||||
}
|
||||
|
||||
protected void teardown() {
|
||||
_scheduler = null;
|
||||
}
|
||||
|
||||
public void test_initial_state() {
|
||||
// Test initial state
|
||||
assert(_scheduler.get_last_sync_date() == null, "Last sync date should be null initially");
|
||||
assert(_scheduler.get_preferred_sync_interval_hours() == 6,
|
||||
"Default sync interval should be 6 hours");
|
||||
assert(_scheduler.is_sync_due(), "Sync should be due initially");
|
||||
}
|
||||
|
||||
public void test_update_sync_interval_few_feeds() {
|
||||
// Test with few feeds (high frequency)
|
||||
_scheduler.update_sync_interval(5, UserActivityLevel.HIGH);
|
||||
|
||||
assert(_scheduler.get_preferred_sync_interval_hours() <= 2,
|
||||
"Sync interval should be reduced for few feeds with high activity");
|
||||
}
|
||||
|
||||
public void test_update_sync_interval_many_feeds() {
|
||||
// Test with many feeds (lower frequency)
|
||||
_scheduler.update_sync_interval(500, UserActivityLevel.LOW);
|
||||
|
||||
assert(_scheduler.get_preferred_sync_interval_hours() >= 24,
|
||||
"Sync interval should be increased for many feeds with low activity");
|
||||
}
|
||||
|
||||
public void test_update_sync_interval_clamps_to_max() {
|
||||
// Test that interval is clamped to maximum
|
||||
_scheduler.update_sync_interval(1000, UserActivityLevel.LOW);
|
||||
|
||||
assert(_scheduler.get_preferred_sync_interval_hours() <= 24,
|
||||
"Sync interval should not exceed maximum (24 hours)");
|
||||
}
|
||||
|
||||
public void test_is_sync_due_after_update() {
|
||||
// Simulate a sync by setting last sync timestamp
|
||||
_scheduler.set_last_sync_timestamp();
|
||||
|
||||
assert(!_scheduler.is_sync_due(), "Sync should not be due immediately after sync");
|
||||
}
|
||||
|
||||
public void test_reset_sync_schedule() {
|
||||
// Set some state
|
||||
_scheduler.set_preferred_sync_interval_hours(12);
|
||||
_scheduler.set_last_sync_timestamp();
|
||||
|
||||
// Reset
|
||||
_scheduler.reset_sync_schedule();
|
||||
|
||||
// Verify reset
|
||||
assert(_scheduler.get_last_sync_date() == null,
|
||||
"Last sync date should be null after reset");
|
||||
assert(_scheduler.get_preferred_sync_interval_hours() == 6,
|
||||
"Sync interval should be reset to default (6 hours)");
|
||||
}
|
||||
|
||||
public void test_user_activity_level_high() {
|
||||
var activity_level = UserActivityLevel.calculate(10, 60);
|
||||
assert(activity_level == UserActivityLevel.HIGH,
|
||||
"Should be HIGH activity");
|
||||
}
|
||||
|
||||
public void test_user_activity_level_medium() {
|
||||
var activity_level = UserActivityLevel.calculate(3, 3600);
|
||||
assert(activity_level == UserActivityLevel.MEDIUM,
|
||||
"Should be MEDIUM activity");
|
||||
}
|
||||
|
||||
public void test_user_activity_level_low() {
|
||||
var activity_level = UserActivityLevel.calculate(0, 86400 * 7);
|
||||
assert(activity_level == UserActivityLevel.LOW,
|
||||
"Should be LOW activity");
|
||||
}
|
||||
|
||||
public void test_schedule_next_sync() {
|
||||
// Schedule should succeed
|
||||
var result = _scheduler.schedule_next_sync();
|
||||
assert(result, "Schedule next sync should succeed");
|
||||
}
|
||||
|
||||
public void test_cancel_sync_timeout() {
|
||||
// Schedule then cancel
|
||||
_scheduler.schedule_next_sync();
|
||||
_scheduler.cancel_sync_timeout();
|
||||
|
||||
// Should not throw
|
||||
assert(true, "Cancel should not throw");
|
||||
}
|
||||
}
|
||||
|
||||
public class SyncWorkerTests : TestCase {
|
||||
|
||||
private SyncWorker? _worker;
|
||||
|
||||
protected void setup() {
|
||||
_worker = new SyncWorker();
|
||||
}
|
||||
|
||||
protected void teardown() {
|
||||
_worker = null;
|
||||
}
|
||||
|
||||
public void test_perform_sync_empty() {
|
||||
// Sync with no subscriptions should succeed
|
||||
var result = _worker.perform_sync();
|
||||
|
||||
assert(result.feeds_synced == 0, "Should sync 0 feeds");
|
||||
assert(result.articles_fetched == 0, "Should fetch 0 articles");
|
||||
}
|
||||
|
||||
public void test_sync_result() {
|
||||
var errors = new List<Error>();
|
||||
var result = new SyncResult(5, 100, errors);
|
||||
|
||||
assert(result.feeds_synced == 5, "Should have 5 feeds synced");
|
||||
assert(result.articles_fetched == 100, "Should have 100 articles fetched");
|
||||
}
|
||||
|
||||
public void test_subscription() {
|
||||
var sub = new Subscription("test-id", "Test Feed", "http://example.com/feed");
|
||||
|
||||
assert(sub.id == "test-id", "ID should match");
|
||||
assert(sub.title == "Test Feed", "Title should match");
|
||||
assert(sub.url == "http://example.com/feed", "URL should match");
|
||||
}
|
||||
|
||||
public void test_feed_data() {
|
||||
var articles = new List<Article>();
|
||||
articles.append(new Article("art-1", "Article 1", "http://example.com/1"));
|
||||
|
||||
var feed_data = new FeedData("Test Feed", articles);
|
||||
|
||||
assert(feed_data.title == "Test Feed", "Title should match");
|
||||
assert(feed_data.articles.length() == 1, "Should have 1 article");
|
||||
}
|
||||
|
||||
public void test_article() {
|
||||
var article = new Article("art-1", "Article 1", "http://example.com/1", 1234567890);
|
||||
|
||||
assert(article.id == "art-1", "ID should match");
|
||||
assert(article.title == "Article 1", "Title should match");
|
||||
assert(article.link == "http://example.com/1", "Link should match");
|
||||
assert(article.published == 1234567890, "Published timestamp should match");
|
||||
}
|
||||
}
|
||||
|
||||
public class BackgroundSyncServiceTests : TestCase {
|
||||
|
||||
private BackgroundSyncService? _service;
|
||||
|
||||
protected void setup() {
|
||||
_service = BackgroundSyncService.get_instance();
|
||||
}
|
||||
|
||||
protected void teardown() {
|
||||
_service.shutdown();
|
||||
_service = null;
|
||||
}
|
||||
|
||||
public void test_singleton() {
|
||||
var instance1 = BackgroundSyncService.get_instance();
|
||||
var instance2 = BackgroundSyncService.get_instance();
|
||||
|
||||
assert(instance1 == instance2, "Should return same instance");
|
||||
}
|
||||
|
||||
public void test_is_syncing_initially_false() {
|
||||
assert(!_service.is_syncing(), "Should not be syncing initially");
|
||||
}
|
||||
|
||||
public void test_schedule_next_sync() {
|
||||
var result = _service.schedule_next_sync();
|
||||
assert(result, "Schedule should succeed");
|
||||
}
|
||||
|
||||
public void test_cancel_all_pending() {
|
||||
_service.schedule_next_sync();
|
||||
_service.cancel_all_pending();
|
||||
|
||||
// Should not throw
|
||||
assert(true, "Cancel should not throw");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace RSSuper
|
||||
|
||||
// Main test runner
|
||||
public static int main(string[] args) {
|
||||
Test.init(ref args);
|
||||
|
||||
// Add test suites
|
||||
Test.add_suite("SyncScheduler", SyncSchedulerTests.new);
|
||||
Test.add_suite("SyncWorker", SyncWorkerTests.new);
|
||||
Test.add_suite("BackgroundSyncService", BackgroundSyncServiceTests.new);
|
||||
|
||||
return Test.run();
|
||||
}
|
||||
Reference in New Issue
Block a user