Skip to content
Open
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
1 change: 1 addition & 0 deletions Limelight/AppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) NSString *pcUuidToLoad;
@property (strong, nonatomic) NSString *pendingAppIdToLoad;
@property (strong, nonatomic) void (^shortcutCompletionHandler)(BOOL);

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
Expand Down
16 changes: 16 additions & 0 deletions Limelight/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import "AppDelegate.h"
#import "Moonlight-Swift.h"

@implementation AppDelegate

Expand All @@ -29,6 +30,13 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
}
#endif

// Handle cold-start deep link
NSURL *url = launchOptions[UIApplicationLaunchOptionsURLKey];
if (url != nil) {
(void)[DeepLinkManager handleURL:url application:application appDelegate:self];
}

return YES;
}

Expand Down Expand Up @@ -56,6 +64,14 @@ - (void)applicationWillEnterForeground:(UIApplication *)application
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}


- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
{
return [DeepLinkManager handleURL:url application:application appDelegate:self];
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
Expand Down
3 changes: 3 additions & 0 deletions Limelight/Database/DataManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import "DataManager.h"
#import "TemporaryApp.h"
#import "TemporarySettings.h"
#import "Moonlight-Swift.h"

@implementation DataManager {
NSManagedObjectContext *_managedObjectContext;
Expand Down Expand Up @@ -188,6 +189,8 @@ - (void) saveData {
}

[_appDelegate saveContext];

[WidgetSnapshot exportSnapshotFromHosts:[self getHosts]];
}

- (NSArray*) getHosts {
Expand Down
85 changes: 85 additions & 0 deletions Limelight/DeepLink/DeepLinkManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Foundation

enum DeepLinkAction: CustomStringConvertible {
case unknown
case launch(hostUUID: String?, appId: String?)

var description: String {
switch self {
case .unknown:
return "unknown"

case let .launch(hostUUID, appId):
return
"launch(hostUUID: \(hostUUID ?? "nil"), appId: \(appId ?? "nil"))"
}
}

func execute(appDelegate: AppDelegate) {
switch self {
case .unknown:
break

case let .launch(hostUUID, appId):
appDelegate.setValue(hostUUID, forKey: "pcUuidToLoad")
appDelegate.setValue(appId, forKey: "pendingAppIdToLoad")
}
}
}

@objcMembers
final class DeepLinkManager: NSObject {
static func parseURL(_ url: URL?) -> DeepLinkAction? {
guard let url else { return nil }

guard
let components = URLComponents(
url: url,
resolvingAgainstBaseURL: false
)
else {
return nil
}

let action: DeepLinkAction

switch components.host {
case "launch":
// Example: moonlight://launch?host=<uuid>&app=<appId>
let hostUUID = components.queryItems?.first { $0.name == "host" }?
.value
let appId = components.queryItems?.first { $0.name == "app" }?.value
action = .launch(hostUUID: hostUUID, appId: appId)

default:
action = .unknown
}

LogTagSwift(
LOG_I,
"DeepLink",
"URL=\(url.absoluteString), action=\(action)"
)

return action
}

static func handleURL(
_ url: URL?,
application _: UIApplication,
appDelegate: AppDelegate
) -> Bool {
guard let action = parseURL(url) else {
return false
}

switch action {
case .unknown:
return false

default:
action.execute(appDelegate: appDelegate)
return true
}
}
}
7 changes: 7 additions & 0 deletions Limelight/Input/Moonlight-Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#import "AppDelegate.h"
#import "TemporaryHost.h"
#import "TemporaryApp.h"
#import "AppAssetManager.h"
#import "Logger.h"
7 changes: 7 additions & 0 deletions Limelight/Limelight-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<string>moonlight</string>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>GCSupportedGameControllers</key>
Expand Down
1 change: 1 addition & 0 deletions Limelight/Utility/Logger.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ typedef enum {

void Log(LogLevel level, NSString* fmt, ...);
void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...);
void LogTagSwift(LogLevel level, NSString* tag, NSString* message);

#endif
4 changes: 4 additions & 0 deletions Limelight/Utility/Logger.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
}
NSLogv(prefixedString, args);
}

void LogTagSwift(LogLevel level, NSString *tag, NSString *message) {
LogTag(level, tag, message);
}
180 changes: 120 additions & 60 deletions Limelight/ViewControllers/MainFrameViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -754,66 +754,13 @@ - (void)appLongClicked:(TemporaryApp *)app view:(UIView *)view {
if (currentApp != nil) {
[alertController addAction:[UIAlertAction actionWithTitle:
[app.id isEqualToString:currentApp.id] ? @"Quit App" : @"Quit Running App and Start" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
Log(LOG_I, @"Quitting application: %@", currentApp.name);
[self showLoadingFrame: ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host];
HttpResponse* quitResponse = [[HttpResponse alloc] init];
HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];

// Exempt this host from discovery while handling the quit operation
[self->_discMan pauseDiscoveryForHost:app.host];
[hMan executeRequestSynchronously:quitRequest];
if (quitResponse.statusCode == 200) {
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
if (![serverInfoResp isStatusOk] || [[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
// On newer GFE versions, the quit request succeeds even though the app doesn't
// really quit if another client tries to kill your app. We'll patch the response
// to look like the old error in that case, so the UI behaves.
quitResponse.statusCode = 599;
}
else if ([serverInfoResp isStatusOk]) {
// Update the host object with this info
[serverInfoResp populateHost:app.host];
}
}
[self->_discMan resumeDiscoveryForHost:app.host];

// If it fails, display an error and stop the current operation
if (quitResponse.statusCode != 200) {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Quitting App Failed"
message:@"Failed to quit app. If this app was started by "
"another device, you'll need to quit from that device."
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateAppsForHost:app.host];
[self hideLoadingFrame: ^{
[[self activeViewController] presentViewController:alert animated:YES completion:nil];
}];
});
}
else {
app.host.currentGame = @"0";
dispatch_async(dispatch_get_main_queue(), ^{
// If it succeeds and we're to start streaming, segue to the stream
if (![app.id isEqualToString:currentApp.id]) {
[self prepareToStreamApp:app];
[self hideLoadingFrame: ^{
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}];
}
else {
// Otherwise, just hide the loading icon
[self hideLoadingFrame:nil];
}
});
}
});
}];

if ([app.id isEqualToString:currentApp.id]) {
Log(LOG_I, @"Quitting application: %@", currentApp.name);
[self quitRunningAppOnHost:app completion:nil];
}
else {
[self quitRunningApp:currentApp andLaunchApp:app];
}
}]];
}

Expand All @@ -838,6 +785,76 @@ - (void)appLongClicked:(TemporaryApp *)app view:(UIView *)view {
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
}

- (void)quitRunningAppOnHost:(TemporaryApp*)app completion:(void (^)(BOOL success))completion
{
[self showLoadingFrame: ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host];
HttpResponse* quitResponse = [[HttpResponse alloc] init];
HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];

// Exempt this host from discovery while handling the quit operation
[self->_discMan pauseDiscoveryForHost:app.host];
[hMan executeRequestSynchronously:quitRequest];
if (quitResponse.statusCode == 200) {
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
if (![serverInfoResp isStatusOk] || [[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
// On newer GFE versions, the quit request succeeds even though the app doesn't
// really quit if another client tries to kill your app. We'll patch the response
// to look like the old error in that case, so the UI behaves.
quitResponse.statusCode = 599;
}
else if ([serverInfoResp isStatusOk]) {
// Update the host object with this info
[serverInfoResp populateHost:app.host];
}
}
[self->_discMan resumeDiscoveryForHost:app.host];

// If it fails, display an error and stop the current operation
if (quitResponse.statusCode != 200) {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Quitting App Failed"
message:@"Failed to quit app. If this app was started by "
"another device, you'll need to quit from that device."
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateAppsForHost:app.host];
[self hideLoadingFrame: ^{
[[self activeViewController] presentViewController:alert animated:YES completion:nil];
if (completion != nil) {
completion(NO);
}
}];
});
}
else {
app.host.currentGame = @"0";
dispatch_async(dispatch_get_main_queue(), ^{
[self hideLoadingFrame:^{
if (completion != nil) {
completion(YES);
}
}];
});
}
});
}];
}

- (void)quitRunningApp:(TemporaryApp*)currentApp andLaunchApp:(TemporaryApp*)targetApp
{
Log(LOG_I, @"Quitting application: %@ (from host %@) to launch %@ (from host %@)", currentApp.name, currentApp.host.name, targetApp.name, targetApp.host.name);
[self quitRunningAppOnHost:currentApp completion:^(BOOL success) {
if (success) {
[self prepareToStreamApp:targetApp];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}
}];
}

- (void) appClicked:(TemporaryApp *)app view:(UIView *)view {
Log(LOG_D, @"Clicked app: %@", app.name);

Expand Down Expand Up @@ -1075,6 +1092,48 @@ -(void)handlePendingShortcutAction
}
}

- (void)handlePendingAppShortcutAction
{
AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;


if (delegate.pendingAppIdToLoad == nil || _selectedHost == nil || _sortedAppList == nil) {

return;
}

TemporaryApp *target = nil;
for (TemporaryApp *app in _sortedAppList) {
if ([app.id isEqualToString:delegate.pendingAppIdToLoad]) {
target = app;
break;
}
}

delegate.pendingAppIdToLoad = nil;

if (target == nil) {
Log(LOG_W, @"App id %@ not found in current list", delegate.pendingAppIdToLoad);
return;
}

TemporaryApp* currentApp = [self findRunningApp:target.host];

if (currentApp == nil) {
Log(LOG_I, @"Launching app %@ on host %@", target.name, target.host.name);
[self prepareToStreamApp:target];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}
else if ([currentApp.id isEqualToString:target.id]) {
Log(LOG_I, @"Resuming app %@ on host %@", currentApp.name, currentApp.host.name);
[self prepareToStreamApp:currentApp];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}
else {
[self quitRunningApp:currentApp andLaunchApp:target];
}
}

-(void)handleReturnToForeground
{
_background = NO;
Expand Down Expand Up @@ -1341,6 +1400,7 @@ - (void) updateAppsForHost:(TemporaryHost*)host {

[hostScrollView removeFromSuperview];
[self.collectionView reloadData];
[self handlePendingAppShortcutAction];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
Expand Down
Loading