Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/apple-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ Ideally, this would be generated automatically based on a fully qualified Xcode
| network-dns-proxy | DNS Proxy Network Extension |
| network-filter-data | Filter Data Network Extension |
| content-blocker | Safari Content Blocker Extension |
| file-provider | File Provider Extension |
| broadcast-upload | Broadcast Upload Extension |
| call-directory | Call Directory Extension |
| message-filter | Message Filter Extension |


<!-- | imessage | iMessage Extension | -->
Expand All @@ -319,13 +323,9 @@ The following iOS extension types exist in Xcode but aren't supported yet. Contr

| Extension Point Identifier | Xcode Template Name |
| --- | --- |
| `com.apple.fileprovider-nonui` | File Provider Extension |
| `com.apple.fileprovider-actionsui` | File Provider UI Extension |
| `com.apple.broadcast-services-upload` | Broadcast Upload Extension |
| `com.apple.broadcast-services-setupui` | Broadcast Setup UI Extension |
| `com.apple.callkit.call-directory` | Call Directory Extension |
| `com.apple.classkit.context-provider` | ClassKit Context Provider Extension |
| `com.apple.identitylookup.message-filter` | Message Filter Extension |
| `com.apple.identitylookup.classification-ui` | Unwanted Communication Reporting Extension |
| `com.apple.photo-editing` | Photo Editing Extension |
| `com.apple.printing.discovery` | Print Service Extension |
Expand Down
8 changes: 8 additions & 0 deletions packages/apple-targets/e2e/__tests__/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ const TARGET_REGISTRY: TargetEntry[] = [
"Watch target requires AppIcon asset catalog which is not generated without an icon config",
},
{ type: "widget", dir: "widget", target: "widget" },
{ type: "file-provider", dir: "file-provider", target: "fileprovider" },
{
type: "broadcast-upload",
dir: "broadcast-upload",
target: "broadcastupload",
},
{ type: "call-directory", dir: "call-directory", target: "calldirectory" },
{ type: "message-filter", dir: "message-filter", target: "messagefilter" },
];

// Derived from the central TARGET_REGISTRY — no need to maintain by hand.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "type": "broadcast-upload" }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "type": "call-directory" }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "type": "file-provider" }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "type": "message-filter" }
4 changes: 4 additions & 0 deletions packages/apple-targets/src/configuration-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,10 @@ function getConfigurationListBuildSettingsForType(
case "quicklook-thumbnail":
case "spotlight":
case "content-blocker":
case "file-provider":
case "broadcast-upload":
case "call-directory":
case "message-filter":
return createNotificationContentConfigurationList(props);
default:
const exhaustiveCheck: never = props.type;
Expand Down
54 changes: 54 additions & 0 deletions packages/apple-targets/src/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,27 @@ export const TARGET_REGISTRY = {
displayName: "Content Blocker",
description: "Safari content blocker extension",
},
"file-provider": {
extensionPointIdentifier: "com.apple.fileprovider-nonui",
frameworks: ["UniformTypeIdentifiers"],
appGroupsByDefault: true,
displayName: "File Provider",
},
"broadcast-upload": {
extensionPointIdentifier: "com.apple.broadcast-services-upload",
frameworks: ["ReplayKit"],
displayName: "Broadcast Upload",
},
"call-directory": {
extensionPointIdentifier: "com.apple.callkit.call-directory",
frameworks: ["CallKit"],
displayName: "Call Directory",
},
"message-filter": {
extensionPointIdentifier: "com.apple.identitylookup.message-filter",
frameworks: ["IdentityLookup"],
displayName: "Message Filter",
},
} as const satisfies Record<string, TargetDefinition>;

export type ExtensionType = keyof typeof TARGET_REGISTRY;
Expand Down Expand Up @@ -474,6 +495,39 @@ export function getTargetInfoPlistForType(type: ExtensionType) {
"$(PRODUCT_MODULE_NAME).FilterDataProvider",
},
};
case "file-provider":
return {
NSExtension: {
NSExtensionPointIdentifier,
NSExtensionPrincipalClass:
"$(PRODUCT_MODULE_NAME).FileProviderExtension",
NSExtensionFileProviderSupportsEnumeration: true,
},
};
case "broadcast-upload":
return {
NSExtension: {
NSExtensionPointIdentifier,
NSExtensionPrincipalClass: "$(PRODUCT_MODULE_NAME).SampleHandler",
RPBroadcastProcessMode: "RPBroadcastProcessModeSampleBuffer",
},
};
case "call-directory":
return {
NSExtension: {
NSExtensionPointIdentifier,
NSExtensionPrincipalClass:
"$(PRODUCT_MODULE_NAME).CallDirectoryHandler",
},
};
case "message-filter":
return {
NSExtension: {
NSExtensionPointIdentifier,
NSExtensionPrincipalClass:
"$(PRODUCT_MODULE_NAME).MessageFilterExtension",
},
};
default:
// Default: used for widget and bg-download
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ module.exports = config => ({
});"
`;

exports[`getTemplateConfig should return a valid template for broadcast-upload 1`] = `
"/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = config => ({
type: \\"broadcast-upload\\",
entitlements: { /* Add entitlements */ },
});"
`;

exports[`getTemplateConfig should return a valid template for call-directory 1`] = `
"/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = config => ({
type: \\"call-directory\\",
entitlements: { /* Add entitlements */ },
});"
`;

exports[`getTemplateConfig should return a valid template for clip 1`] = `
"/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = config => ({
Expand All @@ -59,6 +75,14 @@ module.exports = config => ({
});"
`;

exports[`getTemplateConfig should return a valid template for file-provider 1`] = `
"/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = config => ({
type: \\"file-provider\\",
entitlements: { /* Add entitlements */ },
});"
`;

exports[`getTemplateConfig should return a valid template for intent 1`] = `
"/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = config => ({
Expand All @@ -83,6 +107,14 @@ module.exports = config => ({
});"
`;

exports[`getTemplateConfig should return a valid template for message-filter 1`] = `
"/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = config => ({
type: \\"message-filter\\",
entitlements: { /* Add entitlements */ },
});"
`;

exports[`getTemplateConfig should return a valid template for notification-content 1`] = `
"/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = config => ({
Expand Down
4 changes: 4 additions & 0 deletions packages/create-target/src/__tests__/createAsync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const ALL_TARGET_TYPES = [
"credentials-provider",
"account-auth",
"device-activity-monitor",
"file-provider",
"broadcast-upload",
"call-directory",
"message-filter",
];

describe(getTemplateConfig, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import ReplayKit

class SampleHandler: RPBroadcastSampleHandler {

override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
}

override func broadcastPaused() {
// User has requested to pause the broadcast. Samples will stop being delivered.
}

override func broadcastResumed() {
// User has requested to resume the broadcast. Samples delivery will resume.
}

override func broadcastFinished() {
// User has requested to finish the broadcast.
}

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
switch sampleBufferType {
case RPSampleBufferType.video:
// Handle video sample buffer
break
case RPSampleBufferType.audioApp:
// Handle audio sample buffer for app audio
break
case RPSampleBufferType.audioMic:
// Handle audio sample buffer for mic audio
break
@unknown default:
// Handle other sample buffer types
fatalError("Unknown type of sample buffer")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation
import CallKit

class CallDirectoryHandler: CXCallDirectoryProvider {

override func beginRequest(with context: CXCallDirectoryExtensionContext) {
context.delegate = self

// Check whether this is an "incremental" data request. If so, only provide the set of phone number blocking
// and identification entries which have been added or removed since the last time this extension's data was loaded.
// But the first time this is called, you must provide the full set of data.
if context.isIncremental {
addOrRemoveIncrementalBlockingPhoneNumbers(to: context)
addOrRemoveIncrementalIdentificationPhoneNumbers(to: context)
} else {
addAllBlockingPhoneNumbers(to: context)
addAllIdentificationPhoneNumbers(to: context)
}

context.completeRequest()
}

private func addAllBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
// Retrieve all phone numbers to block from data store. For optimal performance and target
// having a responsive user interface, consider only loading a subset of numbers at a given time
// and using autoreleasepools to release objects allocated during each batch of numbers which are loaded.
//
// Numbers must be provided in numerically ascending order.
let allPhoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1_408_555_5555, 1_800_555_5555 ]
for phoneNumber in allPhoneNumbers {
context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
}
}

private func addOrRemoveIncrementalBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
// Retrieve any changes to the set of phone numbers to block from data store.
}

private func addAllIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
// Retrieve phone numbers to identify and their identification labels from data store.
//
// Numbers must be provided in numerically ascending order.
let allPhoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1_877_555_5555, 1_888_555_5555 ]
let labels = [ "Telemarketer", "Local business" ]

for (phoneNumber, label) in zip(allPhoneNumbers, labels) {
context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
}
}

private func addOrRemoveIncrementalIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
// Retrieve any changes to the set of phone numbers to identify from data store.
}

}

extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {

func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) {
// An error occurred while adding blocking or identification entries, check the NSError for details.
// For Call Directory error codes, see the CXErrorCodeCallDirectoryManagerError enum in <CallKit/CXError.h>.
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import FileProvider

class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {

private let enumeratedItemIdentifier: NSFileProviderItemIdentifier

init(enumeratedItemIdentifier: NSFileProviderItemIdentifier) {
self.enumeratedItemIdentifier = enumeratedItemIdentifier
super.init()
}

func invalidate() {
// Clean up any resources
}

func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
observer.finishEnumerating(upTo: nil)
}

func enumerateChanges(for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor) {
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
}

func currentSyncAnchor(completionHandler: @escaping (NSFileProviderSyncAnchor?) -> Void) {
completionHandler(NSFileProviderSyncAnchor("anchor".data(using: .utf8)!))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import FileProvider
import UniformTypeIdentifiers

class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {

required init(domain: NSFileProviderDomain) {
super.init()
}

func invalidate() {
// Clean up any resources
}

func item(for identifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) -> Progress {
completionHandler(FileProviderItem(identifier: identifier), nil)
return Progress()
}

func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier, version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest, completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress {
completionHandler(nil, nil, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError))
return Progress()
}

func createItem(basedOn itemTemplate: NSFileProviderItem, fields: NSFileProviderItemFields, contents url: URL?, options: NSFileProviderCreateItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
completionHandler(itemTemplate, [], false, nil)
return Progress()
}

func modifyItem(_ item: NSFileProviderItem, baseVersion version: NSFileProviderItemVersion, changedFields: NSFileProviderItemFields, contents newContents: URL?, options: NSFileProviderModifyItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
completionHandler(item, [], false, nil)
return Progress()
}

func deleteItem(identifier: NSFileProviderItemIdentifier, baseVersion version: NSFileProviderItemVersion, options: NSFileProviderDeleteItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (Error?) -> Void) -> Progress {
completionHandler(nil)
return Progress()
}

func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest) throws -> NSFileProviderEnumerator {
return FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import FileProvider
import UniformTypeIdentifiers

class FileProviderItem: NSObject, NSFileProviderItem {

let itemIdentifier: NSFileProviderItemIdentifier

init(identifier: NSFileProviderItemIdentifier) {
self.itemIdentifier = identifier
}

var identifier: NSFileProviderItemIdentifier {
return itemIdentifier
}

var parentItemIdentifier: NSFileProviderItemIdentifier {
return .rootContainer
}

var capabilities: NSFileProviderItemCapabilities {
return [.allowsReading, .allowsWriting, .allowsRenaming, .allowsReparenting, .allowsTrashing, .allowsDeleting]
}

var filename: String {
return "Example File"
}

var contentType: UTType {
return .plainText
}
}
Loading