How to get FamilyActivitySelection from DeviceActivityMonitorExtension in iOS Screen Time API

721 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 I can't get FamilyActivitySelection values from DeviceActivityMonitorExtension, and it cause selected app always empty and I can't do restriction.

I have attempted various ways, such as:

  1. Using singleton on MyModel
  2. Using same App Groups both App & Extension Target
  3. Using UserDefault to save & load FamilyActivitySelection values (already tested on App and it returns correct value, but when I count it from DeviceActivityMonitorExtension and shows with local notification, the value is always empty)

Is there any solution to address this matter?

import Foundation

import FamilyControls
import ManagedSettings
import DeviceActivity
import UserNotifications

class MyModel: ObservableObject {
    let store = ManagedSettingsStore(named: .mySettingStore)
    static let shared = MyModel()
    
    @Published var familyActivitySelection: FamilyActivitySelection
    
    // Used to encode codable to UserDefaults
    private let encoder = PropertyListEncoder()

    // Used to decode codable from UserDefaults
    private let decoder = PropertyListDecoder()

    private let userDefaultsKey = "ScreenTimeSelection"
    
    //save family activity selection to UserDefault
    func saveFamilyActivitySelection(selection: FamilyActivitySelection) {
        print("selected app updated: ", selection.applicationTokens.count," category: ", selection.categoryTokens.count)
        let defaults = UserDefaults.standard

        defaults.set(
            try? encoder.encode(selection),
            forKey: userDefaultsKey
        )
        
        //check is data saved to user defaults
        getSavedFamilyActivitySelection()
    }
    
    //get saved family activity selection from UserDefault
    func getSavedFamilyActivitySelection() -> FamilyActivitySelection? {
        let defaults = UserDefaults.standard
        guard let data = defaults.data(forKey: userDefaultsKey) else {
            return nil
        }
        var selectedApp: FamilyActivitySelection?
        let decoder = PropertyListDecoder()
        selectedApp = try? decoder.decode(FamilyActivitySelection.self, from: data)
        
        print("saved selected app updated: ", selectedApp?.categoryTokens.count ?? "0")
        return selectedApp
    }

    init() {
        familyActivitySelection = FamilyActivitySelection()
    }
    
    func saveToStore(apps : Set<Application>){
        store.application.blockedApplications = apps
    }
    
    func startAppRestrictions() {
        print("Start App Restriction")
        
        // Pull the selection out of the app's model and configure the application shield restriction accordingly
//        let applications = MyModel.shared.familyActivitySelection
        let applications = getSavedFamilyActivitySelection()
        
        if(applications == nil){
            print("application not selected")
            return
        }
        

        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
        
        //prevent set date time
        store.dateAndTime.requireAutomaticDateAndTime = true
        store.application.blockedApplications = applications!.applications
        
    }
    
    func stopAppRestrictions(){
        print("Stop App Restriction")
        store.clearAllSettings()
    }
    
    func isAppLocked() -> Bool {
        let isShieldEmpty = (store.shield.applicationCategories == nil);
        return !isShieldEmpty
    }
    
    // count selected category
    func countSelectedAppCategory() -> Int {
//        let applications = MyModel.shared.familyActivitySelection
        let applications = getSavedFamilyActivitySelection()
        
        if(applications == nil){
            print("application not selected")
            return 0
        }
        return applications!.categoryTokens.count
    }
    
    // count selected category
    func countSelectedApp() -> Int {
        let applications = getSavedFamilyActivitySelection()
        if(applications == nil){
            print("category not selected")
            return 0
        }
        return applications!.applicationTokens.count
    }
    
    // start scheduling restriction with DeviceActivitySchedule
    func schedulingRestrictions(scheduleInSecond: Double) {
        
        print("Start monitor restriction, by", scheduleInSecond, "seconds")
        
        //schedule device activity from now to n-second's
        let startSchedulingTime = Date()
        let endSchedulingTime = Calendar.current.date(byAdding: .second, value: Int(scheduleInSecond), to: startSchedulingTime)
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "HH:mm:ss"
        print("Scheduling monitor started on ",dateFormatter.string(from: startSchedulingTime))
        print("Scheduling monitor will end on ",dateFormatter.string(from: endSchedulingTime ?? Date()))
        let schedule = DeviceActivitySchedule(intervalStart: Calendar.current.dateComponents([.hour, .minute], from: startSchedulingTime),intervalEnd: Calendar.current.dateComponents([.hour, .minute], from: endSchedulingTime ?? startSchedulingTime), repeats: true, warningTime: nil)

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

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


extension ManagedSettingsStore.Name {
    static let mySettingStore = Self("mySettingStore")
}
//
//  DeviceActivityMonitorExtension.swift
//  NewMonitor
//
//  Created by Mochammad Yusuf Fachroni on 18/08/23.
//

import DeviceActivity
import UserNotifications
import ManagedSettings


// Optionally override any of the functions below.
// Make sure that your class name matches the NSExtensionPrincipalClass in your Info.plist.

class DeviceActivityMonitorExtension: DeviceActivityMonitor {
    let model = MyModel.shared
    
    func showLocalNotification(title: String, desc: String) {
        let countSelectedAppToken =  model.countSelectedApp()
        let countSelectedCategoryToken =  model.countSelectedAppCategory()

          let content = UNMutableNotificationContent()
          content.title = title
          content.body = "Selected app: "+String(countSelectedAppToken)+" category: "+String(countSelectedCategoryToken)
          content.sound = .default

          let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
          let request = UNNotificationRequest(identifier: "localNotification", content: content, trigger: trigger)

          UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
              print("Failed to show notification: \(error.localizedDescription)")
            }
          }

        let center = UNUserNotificationCenter.current()
        center.add(request) { (error) in
            if let error = error {
                print("Failed to add notification: \(error)")
            } else {
                print("Success add notification")
            }
        }
    }
    
    override func intervalDidStart(for activity: DeviceActivityName) {
        super.intervalDidStart(for: activity)
//        model.startAppRestrictions()
        showLocalNotification(title: "My Start Restrict App", desc: "Restriction App started successfully")
        
        let socialStore = ManagedSettingsStore(named: .mySettingStore)
        socialStore.clearAllSettings()
        model.startAppRestrictions()
    }
    
    override func intervalDidEnd(for activity: DeviceActivityName) {
        super.intervalDidEnd(for: activity)
        model.stopAppRestrictions()
        showLocalNotification(title: "My Restriction Stopped", desc: "Restriction App is stopped")
    }   
}
1

There are 1 best solutions below

0
Jorn Rigter On

I see you're using UserDefaults.standard. You set up the App Group entitlement correctly it seems, but when saving from your main app to storage, you have to save it to a UserDefaults group that the extension can access as well:

let sharedStorage = UserDefaults(suiteName: "YOUR_SHARED_STORAGE_NAME")!

Without that, you will not be able to retrieve the correct values from your extension, since they're different processes.