Issues with Silent Notifications in Parental Control App Using FCM and Background Tasks

Hello,

We are developing a parental control app consisting of two parts: a parent app to manage settings and a child app to enforce these settings using iOS's Screen Time API, CoreData, and other components. We've attempted to use silent notifications with Firebase Cloud Messaging (FCM) to communicate updates from the parent app to the child app. Our current implementation involves background modes for remote messages and background tasks.

However, we're facing a challenge: while normal FCM push notifications with a 'message' key work as expected, silent notifications (with only a 'data' key) do not trigger the desired behavior in the child app, even though FCM returns a success response.

We're looking for assistance with two main issues:

  1. Alternative Approaches: Is there a better way to notify the child app of changes? We're considering a system where the child app periodically checks for updates via API and then updates CoreData and managed settings. Any recommendations for this architecture or a more reliable notification system would be greatly appreciated.
  2. Debugging Silent Notifications: If our current approach using silent notifications is feasible, could someone help us debug why these notifications are not working as expected? We've been stuck on this for a week, and any help would be a lifesaver.

Here's the relevant part of our AppDelegate code:

import UIKit
import FirebaseCore
import FirebaseMessaging
import BackgroundTasks

@objc class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    let gcmMessageIDKey = "gcm.message_id"
    let backgroundTaskIdentifier = "com.your-company.your-app.silentnotification"

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        Messaging.messaging().delegate = self
        
        // Register for remote notifications
        UNUserNotificationCenter.current().delegate = self
        application.registerForRemoteNotifications()
        
        // Register background task
        BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in
            self.handleBackgroundTask(task: task as! BGProcessingTask)
        }
        
        return true
    }

    // Handle incoming remote notifications
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                     fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        if let aps = userInfo["aps"] as? [String: Any],
           let contentAvailable = aps["content-available"] as? Int, contentAvailable == 1 {
            // This is a silent notification
            handleSilentNotification(userInfo: userInfo, completionHandler: completionHandler)
        } else {
            // This is a regular notification
            Messaging.messaging().appDidReceiveMessage(userInfo)
            completionHandler(.newData)
        }
    }

    // Handle silent notification
    func handleSilentNotification(userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        let request = BGProcessingTaskRequest(identifier: backgroundTaskIdentifier)
        request.requiresNetworkConnectivity = true
        
        do {
            try BGTaskScheduler.shared.submit(request)
            performAPICall { result in
                switch result {
                case .success(_):
                    completionHandler(.newData)
                case .failure(_):
                    completionHandler(.failed)
                }
            }
        } catch {
            completionHandler(.failed)
        }
    }

    // Handle background task
    func handleBackgroundTask(task: BGProcessingTask) {
        task.expirationHandler = {
            task.setTaskCompleted(success: false)
        }
        
        performAPICall { result in
            task.setTaskCompleted(success: result != nil)
        }
    }

    // Perform API call (placeholder implementation)
    func performAPICall(completion: @escaping (Data?) -> Void) {
        // Your API call implementation here
        // For testing, you can use a simple delay:
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            completion(Data())
        }
    }
}

extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        print("FCM token: \(fcmToken ?? "nil")")
        // TODO: Send this token to your server
    }
}

Additionally, here is how we're sending notifications from the server side using Node.js:

// Import the required Firebase Admin SDK (assumed to be initialized elsewhere)
// const { getMessaging } = require('firebase-admin/messaging');

/**
 * Sends a background push notification to an iOS device
 * @returns {Promise<string>} The message ID if successful
 * @throws Will throw an error if the sending process fails
 */
async function sendBackgroundPushNotification() {
  // Construct the message object for a background push notification
  const message = {
    apns: {
      headers: {
        // Set the priority of the push notification
        "apns-priority": "5",
        priority: "5",
        // Indicate that this is a background refresh notification
        "content-available": "1",
        content_available: "1",
        // Specify the push type as background
        "apns-push-type": "background",
        // Set the topic to your app's bundle identifier
        "apns-topic": "com.your-company.your-app", // Replace with your actual bundle identifier
      },
      payload: {
        aps: {
          // This tells iOS to wake up your app in the background
          "content-available": 1,
        },
      },
    },
    // Custom data payload to be sent with the notification
    // Modify this object to include the data you want to send
    data: {
      // Add your custom key-value pairs here
    },
    // Uncomment the following block if you want to include a visible notification
    // notification: {
    //   title: "Notification Title",
    //   body: "Notification Body",
    // }, 
token
    token: "DEVICE_FCM_TOKEN_PLACEHOLDER",
  };

  try {
    // Attempt to send the message using Firebase Cloud Messaging
    const response = await getMessaging().send(message);
    console.log("Successfully sent background data to iOS:", response);
    return response;
  } catch (error) {
    console.error("Error sending background data to iOS:", error);
    throw error;
  }
}

// Example usage:
// sendBackgroundPushNotification()
//   .then((response) => console.log("Message sent successfully:", response))
//   .catch((error) => console.error("Failed to send message:", error));

We would really appreciate any insights or guidance on these issues. Thank you!

Silent notifications is not suitable for this use case, as they are heavily throttled, and sometimes not immediately delivered.

I don't know your exact use case and what kind of calls you are planning to make and what features of what frameworks you want to use, but if you need to execute code every time a push notification is sent, you will want to look into using a Notification Service Extension as discussed at https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension. The Notification Service Extension will be executed for every visible push notification. So, it could serve your needs, as long as the user has not disabled the visibility of your notifications through various settings. The service extension will not be executed for push notifications that will not be presented visually, like you have been trying to send.


Argun Tekant /  DTS Engineer / Core Technologies

Hello Argun,

Thank you for your assistance. I'd like to delve deeper into the Notification Service Extension capabilities and its fit for our parental control app, Superr. Our application ensures transparency and involves both parents and children in the digital management process.

Visibility of Notifications: We do not require that the notifications remain hidden or discreet. Our application acts as a digital parenting assistant, and part of its function is to keep children informed about the changes or restrictions applied, promoting transparency in digital parenting.

Use Case and Requirements: Our app includes two main components: a parent app and a child app. When a parent modifies settings, such as setting time restrictions or blocking certain apps, these changes need to be communicated immediately and reliably to the child's device.

Example Scenario:

  1. Parent Action: A parent decides to set a new screen time schedule from 7 PM to 9 PM for educational apps on the child’s device and blocks a gaming app that was previously allowed.
  2. Notification Trigger: These settings are updated in the parent app, triggering a push notification to the child’s device.
  3. Child App Response: Upon receiving this notification, the child app must:
  4. Fetch the latest configuration from our server.
  5. Serialize the data and update the CoreData database.
  6. Apply the new screen time schedule.
  7. Block the specified gaming app.

Key Questions:

  1. Execution Time Limit: Can you specify how much time is typically allotted for operations within the didReceive(_:withContentHandler:) method of the Notification Service Extension? Is this typically sufficient to handle complex tasks such as data serialization, CoreData updates, and settings applications? as quotes from document - "That method has a limited amount of time to perform its task and execute the provided completion block."
  2. Reliability: How reliable is the Notification Service Extension for ensuring that tasks, particularly those crucial for enforcing parental controls, are executed without being skipped? Are there particular conditions under which the extension might fail to perform as expected?

We aim to ensure robust and timely updates to maintain the efficacy of parental controls. Your insights on the above points will greatly assist us in optimizing our approach. Thank you once again for your time and assistance.

@Engineer Additionally, obtaining clarity on these aspects of the Notification Service Extension will be instrumental in helping us assess whether this solution best fits our needs or if we should consider alternative methods. Your guidance is crucial as it will directly influence our strategy to ensure seamless and effective implementation of parental controls within our app. We are committed to creating a robust and reliable environment for families using Superr and deeply value the expert advice you provide to help us achieve this.

Thank you again for your support and expertise.

As of this writing, the time and memory limits for Notification Service Extensions are 30 seconds, and 24 MB (that includes all memory objects, your code, any data, libraries or frameworks you import, etc.)

Unless something goes wrong (there used to be a bug that caused the extension to not be executed, but as of iOS 17.5 we are in the clear for that) the extension will reliably be launched as long as the criteria is met:

  • payload must have the mutable-content key set to 1
  • payload must have visible content (alert dictionary)
  • if set apns-push-type header must be alert
  • the user must not have turned off notifications for the device or the app, and not turned off the visibility either (in summary is OK)
  • if the device is locked, the "show notification content" must be ON
  • in iOS 18+ the app must not be hidden

As for whether any specific code, calls, libraries, frameworks etc. will work (or fit in memory) in your specific use case, we can't say one way or the other. You must test your code yourself. The NSE may have some additional sandboxing rules that conflict with what you want to do, and it is not possible to say if your code will work without trying.

Keep in mind that the NSE has been designed as a way to change (mutate) the content that is going to be shown to the user, and instead using it to trigger other specific functionalities or running specific arbitrary code may or may not work, which is something you will need to try out and see.


Argun Tekant /  DTS Engineer / Core Technologies

We recently implemented a Network Service Extension (NSE) and tested its capabilities to perform the following tasks when receiving a network service extension call:

Apply App Restriction Using Managed Settings: We attempted to apply app restrictions using the ManagedSettings API from within the NSE. However, we found that this approach does not work directly from the NSE. Managed Settings in Family Control Documentation

Add a Device Activity Schedule Directly from NSE: We also tried to add a device activity schedule using the DeviceActivity API from within the NSE. Similar to the managed settings, this approach also proved to be unsuccessful. Device Activity Schedule in Family Control Documentation

Additionally, we explored running a background task from the NSE. However, it was discovered that most of the main app functionalities cannot be triggered from the NSE. Despite these limitations, we were able to save data to UserDefaults or Core Data successfully from the NSE.

If you have any suggestions or alternative approaches to achieving these tasks from within an NSE, your input would be greatly appreciated.

Issues with Silent Notifications in Parental Control App Using FCM and Background Tasks
 
 
Q