// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/app/background_refresh/background_refresh_app_agent.h"
#import <BackgroundTasks/BackgroundTasks.h>
#import "base/ios/block_types.h"
#import "base/logging.h"
#import "base/metrics/histogram_functions.h"
#import "base/task/sequenced_task_runner.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/background_refresh/app_refresh_provider.h"
#import "ios/chrome/app/background_refresh/background_refresh_metrics.h"
#import "ios/chrome/app/background_refresh_constants.h"
#import "ios/chrome/browser/shared/public/features/features.h"
@interface BGTaskScheduler (cheating)
- (void)_simulateLaunchForTaskWithIdentifier:(NSString*)ident;
@end
@interface BackgroundRefreshAppAgent ()
@property(nonatomic) NSMutableSet<AppRefreshProvider*>* providers;
@end
@implementation BackgroundRefreshAppAgent
- (instancetype)init {
if ((self = [super init])) {
_providers = [NSMutableSet set];
[self registerBackgroundRefreshTask];
}
return self;
}
- (void)addAppRefreshProvider:(AppRefreshProvider*)provider {
CHECK(provider);
[self.providers addObject:provider];
}
- (void)requestAppRefreshWithDelay:(NSTimeInterval)delay {
// Schedule requests only if flag is enabled.
if (!IsAppBackgroundRefreshEnabled()) {
return;
}
// TODO(crbug.com/354918222): coalesce multiple requests so there's only ever
// a single scheduled refresh pending.
BGAppRefreshTaskRequest* request = [[BGAppRefreshTaskRequest alloc]
initWithIdentifier:kAppBackgroundRefreshTaskIdentifier];
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:delay];
NSError* error = nil;
[BGTaskScheduler.sharedScheduler submitTaskRequest:request error:&error];
BGTaskSchedulerErrorActions action = BGTaskSchedulerErrorActions::kUnknown;
if (error) {
BGTaskSchedulerErrorCode code = (BGTaskSchedulerErrorCode)error.code;
switch (code) {
case BGTaskSchedulerErrorCodeUnavailable:
action = BGTaskSchedulerErrorActions::kErrorCodeUnavailable;
break;
case BGTaskSchedulerErrorCodeNotPermitted:
action = BGTaskSchedulerErrorActions::kErrorCodeNotPermitted;
break;
case BGTaskSchedulerErrorCodeTooManyPendingTaskRequests:
action =
BGTaskSchedulerErrorActions::kErrorCodeTooManyPendingTaskRequests;
break;
}
} else {
action = BGTaskSchedulerErrorActions::kSuccess;
}
base::UmaHistogramEnumeration(kBGTaskSchedulerErrorHistogram, action);
// Time-saving debug mode.
if (delay == 0.0) {
[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:
kAppBackgroundRefreshTaskIdentifier];
}
}
#pragma mark - Private
- (void)registerBackgroundRefreshTask {
auto handler = ^(BGTask* task) {
[self systemTriggeredRefreshForTask:task];
};
// TODO(crbug.com/354919106): Consider moving this task to a queue known to
// Chromium, so it's easy to safely thread hop.
[BGTaskScheduler.sharedScheduler
registerForTaskWithIdentifier:kAppBackgroundRefreshTaskIdentifier
usingQueue:nil
launchHandler:handler];
}
// Debugging note: To induce the scheduler to call this task, you should
// (1) Set a breakpoint sometime after `-registerBaskgroundRefreshTask` is
// called.
// (2) When the app is paused, run the following command in the debugger:
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler]
// _simulateLaunchForTaskWithIdentifier:@"chrome.app.refresh"]
// (3) Resume execution.
//
// Note that calling -requestAppRefreshWithDelay: with a delay value of 0.0
// will call _simulateLaunchForTaskWithIdentifier: immediately.
//
// To trigger the expiration handler (that is, to forcibly expire the task):
// (1) Set a breakpoint in the followsing method, after the for-loop that
// calls all of the providers.
// (2) Make sure this method is called by triggering the task as described
// above.
// (3) When the app is paused, run the following command in the debugger:
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler]
// _simulateExpirationForTaskWithIdentifier:@"chrome.app.refresh"]
// (4) Resume execution.
//
// Remember also that BACKGROUND REFRESH REQUIRES A DEVICE. It doesn't work
// on simulators at all.
// Handle background refresh. This is called by the (OS) background task
// scheduler and is **not called on the main thread**.
- (void)systemTriggeredRefreshForTask:(BGTask*)task {
// TODO(crbug.com/354919106): This is the simplest possible implementation,
// and it provides no thread safety or signalling to the app state about
// status. Some of the many things that must be implemented for this to work
// correctly:
// - No processing if this is a safe mode launch.
// - Configure an expiration handler on `task`, which cancels all refresh
// tasks.
// - Update the app state for both starting and ending refresh work; this
// must hapopen on the main thread, and further processing should wait on
// it.
// - Handle tracking completion of each task, and only signal success if
// all tasks succeeded overall.
ProceduralBlock completion = ^{
[task setTaskCompletedWithSuccess:YES];
};
for (AppRefreshProvider* provider in self.providers) {
// Only execute due tasks.
if ([provider isDue]) {
[provider handleRefreshWithCompletion:completion];
}
}
}
@end