Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ios): new background tasks api for iOS 13+ #11689

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 24 additions & 17 deletions build/lib/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,34 +104,41 @@ class Builder {
this.program.gitHash = hash || 'n/a';
}

async transpile(platform, babelOptions, outFile) {
async transpile(platform, babelOptions, outPath) {
// Copy over common dir, into some temp dir
// Then run rollup/babel on it, then just copy the resulting bundle to our real destination!
// The temporary location we'll assembled the transpiled bundle
const TMP_COMMON_DIR = path.join(TMP_DIR, '_common');
const TMP_COMMON_PLAFORM_DIR = path.join(TMP_DIR, '_common', platform);
if (!outPath) {
outPath = path.join(TMP_DIR, 'common', platform);
}

console.log(`Creating temporary 'common' directory...`); // eslint-disable-line quotes
await fs.copy(path.join(ROOT_DIR, 'common'), TMP_COMMON_PLAFORM_DIR);

// create a bundle
console.log('Transpile and run rollup...');
const bundle = await rollup({
input: `${TMP_COMMON_PLAFORM_DIR}/Resources/ti.main.js`,
plugins: [
resolve(),
commonjs(),
babel(determineBabelOptions(babelOptions))
],
external: [ './app', 'com.appcelerator.aca' ]
});

if (!outFile) {
outFile = path.join(TMP_DIR, 'common', platform, 'ti.main.js');
}

console.log(`Writing 'common' bundle to ${outFile} ...`); // eslint-disable-line quotes
await bundle.write({ format: 'cjs', file: outFile });
const configs = [
{ entry: 'ti.main.js' },
{ entry: 'ti.task.js' }
];
await Promise.all(configs.map(async (config) => {
const { entry, plugins = [] } = config;
const bundle = await rollup({
input: `${TMP_COMMON_PLAFORM_DIR}/Resources/${entry}`,
plugins: [
resolve(),
commonjs(),
babel(determineBabelOptions(babelOptions)),
...plugins
],
external: [ './app', 'com.appcelerator.aca' ]
});
const outFile = path.join(outPath, entry);
console.log(`Writing '${entry}' bundle to ${outFile} ...`); // eslint-disable-line quotes
await bundle.write({ format: 'cjs', file: outFile });
}));

// We used to have to copy over ti.internal, but it is now bundled into ti.main.js
// if we ever have files there that cannot be bundled or are not hooked up properly, we'll need to copy them here manually.
Expand Down
2 changes: 1 addition & 1 deletion build/scons-xcode-project-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async function generateBundle(outputDir) {
const builder = new Builder({ args: [ 'ios' ] });
const ios = new IOS({ });

await builder.transpile('ios', ios.babelOptions, path.join(outputDir, 'ti.main.js'));
await builder.transpile('ios', ios.babelOptions(), outputDir);
}

async function main(tmpBundleDir) {
Expand Down
16 changes: 9 additions & 7 deletions common/Resources/ti.internal/extensions/ti/ti.blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
if (Ti.Platform.osname === 'iphone' || Ti.Platform.osname === 'ipad') {
const buffer = Ti.createBuffer({ value: '' });
const blob = buffer.toBlob();
blob.constructor.prototype.toString = function () {
const value = this.text;
return (value === undefined) ? '[object TiBlob]' : value;
};
if (blob) {
blob.constructor.prototype.toString = function () {
const value = this.text;
return (value === undefined) ? '[object TiBlob]' : value;
};

if ((parseInt(Ti.Platform.version.split('.')[0]) < 11)) {
// This is hack to fix TIMOB-27707. Remove it after minimum target set iOS 11+
setTimeout(function () {}, Infinity);
if ((parseInt(Ti.Platform.version.split('.')[0]) < 11)) {
// This is hack to fix TIMOB-27707. Remove it after minimum target set iOS 11+
setTimeout(function () {}, Infinity);
}
}
}
16 changes: 16 additions & 0 deletions common/Resources/ti.task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Appcelerator Titanium Mobile
* Copyright (c) 2020 by Axway, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*
* This script is loaded on background task startup on all platforms. It is used to load
* JS polyfills and Titanium's core JavaScript extensions shared by all platforms.
*/

// Load JS language polyfills
import 'core-js/es';
// Load polyfill for async/await usage
import 'regenerator-runtime/runtime';
// import all of our polyfills/extensions
import './ti.internal/extensions';
87 changes: 87 additions & 0 deletions iphone/Classes/RunScriptOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// RunScriptOperation.swift
// Titanium
//
// Created by Jan Vennemann on 24.04.20.
//

import Foundation
import TitaniumKit

@available(iOS 13.0, *)
class RunScriptOperation : Operation {
private let url: String
private let host: TiHost
private var bridge: KrollBridge? = nil
private var resolved = false
private var contextRunning = false

@objc
init(url: String, host: TiHost) {
self.url = url
self.host = host
}

override var isExecuting: Bool {
return self.contextRunning
}

override var isFinished: Bool {
return self.resolved
}

override func start() {
guard !isCancelled else {
return
}

self.bridge = KrollBridge(host: self.host)
let url = TiHost.resourceBasedURL("ti.task.js", baseURL: nil)
// pass in an empty preload dictionary to force booting into our provided url
// instead of ti.main.js
bridge?.boot(self, url: url, preload: [:])

willChangeValue(forKey: #keyPath(isExecuting))
contextRunning = true
didChangeValue(forKey: #keyPath(isExecuting))
}

override func cancel() {
super.cancel()
let condition = NSCondition()
self.bridge?.shutdown(condition)
}

@objc(booted:)
public func booted(_ bridge: KrollBridge) {
let context = JSContext(jsGlobalContextRef: bridge.krollContext()?.context())!
context.exceptionHandler = { (context, value) in
if let error = value {
NSLog("Error: \(error.toString() ?? "\(error)")")
} else {
NSLog("Unknown error in background task")
}
}
let task = context.evaluateScript("require('\(self.url)');");
let callbackBlock: @convention(block) () -> Void = {
self.finish()
}
let callback = JSValue(object: callbackBlock, in: context)!;
task?.call(withArguments: [callback]);
}

func finish() {
let condition = NSCondition()
self.bridge?.shutdown(condition)

// TODO: is there a way to check when bridge shutdown is complete?
willChangeValue(forKey: #keyPath(isExecuting))
willChangeValue(forKey: #keyPath(isFinished))

self.contextRunning = false
self.resolved = true

didChangeValue(forKey: #keyPath(isFinished))
didChangeValue(forKey: #keyPath(isExecuting))
}
}
157 changes: 157 additions & 0 deletions iphone/Classes/TiAppiOSBackgroundTaskProxy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//
// TiAppiOSBackgroundTaskProxy.swift
// Titanium
//
// Created by Jan Vennemann on 24.04.20.
//

import TitaniumKit
import BackgroundTasks

@available(iOS 13.0, *)
@objc
public class TiAppiOSBackgroundTaskProxy : TiProxy {

var type: String {
get {
return self.value(forUndefinedKey: "type") as! String
}
}

var identifier: String {
get {
return self.value(forUndefinedKey: "identifier") as! String
}
}

var url: String {
get {
return self.value(forUndefinedKey: "url") as! String
}
}

var interval: Double? {
if let interval = self.value(forUndefinedKey: "interval") as? NSNumber {
return interval.doubleValue
} else {
return nil
}
}

var options: [String] {
get {
return self.value(forUndefinedKey: "options") as? [String] ?? []
}
}

private var _repeat: JSValue {
get {
guard let rawValue = self.value(forUndefinedKey: "repeat") else {
return JSValue(bool: false, in: self.context)
}
if let number = rawValue as? NSNumber {
return JSValue(bool: number.boolValue, in: self.context)
} else if let function = rawValue as? KrollCallback {
return JSValue(jsValueRef: function.function(), in: self.context)
} else {
return JSValue(bool: false, in: self.context)
}
}
}

private var context: JSContext!

public override func _init(withPageContext context: TiEvaluator!) -> Self? {
if let self = super._init(withPageContext: context) as! Self? {
self.context = JSContext(jsGlobalContextRef: context.krollContext()?.context())
}

return self
}

public override func _init(withProperties properties: [AnyHashable : Any]!) {
guard let type = properties["type"] as? String, (type == "refresh" || type == "processing") else {
self.throwException("Invalid task", subreason: "Missing or invalid type", location: CODELOCATION)
return;
}
guard let identifier = properties["identifier"] as? String else {
self.throwException("Invalid task", subreason: "No identifier specified", location: CODELOCATION)
return;
}
guard let url = properties["url"] as? String else {
self.throwException("Invalid task", subreason: "No url specified", location: CODELOCATION)
return;
}

super._init(withProperties: properties)
}

@objc
public func schedule() {
var request: BGTaskRequest
if self.type == "refresh" {
request = BGAppRefreshTaskRequest(identifier: self.identifier)
} else {
let processingRequest = BGProcessingTaskRequest(identifier: self.identifier)
if self.options.contains("network") {
processingRequest.requiresNetworkConnectivity = true
}
if self.options.contains("power") {
processingRequest.requiresExternalPower = true
}
request = processingRequest
}
if let interval = self.interval {
request.earliestBeginDate = Date(timeIntervalSinceNow: interval)
}

do {
try BGTaskScheduler.shared.submit(request)
} catch {
NSLog("Could not schedule background task: \(error)")
}
}

func shouldRepeat() -> Bool {
if (self._repeat.isBoolean) {
return self._repeat.toBool()
} else if (self._repeat.isFunction) {
return self._repeat.call(withArguments: [])?.toBool() ?? false
} else {
return false
}
}
}

@available(iOS 13.0, *)
extension TiApp {
@objc(registerBackgroundTask:)
func registerBackgroundTask(_ taskProxy: TiAppiOSBackgroundTaskProxy) {
if (self.backgroundTasks == nil) {
self.backgroundTasks = NSMutableDictionary()
}
self.backgroundTasks.setObject(taskProxy, forKey: taskProxy.identifier as NSString);
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskProxy.identifier, using: nil) { (task) in
self.handleBackgroundTask(task)
}
}

func handleBackgroundTask(_ task: BGTask) {
let taskProxy = self.backgroundTasks[task.identifier] as! TiAppiOSBackgroundTaskProxy
if (taskProxy.shouldRepeat()) {
taskProxy.schedule()
}
let url = taskProxy.url
let queue = Foundation.OperationQueue()

let operation = RunScriptOperation(url: url, host: taskProxy._host())
task.expirationHandler = {
queue.cancelAllOperations()
}
operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}

queue.addOperations([operation], waitUntilFinished: false)
}
}