Breezy
Jailbreak implementation & research for AirDrop on tvOS
Install / Use
/learn @lechium/BreezyREADME
Breezy
Jailbreak implementation & research for AirDrop on tvOS.
Unified implementation
In the latest update the implementation has been improved and standardized to be more consistent with what you expect / experience on iOS and macOS when adding AirDrop support. Utilizing UTI types and Document types to enable users to discern what application (if there are multiple) will open / import the files.
Below you will find some resources on how to edit your Info.plist file to add AirDrop receiver support to your app.
It is also necessary to handle opening file URL's (done the same way it is in iOS) to handle the files being fed to you through launch services.
Will use my changes to RetroArch here as the implementation example:
Example of an incoming notification
app: <UIApplication: 0x14be16ed0> app
openURL: file:///Library/Caches/com.nito.AirPhoto/Screen%20Shot%202019-12-17%20at%206.47.15%20PM.png url,
options: {
UIApplicationOpenURLOptionsAnnotationKey = {
LSDocumentDropCount = 13;
LSDocumentDropIndex = 0;
LSMoveDocumentOnOpen = 0;
};
UIApplicationOpenURLOptionsOpenInPlaceKey = 0;
UIApplicationOpenURLOptionsSourceApplicationKey = "com.apple.PineBoard";
}
You will notice a LSDocumentDropCount & LSDocumentDropIndex key, these are special keys we add so you can either support the files in a cluster once they are all received, OR you can process them one at a time. This part is completely up to you on implementation. For instance nitoTV wants to process DEB files in a cluster because they may include dependencies needed for the packages to install. However, the drawback is things will not start visibly processing until it has received ALL of the files.
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
NSFileManager *man = [NSFileManager defaultManager];
NSLog(@"[RetroArchTV] host: %@ path: %@", url.host, url.path);
NSString *filename = (NSString*)url.path.lastPathComponent;
NSError *error = nil;
NSString *newDocs = [self outputPathForFile:filename];
if (![man fileExistsAtPath:newDocs]){
NSLog(@"[RetroArchTV] %@ does not exist! attempting to create it", newDocs);
[man createDirectoryAtPath:newDocs withIntermediateDirectories:TRUE attributes:nil error:nil];
}
[man moveItemAtPath:[url path] toPath:[newDocs stringByAppendingPathComponent:filename] error:&error];
if (error) { //this will error out on chimera
NSLog(@"[RetroArchTV] move file error error: %@", [error description]);
printf("%s\n", [[error description] UTF8String]);
[man copyItemAtPath:[url path] toPath:[newDocs stringByAppendingPathComponent:filename] error:&error];
}
return true;
}
This Info.plist will give you the info necessary to see how "all documents" support was added (not recommended to add ALL documents)
NOTE: Obsolete Legacy Instructions
You can also reference the file in this repo: VLC-tvOS-Info.plist to see how I took the Info.plist from VLC for iOS and grabbed the necessary keys and added them to the tvOS version. Just replaced the old Info.plist inside the original with this one, ran uicache and was good to go!
NEW HOTNESS
To add VLC support (to show in the listings of Applications available- more work is needed for handling openURL:) there is a new key added to the preferences file in /var/mobile/Library/Preferences/com.nito.Breezy.plist called appMimicMap which is a dictionary of arrays. The keys of the dictionary are the applications you want to mimic the AirDrop settings of, ie *@{@"com.nito.Ethereal":@[@"org.videolan.vlc-ios"]} are the default values. This means that org.videolan.vlc-ios will mimic the settings of Ethereal. Breezy.xm
Exporting files using AirDrop
AirDropHelper
AirDropHelper is a headerless application that uses a URL scheme (airdropper://) to receive files from any other application / tweak or command line utility on the device and will handle presenting the standard AirDrop sharing UI
Calling from an application (whether original or tweaked)
NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"airdropper://%@?sender=%@", @"/path/to/file", bundleID]];
[[UIApplication sharedApplication] openURL:url];
Calling from a CLI tool or Daemon (anything without a user interface)
#import <objc/runtime.h>
@interface LSApplicationWorkspace: NSObject
- (BOOL)openURL:(id)string;
+ (id)defaultWorkspace;
@end
- (BOOL)sendFileToBreezy:(NSString *)theFile {
//create URL
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"airdropper://%@", theFile]];
//load Mobile Core Services framework
[[NSBundle bundleWithPath:@"/System/Library/Frameworks/MobileCoreServices.framework"] load];
//load URL
[[objc_getClass("LSApplicationWorkspace") defaultWorkspace] openURL:url];
}
Thats it!
As long as you add
com.nito.breezy (>=2.6-78)
to your dependencies, this will open an AirDrop sharing dialog with whatever file you feed it with the call to
[[UIApplication sharedApplication] openURL:url]
This insanely simple application is explained below.
as previously mentioned AirDropHelper is a headless application (full fledged Application with a view controller heirarchy, just no visible icon on the home screen)
This is achieved by adding the following to the Info.plist file:
<key>SBAppTags</key>
<array>
<string>hidden</string>
</array>
I abuse the same URL scheme system that determines where https://mywebsite.com is open in your default browser.
airdropper:// is the custom scheme AirDropHelper listens for. From there its as simple as implementing the standard methods in AppDelegate.m to handle URL's opening and calling a custom method to display the standard UIViewController for sharing via AirDrop from the private Sharing (or SharingUI) framework.
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options
{
NSLog(@"url: %@ app identifier: %@", url.host, url.path.lastPathComponent);
NSString *filePath = [url path];
[self showAirDropSharingView:filePath];
return TRUE;
}
- (void)showAirDropSharingView:(NSString *)filePath {
NSBundle *bundle = [NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/Sharing.framework"];
[bundle load];
UIViewController *rvc = [[[UIApplication sharedApplication] keyWindow] rootViewController];
NSURL *url = [NSURL fileURLWithPath:filePath];
NSLog(@"url: %@", url);
id sharingView = [[objc_getClass("SFAirDropSharingViewControllerTV") alloc] initWithSharingItems:@[url]];
[sharingView setCompletionHandler:^(NSError *error) {
NSLog(@"complete with error: %@", error);
//quit the application when we are done.
[[UIApplication sharedApplication] terminateWithSuccess];
}];
[rvc presentViewController:sharingView animated:true completion:nil];
}
illustrated in AppDelegate.m
The only other missing piece of the puzzle is signing the application with our own special entitlements specifically "com.apple.private.airdrop.discovery"
Provenance support (provscience folder)
There are a few reasons I opted for code injection to add support to Provenance, its written in swift and has tons of dependencies both managed by cocoapods and carthage and is very difficult & time consuming to build, I have opted to add AirDrop support through tweaking the application. The copies that I distribute have the Info.plist files augmented to support all the BIOS and ROM files, and then Tweak.x takes care of the rest, (handling the openURL:... calls)
VLC Support (vlcscience folder)
Due to the fact VLC is an App Store app, we NEED to tweak it to inject support, plus this is such a popular app I didn't mind including this as part of Breezy (it should probably be a separate module) Same thing applies here, using code injection to add openURL: calls and moving the files into the folder where VLC will detect them. The other injection is done in Breezy.xm to avoid needing to modify the Info.plist file to advertise what UTI's are support (covered elsewhere in this README)
Abusing sharingd
On 12+ you need a special entitlement added to your application for it to appear as an airdrop server: com.apple.private.airdrop.settings
How this works
The core functionality of Breezy is mostly achieved through stock features in Sharing[UI].framework (including the sharing UI and toggling AirDrop sharing state)
With vanilla / stock implementation sharingd will throw an exception when AirDropped files are received, halting the process in its tracks.
The file linked below is where the exception is thrown in sharingd, a partial reconstruction of the method that throws the exception.
tl;dr the transfer needs a "handler" to determine what to do with the file once the transfer is complete. if this handler is nil, it throws an exception and the transfer is killed. (this exception is only thrown on versions < 13, just mentioned for posterity, not incr
