Schedule Task (Screen Time API) on iOS in background mode

784 views Asked by At

I have developed a Flutter application to lock selected apps using the Screen Time API (iOS). My app has been registered in Family Control and currently successfully executes app restrictions in both emulators and devices through TestFlight. I intend to incorporate a countdown timer feature to ensure that the app restriction function is activated when the timer stops. However, the issue I'm facing is that on iOS, when the app goes into the background mode, all processes, including the countdown, halt, rendering the restriction function unusable. I have attempted various mechanisms, such as:

  1. DeviceActivityMonitor extension => Not triggered even though scheduling is set.
  2. Cron => Stops when in background mode.
  3. Local notification (Alarm) => Only triggered when the user interacts with the notification, not automatically execute function in background

Is there a solution to address this matter?

import DeviceActivity
import FamilyControls
import Flutter
import ManagedSettings
import SwiftUI
import UIKit
import workmanager
import BackgroundTasks
import UserNotifications



var globalMethodCall = ""
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    
    override func applicationWillTerminate(_ application: UIApplication) {
        @StateObject var model = MyModel.shared
        print("App is terminating")
        model.startAppRestrictions()
    }
    
      
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        let METHOD_CHANNEL_NAME = "flutter_screentime"
        let methodChannel = FlutterMethodChannel(name: METHOD_CHANNEL_NAME, binaryMessenger: controller as! FlutterBinaryMessenger)

        @StateObject var model = MyModel.shared
        @StateObject var store = ManagedSettingsStore()
        
        methodChannel.setMethodCallHandler {
            (call: FlutterMethodCall, result: @escaping FlutterResult) in
            Task {
                do {
                    if #available(iOS 16.0, *) {
                        print("try requestAuthorization")
                        // request authorization
                        try await AuthorizationCenter.shared.requestAuthorization(for: FamilyControlsMember.individual)
                        print("requestAuthorization success")                
                        switch AuthorizationCenter.shared.authorizationStatus {
                        case .notDetermined:
                            print("not determined")
                        case .denied:
                            print("denied")
                        case .approved:
                            print("approved")
                        @unknown default:
                            break
                        }
                    } else {
                        // Fallback on earlier versions
                    }
                } catch {
                    print("Error requestAuthorization: ", error)
                }
            }
            switch call.method {
            case "selectAppsToDiscourage":
                globalMethodCall = "selectAppsToDiscourage"
                let vc = UIHostingController(rootView: ContentView(doneButtonAction: {
                    print("Finish selected")
                    methodChannel.invokeMethod("updateSelectedApp", arguments: nil) { (result) in
                        if let message = result as? String {
                            print("Balasan dari Flutter: \(message)")
                        }
                    }
                })
                    .environmentObject(model)
                    .environmentObject(store))

                controller.present(vc, animated: false, completion: nil)

                // Setel nilai dynamicTitle dan dynamicCancelButtonTitle di AppDelegate
                   if let arguments = call.arguments as? [String: Any],
                      let selectAppTitle = arguments["selectAppTitle"] as? String,
                      let cancelTitle = arguments["cancelTitle"] as? String,
                      let doneTitle = arguments["doneTitle"] as? String {
                       model.selectAppTitle = selectAppTitle
                       model.cancelButtonTitle = cancelTitle
                       model.doneButtonTitle = doneTitle
                   }
                
                
                print("selectAppsToDiscourage")
                result(nil)
            case "startLockApp":
                globalMethodCall = "startLockApp"
                model.startAppRestrictions()
                result("startLockApp started")
            case "stopLockApp":
                globalMethodCall = "stopLockApp"
                model.stopAppRestrictions()
                print("stopLockApp")
                result("stopLockApp started")
            case "isLocked":
                print("method channel result ", model.isAppLocked())
                result(model.isAppLocked())
            case "countSelectedAppCategory":
                result(model.countSelectedAppCategory())
            case "startSchedulingApp":
                model.schedulingRestrictions()
                result("startSchedulingApp started")
            case "countSelectedApp":
                result(model.countSelectedApp())
            default:
                print("no method")
                result(FlutterMethodNotImplemented)
            }
        }
        
        if #available(iOS 10.0, *) {
          UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
        }

        GeneratedPluginRegistrant.register(with: self)
        
        // ask permission to show notification
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
            if granted {
                UNUserNotificationCenter.current().delegate = self
                self.scheduleLocalNotification()
            }
        }
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    func scheduleLocalNotification() {
        print("start scheduling local notification")
        let content = UNMutableNotificationContent()
        content.title = "Time to execute"
        content.body = "This function will executed at 14:20"
        
        var dateComponents = DateComponents()
        dateComponents.hour = 14
        dateComponents.minute = 20
        let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
        
        let request = UNNotificationRequest(identifier: "execStartRestriction", content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(request) { (error) in
            if let error = error {
                print("Failed to add notification \(error)")
            } else {
                print("Success register notification")
            }
        }
        
    }
    
    override func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        if response.notification.request.identifier == "execStartRestriction" {
            print("execute function from notification")
            @StateObject var model = MyModel.shared
            model.startAppRestrictions()
        }
        completionHandler()
    }
    
}

MyModel class

import Foundation

import FamilyControls
import ManagedSettings
import DeviceActivity

private let _MyModel = MyModel()

class MyModel: ObservableObject {
    let store = ManagedSettingsStore() 
    
    @Published var familyActivitySelection: FamilyActivitySelection
    
    var selectAppTitle: String = ""
    var cancelButtonTitle: String = ""
    var doneButtonTitle: String = ""
    
    
    init() {
        familyActivitySelection = FamilyActivitySelection()
    }
    
    class var shared: MyModel {
        return _MyModel
    }
    
    func startAppRestrictions() {
        print("setShieldRestrictions")
        
        // Pull the selection out of the app's model and configure the application shield restriction accordingly
        let applications = MyModel.shared.familyActivitySelection
        
        if applications.applicationTokens.isEmpty {
            print("empty applicationTokens")
        }                    
        
        if applications.categoryTokens.isEmpty {
            print("empty categoryTokens")
        }
        
        //lock application
        store.shield.applications = applications.applicationTokens.isEmpty ? nil : applications.applicationTokens
        store.shield.applicationCategories = applications.categoryTokens.isEmpty ? nil : ShieldSettings.ActivityCategoryPolicy.specific(applications.categoryTokens)
        
        //more rules
        store.media.denyExplicitContent = true
        
        //prevent app removal
        store.application.denyAppRemoval = true
        print("deny app removal: ",  store.application.denyAppRemoval ?? false)
        
        //prevent set date time
        store.dateAndTime.requireAutomaticDateAndTime = true
    }
    
    func stopAppRestrictions(){
        //lock application
        store.shield.applications = nil
        store.shield.applicationCategories = nil
        
        //more rules
        store.media.denyExplicitContent = false
        
        //prevent app removal
        store.application.denyAppRemoval = false
        print("deny app removal: ",  store.application.denyAppRemoval ?? false)
        
        //prevent set date time
        store.dateAndTime.requireAutomaticDateAndTime = false
    }
    
    func isAppLocked() -> Bool {
        let isShieldEmpty = (store.shield.applicationCategories == nil);
         return !isShieldEmpty
    }
    
    func countSelectedAppCategory() -> Int {
        let applications = MyModel.shared.familyActivitySelection
        return applications.categoryTokens.count
    }

    func countSelectedApp() -> Int {
        let applications = MyModel.shared.familyActivitySelection
        return applications.applicationTokens.count
    }
    
    func schedulingRestrictions() {
        
        print("Start monitor restriction")
        
        // Schedule restriction 15 minutes after started
        let now = Date()
        let fiveMinutesLater = Calendar.current.date(byAdding: .minute, value: 15, to: now)
        let schedule = DeviceActivitySchedule(intervalStart: Calendar.current.dateComponents([.hour, .minute], from: now),
                                              intervalEnd: Calendar.current.dateComponents([.hour, .minute], from: fiveMinutesLater ?? now),
                                              repeats: true,
                                              warningTime: nil)

        let center = DeviceActivityCenter()
        
        do {
            try center.startMonitoring(.restrictAppActivityName, during: schedule)
            
            print("Success with Starting Monitor Activity")
        }
        catch {
            print("Error with Starting Monitor Activity: \(error.localizedDescription)")
        }

        // Setting various properties of the ManagedSettingsStore instance
        let applications = MyModel.shared.familyActivitySelection
        if applications.applicationTokens.isEmpty {
            print("empty applicationTokens")
        }
        if applications.categoryTokens.isEmpty {
            print("empty categoryTokens")
        }
        print("monitoring model started")
    }
}

extension DeviceActivityName {
    static let restrictAppActivityName = Self("restrictApp")
}

1

There are 1 answers

0
Ahmed Yusuf On BEST ANSWER

Print log doesn't work on extension, I'm change it with local notification and it works