Device Activity Monitor

I'd like to block the apps selected in FamilyActivityPicker individually when a certain threshold is met.

For example, let's say the threshold is 15 minutes, and I want to block both Photos and Freeform. If I spend 15 minutes on Photos, Photos should be blocked. Then, if I spend 15 minutes on Freeform, Freeform should also be blocked.

Currently, only Photos gets blocked after 15 minutes, but Freeform does not. How can I fix this problem so that each app is blocked individually when its respective 15-minute threshold is met?

Thank you in advance

File 1 :


class GlobalSelection {
    static let shared = GlobalSelection()
    
    var selection = FamilyActivitySelection()

    private init() {}
}

extension DeviceActivityName{
  static let daily = Self("daily")
}

@objc(DeviceActivityMonitorModule)
class DeviceActivityMonitorModule: NSObject {
  
  private let store = ManagedSettingsStore()

  @objc
  func startMonitoring(_ limitInMinutes: Int) {
          
    let schedule = DeviceActivitySchedule(
      intervalStart: DateComponents(hour: 0, minute: 0),
      intervalEnd: DateComponents(hour: 23, minute: 59),
      repeats: true
    )
    
    let threshold = DateComponents(minute: limitInMinutes)
    
    var events: [DeviceActivityEvent.Name: DeviceActivityEvent] = [:]
    
    // Iterate over each selected application's token
    for token in GlobalSelection.shared.selection.applicationTokens {
        // Create a unique event name for each application
        let eventName = DeviceActivityEvent.Name("dailyLimitEvent_\(token)")
        
        // Create an event for this specific application
        let event = DeviceActivityEvent(
            applications: [token],  // Single app token
            threshold: threshold
        )
        
        // Add the event to the dictionary
        events[eventName] = event
    }
        
    // Register the monitor with the activity name and schedule
    do {
      try DeviceActivityCenter().startMonitoring(.daily, during: schedule, events: events)
      print("24/7 Monitoring started with time limit : \(limitInMinutes) m")
      
    } catch {
        print("Failed to start monitoring: \(error)")
    }
  
    
  }
  
  @objc
  static func requiresMainQueueSetup() -> Bool {
    return true
  }
}

FIle 2 :


class DeviceActivityMonitorExtension: DeviceActivityMonitor {
  
    let store = ManagedSettingsStore()
  var blockedApps: Set<ApplicationToken> = []
  
  
    func scheduleNotification(with title: String) {
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if granted {
                let content = UNMutableNotificationContent()
                content.title = "Notification" // Using the custom title here
                content.body = title
                content.sound = UNNotificationSound.default
                
                let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) // 5 seconds from now
                
                let request = UNNotificationRequest(identifier: "MyNotification", content: content, trigger: trigger)
                
                center.add(request) { error in
                    if let error = error {
                        print("Error scheduling notification: \(error)")
                    }
                }
            } else {
                print("Permission denied. \(error?.localizedDescription ?? "")")
            }
        }
    }
  
    // Function to retrieve selected apps
    func retrieveSelectedApps() -> FamilyActivitySelection? {
        if let sharedDefaults = UserDefaults(suiteName: "group.timelimit.com.zerodistract") {
            // Retrieve the encoded data
            if let data = sharedDefaults.data(forKey: "selectedAppsTimeLimit") {
                // Decode the data back into FamilyActivitySelection
                let decoder = JSONDecoder()
                if let selection = try? decoder.decode(FamilyActivitySelection.self, from: data) {
                    return selection
                }
            }
        }
        return nil // Return nil if there was an error
    }
  
    override func intervalDidStart(for activity: DeviceActivityName){
        super.intervalDidStart(for: activity)
        scheduleNotification(with: "Interval did start")
        scheduleNotification(with: "\(retrieveSelectedApps())")
    }
    
    override func intervalDidEnd(for activity: DeviceActivityName) {
        super.intervalDidEnd(for: activity)
    }
    
  override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
      super.eventDidReachThreshold(event, activity: activity)

      // Notify that the threshold is met
      scheduleNotification(with: "Threshold met")
      
      // Retrieve the selected apps
      if let selectedApps = retrieveSelectedApps() {
          // Extract the app token identifier from the event name
          let appTokenIdentifier = event.rawValue.replacingOccurrences(of: "dailyLimitEvent_", with: "")
          
          // Iterate over the selected application tokens
          for appToken in selectedApps.applicationTokens {
              // Convert the app token to a string representation (or use its debugDescription)
              let tokenString = "\(appToken)"
              
              // Check if the app token matches the token identifier in the event name
              if tokenString == appTokenIdentifier {
                
                blockedApps.insert(appToken)
                // Block only the app associated with this event
                store.shield.applications = blockedApps
                scheduleNotification(with: "store.shield.applications = blockedApps is reached")
                break
              }
          }
      } else {
          scheduleNotification(with: "No stored data for selectedAppsTimeLimit")
      }
  }
    override func intervalWillStartWarning(for activity: DeviceActivityName) {
        super.intervalWillStartWarning(for: activity)
        
        // Handle the warning before the interval starts.
    }
    
    override func intervalWillEndWarning(for activity: DeviceActivityName) {
        super.intervalWillEndWarning(for: activity)
        
        // Handle the warning before the interval ends.
    }
    
    override func eventWillReachThresholdWarning(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
        super.eventWillReachThresholdWarning(event, activity: activity)
        
        // Handle the warning before the event reaches its threshold.
    }
}

You can do something like this:

  1. Unlock app for certain period of time via ShieldActionExtension, during this unlock process save that app using it's Token in shared preference (Shared User Defaults).

You can save using this procedure:

struct ApplicationProfile: Codable, Hashable {
    let id: UUID
    let applicationToken: ApplicationToken
    
    init(id: UUID = UUID(), applicationToken: ApplicationToken) {
        self.applicationToken = applicationToken
        self.id = id
    }
}

// You can get this token from ShieldAction delegate method handle action
let profile = ApplicationProfile(applicationToken: token)
SharedData.addApplicationProfile(profile)
  1. The app you have unlocked is saved, now schedule an event according to your desired threshold using the profile you have in your shared cache.
       
let unlockTime = 900 // Time in seconds
 let threshold = DateComponents(second: unlockTime)
        
 let event: [DeviceActivityEvent.Name: DeviceActivityEvent] = [
            DeviceActivityEvent.Name(profile.id.uuidString): DeviceActivityEvent(
                applications: Set<ApplicationToken>([application]),
                threshold: threshold
            )
        ]
        
let center = DeviceActivityCenter()
        
 let schedule = DeviceActivitySchedule(
            intervalStart: DateComponents(hour: 0, minute: 0),
            intervalEnd: DateComponents(hour: 23, minute: 59),
            repeats: true
        )
        do {
            try center.startMonitoring(DeviceActivityName(profile.id.uuidString), during: schedule, events: event)
        } catch {
            print("Device monitoring error: \(error)")
        }
    }

This will schedule a device activity for 24 hours, which will monitor the app usage.

  1. Once that threshold reached (user have used app for 15 minutes in your case) your DeviceActivityMonitorExtension delegate method eventDidReachThreshold will get called

Now lock the app again.

    override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
        super.eventDidReachThreshold(event, activity: activity)
        // Handle the event reaching its threshold.
                
        guard let activityId = UUID(uuidString: activity.rawValue) else { return }
        guard let application = SharedData.getApplicationProfile(id: activityId) else { return }
        store.shield.applications?.insert(application.applicationToken)
        SharedData.removeApplicationProfile(application)
        
        logger.log("DeviceActivityMonitorExtension: Apps shield set again")
    }

In this way you will be able to apply shield on respective app after your threshold reached.

Hope that helps you.

Device Activity Monitor
 
 
Q