2014-08-31 13:13:46 -04:00
// MainFrameViewController . m
2015-05-12 21:48:30 -04:00
// Moonlight
2014-08-31 13:13:46 -04:00
//
// Created by Diego Waxemberg on 1 / 17 / 14.
2015-05-12 21:48:30 -04:00
// Copyright ( c ) 2014 Moonlight Stream . All rights reserved .
2014-08-31 13:13:46 -04:00
//
2015-11-10 21:23:00 -08:00
@ import ImageIO ;
2014-08-31 13:13:46 -04:00
# import "MainFrameViewController.h"
2014-10-16 21:20:27 -04:00
# import "CryptoManager.h"
2014-10-18 21:13:48 -04:00
# import "HttpManager.h"
2014-10-20 11:31:40 -04:00
# import "Connection.h"
2014-10-20 13:35:42 -04:00
# import "StreamManager.h"
2014-10-20 20:54:03 -04:00
# import "Utils.h"
2014-10-24 16:02:10 -04:00
# import "UIComputerView.h"
# import "UIAppView.h"
2014-10-29 00:47:51 -04:00
# import "DataManager.h"
2015-12-02 00:25:38 -08:00
# import "TemporarySettings.h"
2015-01-02 19:15:47 -05:00
# import "WakeOnLanManager.h"
2015-02-01 04:26:47 -05:00
# import "AppListResponse.h"
# import "ServerInfoResponse.h"
2015-02-01 19:46:28 -05:00
# import "StreamFrameViewController.h"
2015-02-24 22:13:33 -05:00
# import "LoadingFrameViewController.h"
2015-09-30 19:13:37 -07:00
# import "ComputerScrollView.h"
2015-09-30 20:34:52 -07:00
# import "TemporaryApp.h"
2015-10-31 23:03:21 -07:00
# import "IdManager.h"
2018-03-27 08:50:40 +02:00
# import "ConnectionHelper.h"
2014-08-31 13:13:46 -04:00
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
# import "SettingsViewController.h"
2019-10-12 18:07:47 -07:00
# else
# import < sys / utsname . h >
2018-08-26 11:56:27 -07:00
# endif
2018-06-02 13:25:53 -07:00
# import < VideoToolbox / VideoToolbox . h >
2014-10-20 11:31:40 -04:00
@ implementation MainFrameViewController {
NSOperationQueue * _opQueue ;
2015-12-01 20:31:24 -08:00
TemporaryHost * _selectedHost ;
2014-10-26 02:15:53 -04:00
NSString * _uniqueId ;
2018-12-22 00:05:48 -08:00
NSData * _clientCert ;
2015-01-01 22:30:03 -05:00
DiscoveryManager * _discMan ;
2015-01-31 20:10:14 -05:00
AppAssetManager * _appManager ;
2015-02-01 19:46:28 -05:00
StreamConfiguration * _streamConfig ;
2015-06-23 22:23:12 -07:00
UIAlertController * _pairAlert ;
2018-08-26 11:56:27 -07:00
LoadingFrameViewController * _loadingFrame ;
2014-10-26 02:15:53 -04:00
UIScrollView * hostScrollView ;
2019-09-11 19:07:53 -07:00
FrontViewPosition currentPosition ;
2015-07-11 01:28:52 -07:00
NSArray * _sortedAppList ;
2015-11-10 21:23:00 -08:00
NSCache * _boxArtCache ;
2018-05-31 00:46:41 -07:00
bool _background ;
2018-08-26 15:05:32 -07:00
# if TARGET_OS _TV
UITapGestureRecognizer * _menuRecognizer ;
2018-08-26 11:56:27 -07:00
# endif
2014-10-20 11:31:40 -04:00
}
2014-10-26 02:15:53 -04:00
static NSMutableSet * hostList ;
2014-08-31 13:13:46 -04:00
2014-10-20 13:35:42 -04:00
- ( void ) showPIN : ( NSString * ) PIN {
2015-07-09 21:02:19 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-04-21 22:20:27 -07:00
self -> _pairAlert = [ UIAlertController alertControllerWithTitle : @ "Pairing"
2019-07-05 22:18:30 -07:00
message : [ NSString stringWithFormat : @ "Enter the following PIN on the host machine: %@" , PIN ]
2018-08-26 11:56:27 -07:00
preferredStyle : UIAlertControllerStyleAlert ] ;
2018-04-21 22:20:27 -07:00
[ self -> _pairAlert addAction : [ UIAlertAction actionWithTitle : @ "Cancel" style : UIAlertActionStyleDestructive handler : ^ ( UIAlertAction * action ) {
self -> _pairAlert = nil ;
[ self -> _discMan startDiscovery ] ;
2019-11-03 15:11:02 -08:00
[ self hideLoadingFrame : ^ {
[ self showHostSelectionView ] ;
} ] ;
2016-10-21 12:07:29 -07:00
} ] ] ;
2018-08-26 11:56:27 -07:00
[ [ self activeViewController ] presentViewController : self -> _pairAlert animated : YES completion : nil ] ;
2014-10-20 13:35:42 -04:00
} ) ;
}
2019-11-03 15:11:02 -08:00
- ( void ) displayPairingFailureDialog : ( NSString * ) message {
2017-05-07 16:51:57 -07:00
UIAlertController * failedDialog = [ UIAlertController alertControllerWithTitle : @ "Pairing Failed"
2018-08-26 11:56:27 -07:00
message : message
preferredStyle : UIAlertControllerStyleAlert ] ;
2018-08-29 09:36:51 -07:00
[ Utils addHelpOptionToDialog : failedDialog ] ;
2018-05-28 18:20:59 -07:00
[ failedDialog addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-08-26 12:25:22 -07:00
2019-11-03 15:11:02 -08:00
[ _discMan startDiscovery ] ;
2018-08-26 12:25:22 -07:00
[ self hideLoadingFrame : ^ {
2019-11-03 15:11:02 -08:00
[ self showHostSelectionView ] ;
2018-08-26 12:25:22 -07:00
[ [ self activeViewController ] presentViewController : failedDialog animated : YES completion : nil ] ;
} ] ;
2017-05-07 16:51:57 -07:00
}
2014-10-20 13:35:42 -04:00
- ( void ) pairFailed : ( NSString * ) message {
2015-07-09 21:02:19 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-04-21 22:20:27 -07:00
if ( self -> _pairAlert ! = nil ) {
[ self -> _pairAlert dismissViewControllerAnimated : YES completion : ^ {
2019-11-03 15:11:02 -08:00
[ self displayPairingFailureDialog : message ] ;
2017-05-07 16:51:57 -07:00
} ] ;
2018-04-21 22:20:27 -07:00
self -> _pairAlert = nil ;
2017-05-07 16:51:57 -07:00
}
2014-10-20 13:35:42 -04:00
} ) ;
}
2018-12-22 00:05:48 -08:00
- ( void ) pairSuccessful : ( NSData * ) serverCert {
2015-07-09 21:02:19 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-12-22 00:05:48 -08:00
// Store the cert from pairing with the host
self -> _selectedHost . serverCert = serverCert ;
2018-04-21 22:20:27 -07:00
[ self -> _pairAlert dismissViewControllerAnimated : YES completion : nil ] ;
self -> _pairAlert = nil ;
2018-12-22 00:05:48 -08:00
2018-04-21 22:20:27 -07:00
[ self -> _discMan startDiscovery ] ;
2015-07-09 20:39:15 -07:00
[ self alreadyPaired ] ;
2014-10-20 13:35:42 -04:00
} ) ;
}
2019-09-14 12:46:50 -07:00
- ( void ) disableUpButton {
# if ! TARGET_OS _TV
[ self -> _upButton setTitle : nil ] ;
# endif
}
- ( void ) enableUpButton {
# if ! TARGET_OS _TV
[ self -> _upButton setTitle : @ "Select New Host" ] ;
# endif
}
2014-10-26 02:15:53 -04:00
- ( void ) alreadyPaired {
2015-07-10 21:22:57 -07:00
BOOL usingCachedAppList = false ;
2015-07-11 23:59:09 -07:00
// Capture the host here because it can change once we
// leave the main thread
2015-12-01 20:31:24 -08:00
TemporaryHost * host = _selectedHost ;
2015-07-11 23:59:09 -07:00
if ( [ host . appList count ] > 0 ) {
2015-07-10 21:22:57 -07:00
usingCachedAppList = true ;
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-04-21 22:20:27 -07:00
if ( host ! = self -> _selectedHost ) {
2018-11-16 20:17:03 -08:00
[ self hideLoadingFrame : nil ] ;
2015-07-11 23:59:09 -07:00
return ;
}
2018-08-26 17:05:13 -07:00
self . title = host . name ;
2019-08-30 21:15:11 -07:00
2019-09-14 12:46:50 -07:00
[ self enableUpButton ] ;
2015-07-11 23:59:09 -07:00
[ self updateAppsForHost : host ] ;
2018-08-26 12:25:22 -07:00
[ self hideLoadingFrame : nil ] ;
2015-07-10 21:22:57 -07:00
} ) ;
}
Log ( LOG_I , @ "Using cached app list: %d" , usingCachedAppList ) ;
2014-10-26 02:15:53 -04:00
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
2015-07-12 00:23:48 -07:00
// Exempt this host from discovery while handling the applist query
2018-04-21 22:20:27 -07:00
[ self -> _discMan removeHostFromDiscovery : host ] ;
2015-11-11 18:14:59 -08:00
2018-12-22 00:05:48 -08:00
AppListResponse * appListResp = [ ConnectionHelper getAppListForHostWithHostIP : host . activeAddress serverCert : host . serverCert uniqueID : self -> _uniqueId ] ;
2015-11-11 18:14:59 -08:00
2018-04-21 22:20:27 -07:00
[ self -> _discMan addHostToDiscovery : host ] ;
2015-07-12 00:23:48 -07:00
2019-10-12 16:41:32 -07:00
if ( ! [ appListResp isStatusOk ] || [ appListResp getAppList ] = = nil ) {
2015-02-10 21:26:55 -05:00
Log ( LOG_W , @ "Failed to get applist: %@" , appListResp . statusMessage ) ;
2015-07-09 21:15:52 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-04-21 22:20:27 -07:00
if ( host ! = self -> _selectedHost ) {
2018-11-16 20:17:03 -08:00
[ self hideLoadingFrame : nil ] ;
2015-07-12 00:30:44 -07:00
return ;
}
2019-10-12 16:41:32 -07:00
UIAlertController * applistAlert = [ UIAlertController alertControllerWithTitle : @ "Connection Interrupted"
message : appListResp . statusMessage
2015-07-09 21:15:52 -07:00
preferredStyle : UIAlertControllerStyleAlert ] ;
2018-08-29 09:36:51 -07:00
[ Utils addHelpOptionToDialog : applistAlert ] ;
2018-05-28 18:20:59 -07:00
[ applistAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-08-26 12:25:22 -07:00
[ self hideLoadingFrame : ^ {
2019-11-03 15:11:02 -08:00
[ self showHostSelectionView ] ;
2018-08-26 12:25:22 -07:00
[ [ self activeViewController ] presentViewController : applistAlert animated : YES completion : nil ] ;
} ] ;
2019-08-30 19:19:23 -07:00
host . state = StateOffline ;
2015-07-09 21:15:52 -07:00
} ) ;
2015-01-31 01:57:23 -05:00
} else {
2015-07-09 21:15:52 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2015-12-01 20:56:08 -08:00
[ self updateApplist : [ appListResp getAppList ] forHost : host ] ;
2015-07-11 23:59:09 -07:00
2018-04-21 22:20:27 -07:00
if ( host ! = self -> _selectedHost ) {
2018-11-16 20:17:03 -08:00
[ self hideLoadingFrame : nil ] ;
2015-07-11 23:59:09 -07:00
return ;
}
2018-08-26 17:05:13 -07:00
self . title = host . name ;
2019-09-14 12:46:50 -07:00
[ self enableUpButton ] ;
2015-01-31 01:57:23 -05:00
2015-07-11 23:59:09 -07:00
[ self updateAppsForHost : host ] ;
2018-04-21 22:20:27 -07:00
[ self -> _appManager stopRetrieving ] ;
[ self -> _appManager retrieveAssetsFromHost : host ] ;
2018-08-26 12:25:22 -07:00
[ self hideLoadingFrame : nil ] ;
2015-07-09 21:15:52 -07:00
} ) ;
2015-01-31 01:57:23 -05:00
}
2014-10-26 02:15:53 -04:00
} ) ;
}
2015-12-01 20:56:08 -08:00
- ( void ) updateApplist : ( NSSet * ) newList forHost : ( TemporaryHost * ) host {
2015-07-11 18:47:05 -07:00
DataManager * database = [ [ DataManager alloc ] init ] ;
2015-07-10 22:19:25 -07:00
2016-01-21 14:03:07 -05:00
for ( TemporaryApp * app in newList ) {
BOOL appAlreadyInList = NO ;
for ( TemporaryApp * savedApp in host . appList ) {
if ( [ app . id isEqualToString : savedApp . id ] ) {
savedApp . name = app . name ;
2018-06-02 12:32:12 -07:00
savedApp . hdrSupported = app . hdrSupported ;
2016-01-21 14:03:07 -05:00
appAlreadyInList = YES ;
break ;
}
}
if ( ! appAlreadyInList ) {
app . host = host ;
[ host . appList addObject : app ] ;
}
}
BOOL appWasRemoved ;
do {
appWasRemoved = NO ;
for ( TemporaryApp * app in host . appList ) {
appWasRemoved = YES ;
for ( TemporaryApp * mergedApp in newList ) {
if ( [ mergedApp . id isEqualToString : app . id ] ) {
appWasRemoved = NO ;
break ;
}
}
if ( appWasRemoved ) {
// Removing the app mutates the list we ' re iterating ( which isn ' t legal ) .
// We need to jump out of this loop and restart enumeration .
[ host . appList removeObject : app ] ;
2016-01-21 16:24:02 -05:00
// It ' s important to remove the app record from the database
// since we ' ll have a constraint violation now that appList
// doesn ' t have this app in it .
[ database removeApp : app ] ;
2016-01-21 14:03:07 -05:00
break ;
}
}
// Keep looping until the list is no longer being mutated
} while ( appWasRemoved ) ;
2015-09-30 20:44:14 -07:00
2016-01-21 14:03:07 -05:00
[ database updateAppsForExistingHost : host ] ;
2019-11-03 13:38:52 -08:00
// This host may be eligible for a shortcut now that the app list
// has been populated
[ self updateHostShortcuts ] ;
2015-07-10 21:22:57 -07:00
}
2014-12-04 19:54:21 +09:00
- ( void ) showHostSelectionView {
2018-08-26 15:05:32 -07:00
# if TARGET_OS _TV
// Remove the menu button intercept to allow the app to exit
// when at the host selection view .
2019-09-24 19:08:22 -07:00
[ self . navigationController . view removeGestureRecognizer : _menuRecognizer ] ;
2018-08-26 15:05:32 -07:00
# endif
2015-02-10 18:40:22 -05:00
[ _appManager stopRetrieving ] ;
2015-07-11 01:28:52 -07:00
_selectedHost = nil ;
2019-11-03 14:40:32 -08:00
_sortedAppList = nil ;
2019-08-30 21:15:11 -07:00
2018-08-26 19:00:16 -07:00
self . title = @ "Select Host" ;
2019-09-14 12:46:50 -07:00
[ self disableUpButton ] ;
2019-08-30 21:15:11 -07:00
2014-12-04 19:54:21 +09:00
[ self . collectionView reloadData ] ;
[ self . view addSubview : hostScrollView ] ;
}
2015-12-01 20:31:24 -08:00
- ( void ) receivedAssetForApp : ( TemporaryApp * ) app {
2015-11-10 21:23:00 -08:00
// Update the box art cache now so we don ' t have to do it
// on the main thread
[ self updateBoxArtCacheForApp : app ] ;
2015-07-11 23:59:09 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self . collectionView reloadData ] ;
} ) ;
2014-10-26 02:15:53 -04:00
}
2014-10-21 14:31:03 -04:00
- ( void ) displayDnsFailedDialog {
UIAlertController * alert = [ UIAlertController alertControllerWithTitle : @ "Network Error"
message : @ "Failed to resolve host."
preferredStyle : UIAlertControllerStyleAlert ] ;
2018-08-29 09:36:51 -07:00
[ Utils addHelpOptionToDialog : alert ] ;
2018-05-28 18:20:59 -07:00
[ alert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-08-26 11:56:27 -07:00
[ [ self activeViewController ] presentViewController : alert animated : YES completion : nil ] ;
2014-10-21 14:31:03 -04:00
}
2015-12-01 20:31:24 -08:00
- ( void ) hostClicked : ( TemporaryHost * ) host view : ( UIView * ) view {
2015-07-09 21:15:52 -07:00
// Treat clicks on offline hosts to be long clicks
// This shows the context menu with wake , delete , etc . rather
// than just hanging for a while and failing as we would in this
// code path .
2019-08-30 19:19:23 -07:00
if ( host . state ! = StateOnline && view ! = nil ) {
2015-07-09 21:15:52 -07:00
[ self hostLongClicked : host view : view ] ;
return ;
}
2015-02-10 21:26:55 -05:00
Log ( LOG_D , @ "Clicked host: %@" , host . name ) ;
2015-01-01 22:30:03 -05:00
_selectedHost = host ;
2015-07-11 23:59:09 -07:00
[ self disableNavigation ] ;
2015-07-11 21:59:41 -07:00
2018-08-26 15:05:32 -07:00
# if TARGET_OS _TV
// Intercept the menu key to go back to the host page
2019-09-24 19:08:22 -07:00
[ self . navigationController . view addGestureRecognizer : _menuRecognizer ] ;
2018-08-26 15:05:32 -07:00
# endif
2015-07-11 21:59:41 -07:00
// If we are online , paired , and have a cached app list , skip straight
// to the app grid without a loading frame . This is the fast path that users
2017-05-07 23:32:31 -07:00
// should hit most . Check for a valid view because we don ' t want to hit the fast
// path after coming back from streaming , since we need to fetch serverinfo too
// so that our active game data is correct .
2019-08-30 19:19:23 -07:00
if ( host . state = = StateOnline && host . pairState = = PairStatePaired && host . appList . count > 0 && view ! = nil ) {
2015-07-11 23:59:09 -07:00
[ self alreadyPaired ] ;
2015-07-11 21:59:41 -07:00
return ;
}
2018-11-15 22:02:05 -08:00
[ self showLoadingFrame : ^ {
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
2018-12-22 00:05:48 -08:00
HttpManager * hMan = [ [ HttpManager alloc ] initWithHost : host . activeAddress uniqueId : self -> _uniqueId serverCert : host . serverCert ] ;
2018-11-15 22:02:05 -08:00
ServerInfoResponse * serverInfoResp = [ [ ServerInfoResponse alloc ] init ] ;
// Exempt this host from discovery while handling the serverinfo request
[ self -> _discMan removeHostFromDiscovery : host ] ;
2018-11-16 19:02:31 -08:00
[ hMan executeRequestSynchronously : [ HttpRequest requestForResponse : serverInfoResp withUrlRequest : [ hMan newServerInfoRequest : false ]
2018-11-15 22:02:05 -08:00
fallbackError : 401 fallbackRequest : [ hMan newHttpServerInfoRequest ] ] ] ;
[ self -> _discMan addHostToDiscovery : host ] ;
2019-10-12 16:41:32 -07:00
if ( ! [ serverInfoResp isStatusOk ] ) {
2018-11-15 22:02:05 -08:00
Log ( LOG_W , @ "Failed to get server info: %@" , serverInfoResp . statusMessage ) ;
2018-05-31 01:53:09 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-11-15 22:02:05 -08:00
if ( host ! = self -> _selectedHost ) {
[ self hideLoadingFrame : nil ] ;
return ;
}
2019-10-12 16:41:32 -07:00
UIAlertController * applistAlert = [ UIAlertController alertControllerWithTitle : @ "Connection Failed"
message : serverInfoResp . statusMessage
2018-11-15 22:02:05 -08:00
preferredStyle : UIAlertControllerStyleAlert ] ;
[ Utils addHelpOptionToDialog : applistAlert ] ;
[ applistAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
// Only display an alert if this was the result of a real
// user action , not just passively entering the foreground again
[ self hideLoadingFrame : ^ {
2019-11-03 15:11:02 -08:00
[ self showHostSelectionView ] ;
2018-11-15 22:02:05 -08:00
if ( view ! = nil ) {
[ [ self activeViewController ] presentViewController : applistAlert animated : YES completion : nil ] ;
}
2018-08-26 12:25:22 -07:00
} ] ;
2018-11-15 22:02:05 -08:00
2019-08-30 19:19:23 -07:00
host . state = StateOffline ;
2018-05-31 01:53:09 -07:00
} ) ;
2018-11-15 22:02:05 -08:00
} else {
2018-12-28 00:32:19 -08:00
// Update the host object with this data
[ serverInfoResp populateHost : host ] ;
if ( host . pairState = = PairStatePaired ) {
2018-11-15 22:02:05 -08:00
Log ( LOG_I , @ "Already Paired" ) ;
[ self alreadyPaired ] ;
}
// Only pair when this was the result of explicit user action
else if ( view ! = nil ) {
Log ( LOG_I , @ "Trying to pair" ) ;
// Polling the server while pairing causes the server to screw up
[ self -> _discMan stopDiscoveryBlocking ] ;
2018-12-22 00:05:48 -08:00
PairManager * pMan = [ [ PairManager alloc ] initWithManager : hMan clientCert : self -> _clientCert callback : self ] ;
2018-11-15 22:02:05 -08:00
[ self -> _opQueue addOperation : pMan ] ;
}
else {
// Not user action , so just return to host screen
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self hideLoadingFrame : ^ {
[ self showHostSelectionView ] ;
} ] ;
} ) ;
}
2018-05-31 01:53:09 -07:00
}
2018-11-15 22:02:05 -08:00
} ) ;
} ] ;
2014-10-26 02:15:53 -04:00
}
2018-08-26 11:56:27 -07:00
- ( UIViewController * ) activeViewController {
UIViewController * topController = [ UIApplication sharedApplication ] . keyWindow . rootViewController ;
while ( topController . presentedViewController ) {
topController = topController . presentedViewController ;
}
return topController ;
}
2015-12-01 20:31:24 -08:00
- ( void ) hostLongClicked : ( TemporaryHost * ) host view : ( UIView * ) view {
2015-02-10 21:26:55 -05:00
Log ( LOG_D , @ "Long clicked host: %@" , host . name ) ;
2019-08-30 19:19:23 -07:00
NSString * message ;
switch ( host . state ) {
case StateOffline :
message = @ "Offline" ;
break ;
case StateOnline :
if ( host . pairState = = PairStatePaired ) {
message = @ "Online - Paired" ;
}
else {
message = @ "Online - Not Paired" ;
}
break ;
case StateUnknown :
message = @ "Connecting" ;
break ;
default :
break ;
}
UIAlertController * longClickAlert = [ UIAlertController alertControllerWithTitle : host . name message : message preferredStyle : UIAlertControllerStyleActionSheet ] ;
if ( host . state ! = StateOnline ) {
2015-01-02 19:15:47 -05:00
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "Wake" style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
2019-10-12 16:03:35 -07:00
UIAlertController * wolAlert = [ UIAlertController alertControllerWithTitle : @ "Wake-On-LAN" message : @ "" preferredStyle : UIAlertControllerStyleAlert ] ;
2015-01-02 19:15:47 -05:00
[ wolAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-11-16 19:30:05 -08:00
if ( host . mac = = nil || [ host . mac isEqualToString : @ "00:00:00:00:00:00" ] ) {
2015-01-02 19:15:47 -05:00
wolAlert . message = @ "Host MAC unknown, unable to send WOL Packet" ;
} else {
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
[ WakeOnLanManager wakeHost : host ] ;
} ) ;
2019-10-12 16:03:35 -07:00
wolAlert . message = @ "Successfully sent wake-up request. It may take a few moments for the PC to wake. If it never wakes up, ensure it's properly configured for Wake-on-LAN." ;
2015-01-02 19:15:47 -05:00
}
2018-08-26 11:56:27 -07:00
[ [ self activeViewController ] presentViewController : wolAlert animated : YES completion : nil ] ;
2015-01-02 19:15:47 -05:00
} ] ] ;
2018-05-28 18:00:19 -07:00
2018-08-29 09:36:51 -07:00
# if ! TARGET_OS _TV
2018-05-28 18:00:19 -07:00
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "Connection Help" style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
[ [ UIApplication sharedApplication ] openURL : [ NSURL URLWithString : @ "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting" ] ] ;
} ] ] ;
2018-08-29 09:36:51 -07:00
# endif
2015-01-02 19:15:47 -05:00
}
2015-01-01 23:25:09 -05:00
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "Remove Host" style : UIAlertActionStyleDestructive handler : ^ ( UIAlertAction * action ) {
2018-04-21 22:20:27 -07:00
[ self -> _discMan removeHostFromDiscovery : host ] ;
2015-01-01 23:40:52 -05:00
DataManager * dataMan = [ [ DataManager alloc ] init ] ;
[ dataMan removeHost : host ] ;
2015-01-08 19:37:51 -05:00
@ synchronized ( hostList ) {
[ hostList removeObject : host ] ;
2015-12-01 20:31:24 -08:00
[ self updateAllHosts : [ hostList allObjects ] ] ;
2015-01-08 19:37:51 -05:00
}
2015-01-01 23:25:09 -05:00
} ] ] ;
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "Cancel" style : UIAlertActionStyleCancel handler : nil ] ] ;
2015-01-08 19:37:51 -05:00
// these two lines are required for iPad support of UIAlertSheet
longClickAlert . popoverPresentationController . sourceView = view ;
2015-01-31 01:57:23 -05:00
2015-01-08 19:37:51 -05:00
longClickAlert . popoverPresentationController . sourceRect = CGRectMake ( view . bounds . size . width / 2.0 , view . bounds . size . height / 2.0 , 1.0 , 1.0 ) ; // center of the view
2018-08-26 11:56:27 -07:00
[ [ self activeViewController ] presentViewController : longClickAlert animated : YES completion : ^ {
2015-01-01 23:25:09 -05:00
[ self updateHosts ] ;
} ] ;
}
2014-10-26 02:15:53 -04:00
- ( void ) addHostClicked {
2015-02-10 21:26:55 -05:00
Log ( LOG_D , @ "Clicked add host" ) ;
2018-11-28 18:43:45 -08:00
UIAlertController * alertController = [ UIAlertController alertControllerWithTitle : @ "Host Address" message : @ "Please enter a hostname or IP address" preferredStyle : UIAlertControllerStyleAlert ] ;
[ alertController addAction : [ UIAlertAction actionWithTitle : @ "Cancel" style : UIAlertActionStyleCancel handler : nil ] ] ;
[ alertController addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
NSString * hostAddress = ( ( UITextField * ) [ [ alertController textFields ] objectAtIndex : 0 ] ) . text ;
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _HIGH , 0 ) , ^ {
[ self -> _discMan discoverHost : hostAddress withCallback : ^ ( TemporaryHost * host , NSString * error ) {
if ( host ! = nil ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
@ synchronized ( hostList ) {
[ hostList addObject : host ] ;
}
[ self updateHosts ] ;
} ) ;
} else {
UIAlertController * hostNotFoundAlert = [ UIAlertController alertControllerWithTitle : @ "Add Host" message : error preferredStyle : UIAlertControllerStyleAlert ] ;
[ Utils addHelpOptionToDialog : hostNotFoundAlert ] ;
[ hostNotFoundAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ [ self activeViewController ] presentViewController : hostNotFoundAlert animated : YES completion : nil ] ;
} ) ;
}
} ] ; } ) ;
} ] ] ;
[ alertController addTextFieldWithConfigurationHandler : nil ] ;
[ [ self activeViewController ] presentViewController : alertController animated : YES completion : nil ] ;
2014-10-26 02:15:53 -04:00
}
2018-06-02 16:38:02 -07:00
- ( void ) prepareToStreamApp : ( TemporaryApp * ) app {
2015-02-01 19:46:28 -05:00
_streamConfig = [ [ StreamConfiguration alloc ] init ] ;
2015-07-11 23:59:09 -07:00
_streamConfig . host = app . host . activeAddress ;
2015-07-10 21:22:57 -07:00
_streamConfig . appID = app . id ;
2018-06-01 19:11:10 -07:00
_streamConfig . appName = app . name ;
2018-12-22 00:05:48 -08:00
_streamConfig . serverCert = app . host . serverCert ;
2014-10-20 20:54:03 -04:00
2014-10-29 00:47:51 -04:00
DataManager * dataMan = [ [ DataManager alloc ] init ] ;
2015-12-02 00:25:38 -08:00
TemporarySettings * streamSettings = [ dataMan getSettings ] ;
2014-10-29 00:47:51 -04:00
2015-02-01 19:46:28 -05:00
_streamConfig . frameRate = [ streamSettings . framerate intValue ] ;
2019-10-12 18:07:47 -07:00
if ( @ available ( iOS 10.3 , * ) ) {
// Don ' t stream more FPS than the display can show
if ( _streamConfig . frameRate > [ UIScreen mainScreen ] . maximumFramesPerSecond ) {
_streamConfig . frameRate = ( int ) [ UIScreen mainScreen ] . maximumFramesPerSecond ;
Log ( LOG_W , @ "Clamping FPS to maximum refresh rate: %d" , _streamConfig . frameRate ) ;
}
}
2015-02-01 19:46:28 -05:00
_streamConfig . height = [ streamSettings . height intValue ] ;
_streamConfig . width = [ streamSettings . width intValue ] ;
2019-10-12 19:20:26 -07:00
# if TARGET_OS _TV
2019-10-12 18:07:47 -07:00
// Don ' t allow streaming 4 K on the Apple TV HD
struct utsname systemInfo ;
uname ( & systemInfo ) ;
if ( strcmp ( systemInfo . machine , "AppleTV5,3" ) = = 0 && _streamConfig . height >= 2160 ) {
Log ( LOG_W , @ "4K streaming not supported on Apple TV HD" ) ;
_streamConfig . width = 1920 ;
_streamConfig . height = 1080 ;
}
# endif
_streamConfig . bitRate = [ streamSettings . bitrate intValue ] ;
2018-06-02 17:07:38 -07:00
_streamConfig . streamingRemotely = streamSettings . streamingRemotely ;
_streamConfig . optimizeGameSettings = streamSettings . optimizeGames ;
_streamConfig . playAudioOnPC = streamSettings . playAudioOnPC ;
2018-06-02 18:13:32 -07:00
_streamConfig . allowHevc = streamSettings . useHevc ;
2018-06-02 13:25:53 -07:00
2018-06-02 13:52:58 -07:00
// multiController must be set before calling getConnectedGamepadMask
2018-06-02 17:07:38 -07:00
_streamConfig . multiController = streamSettings . multiController ;
2018-06-02 13:52:58 -07:00
_streamConfig . gamepadMask = [ ControllerSupport getConnectedGamepadMask : _streamConfig ] ;
2019-08-26 22:46:35 -07:00
// Probe for supported channel configurations
2019-08-29 21:07:10 -07:00
Log ( LOG_I , @ "Audio device supports %d channels" , [ AVAudioSession sharedInstance ] . maximumOutputNumberOfChannels ) ;
2019-08-26 22:46:35 -07:00
if ( [ AVAudioSession sharedInstance ] . maximumOutputNumberOfChannels >= 6 ) {
_streamConfig . audioChannelCount = 6 ;
_streamConfig . audioChannelMask = 0 xFC ;
}
else {
_streamConfig . audioChannelCount = 2 ;
_streamConfig . audioChannelMask = 0 x3 ;
}
2018-06-02 13:25:53 -07:00
// HDR requires HDR10 game , HDR10 display , and HEVC Main10 decoder on the client .
// It additionally requires an HEVC Main10 encoder on the server ( GTX 1000 + ) .
//
2018-06-02 17:07:38 -07:00
// It should also be a user preference , since some games may require higher peak
// brightness than the iOS device can support to look correct in HDR mode .
2018-06-02 13:25:53 -07:00
if ( @ available ( iOS 11.3 , * ) ) {
_streamConfig . enableHdr =
app . hdrSupported && // App supported
( app . host . serverCodecModeSupport & 0 x200 ) ! = 0 && // HEVC Main10 encoding on host PC GPU
VTIsHardwareDecodeSupported ( kCMVideoCodecType_HEVC ) && // Decoder supported
( AVPlayer . availableHDRModes & AVPlayerHDRModeHDR10 ) ! = 0 && // Display supported
2018-06-02 17:07:38 -07:00
streamSettings . enableHdr ; // User wants it enabled
2018-06-02 13:25:53 -07:00
}
2018-06-02 16:38:02 -07:00
}
- ( void ) appClicked : ( TemporaryApp * ) app {
Log ( LOG_D , @ "Clicked app: %@" , app . name ) ;
2014-10-29 00:47:51 -04:00
2015-05-28 23:47:30 -07:00
[ _appManager stopRetrieving ] ;
2014-11-27 11:36:28 -05:00
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2014-11-27 11:36:28 -05:00
if ( currentPosition ! = FrontViewPositionLeft ) {
2018-10-27 11:57:29 -07:00
// This must not be animated because we need the position
// to change ( and notify our callback to save settings data )
// before we call prepareToStreamApp .
[ [ self revealViewController ] revealToggleAnimated : NO ] ;
2014-11-27 11:36:28 -05:00
}
2018-08-26 11:56:27 -07:00
# endif
2015-01-01 16:50:10 -05:00
2015-12-01 20:31:24 -08:00
TemporaryApp * currentApp = [ self findRunningApp : app . host ] ;
2015-01-01 16:50:10 -05:00
if ( currentApp ! = nil ) {
2015-01-09 17:25:18 -05:00
UIAlertController * alertController = [ UIAlertController
2015-07-10 21:22:57 -07:00
alertControllerWithTitle : app . name
message : [ app . id isEqualToString : currentApp . id ] ? @ "" : [ NSString stringWithFormat : @ "%@ is currently running" , currentApp . name ] preferredStyle : UIAlertControllerStyleAlert ] ;
2015-01-09 17:25:18 -05:00
[ alertController addAction : [ UIAlertAction
2015-07-10 21:22:57 -07:00
actionWithTitle : [ app . id isEqualToString : currentApp . id ] ? @ "Resume App" : @ "Resume Running App" style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
Log ( LOG_I , @ "Resuming application: %@" , currentApp . name ) ;
2018-06-02 16:38:02 -07:00
[ self prepareToStreamApp : currentApp ] ;
2015-01-31 01:57:23 -05:00
[ self performSegueWithIdentifier : @ "createStreamFrame" sender : nil ] ;
} ] ] ;
2015-01-09 17:25:18 -05:00
[ alertController addAction : [ UIAlertAction actionWithTitle :
2015-07-10 21:22:57 -07:00
[ app . id isEqualToString : currentApp . id ] ? @ "Quit App" : @ "Quit Running App and Start" style : UIAlertActionStyleDestructive handler : ^ ( UIAlertAction * action ) {
Log ( LOG_I , @ "Quitting application: %@" , currentApp . name ) ;
2018-11-15 22:02:05 -08:00
[ self showLoadingFrame : ^ {
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
2018-12-22 00:05:48 -08:00
HttpManager * hMan = [ [ HttpManager alloc ] initWithHost : app . host . activeAddress uniqueId : self -> _uniqueId serverCert : app . host . serverCert ] ;
2018-11-15 22:02:05 -08:00
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 removeHostFromDiscovery : app . host ] ;
[ hMan executeRequestSynchronously : quitRequest ] ;
if ( quitResponse . statusCode = = 200 ) {
ServerInfoResponse * serverInfoResp = [ [ ServerInfoResponse alloc ] init ] ;
2018-11-16 19:02:31 -08:00
[ hMan executeRequestSynchronously : [ HttpRequest requestForResponse : serverInfoResp withUrlRequest : [ hMan newServerInfoRequest : false ]
2018-11-15 22:02:05 -08:00
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 ;
}
2018-12-28 00:32:19 -08:00
else if ( [ serverInfoResp isStatusOk ] ) {
// Update the host object with this info
[ serverInfoResp populateHost : app . host ] ;
}
2016-09-24 21:26:50 -07:00
}
2018-11-15 22:02:05 -08:00
[ self -> _discMan addHostToDiscovery : app . host ] ;
2019-09-14 13:08:47 -07:00
2018-11-15 22:02:05 -08:00
// If it fails , display an error and stop the current operation
if ( quitResponse . statusCode ! = 200 ) {
2019-09-14 13:08:47 -07:00
UIAlertController * alert = [ UIAlertController alertControllerWithTitle : @ "Quitting App Failed"
2018-11-15 22:02:05 -08:00
message : @ "Failed to quit app. If this app was started by "
"another device, you'll need to quit from that device."
preferredStyle : UIAlertControllerStyleAlert ] ;
2019-09-14 13:08:47 -07:00
[ alert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-11-15 22:02:05 -08:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self updateAppsForHost : app . host ] ;
[ self hideLoadingFrame : ^ {
2019-09-14 13:08:47 -07:00
[ [ self activeViewController ] presentViewController : alert animated : YES completion : nil ] ;
2018-11-15 22:02:05 -08:00
} ] ;
} ) ;
}
else {
app . host . currentGame = @ "0" ;
2019-09-14 13:08:47 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
// Refresh the UI
[ self updateAppsForHost : app . host ] ;
// 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 ] ;
}
} ) ;
2018-11-15 22:02:05 -08:00
}
2015-07-09 20:33:23 -07:00
} ) ;
2018-11-15 22:02:05 -08:00
} ] ;
2015-01-31 01:57:23 -05:00
} ] ] ;
2015-01-01 16:50:10 -05:00
[ alertController addAction : [ UIAlertAction actionWithTitle : @ "Cancel" style : UIAlertActionStyleCancel handler : nil ] ] ;
2018-08-26 11:56:27 -07:00
[ [ self activeViewController ] presentViewController : alertController animated : YES completion : nil ] ;
2015-01-01 16:50:10 -05:00
} else {
2018-06-02 16:38:02 -07:00
[ self prepareToStreamApp : app ] ;
2015-01-01 16:50:10 -05:00
[ self performSegueWithIdentifier : @ "createStreamFrame" sender : nil ] ;
}
}
2015-12-01 20:31:24 -08:00
- ( TemporaryApp * ) findRunningApp : ( TemporaryHost * ) host {
for ( TemporaryApp * app in host . appList ) {
2016-10-21 11:41:42 -07:00
if ( [ app . id isEqualToString : host . currentGame ] ) {
2015-01-01 16:50:10 -05:00
return app ;
}
}
return nil ;
}
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2014-10-29 00:47:51 -04:00
- ( void ) revealController : ( SWRevealViewController * ) revealController didMoveToPosition : ( FrontViewPosition ) position {
// If we moved back to the center position , we should save the settings
if ( position = = FrontViewPositionLeft ) {
[ ( SettingsViewController * ) [ revealController rearViewController ] saveSettings ] ;
2014-10-20 20:54:03 -04:00
}
2018-05-28 12:04:38 -07:00
2014-11-27 11:36:28 -05:00
currentPosition = position ;
2014-08-31 13:13:46 -04:00
}
2018-08-26 11:56:27 -07:00
# endif
# if TARGET_OS _TV
- ( void ) collectionView : ( UICollectionView * ) collectionView didSelectItemAtIndexPath : ( NSIndexPath * ) indexPath {
[ self appClicked : _sortedAppList [ indexPath . row ] ] ;
}
# endif
2014-08-31 13:13:46 -04:00
2015-02-01 19:46:28 -05:00
- ( void ) prepareForSegue : ( UIStoryboardSegue * ) segue sender : ( id ) sender {
if ( [ segue . destinationViewController isKindOfClass : [ StreamFrameViewController class ] ] ) {
StreamFrameViewController * streamFrame = segue . destinationViewController ;
streamFrame . streamConfig = _streamConfig ;
}
}
2018-11-15 22:02:05 -08:00
- ( void ) showLoadingFrame : ( void ( ^ ) ( void ) ) completion {
2018-12-28 16:45:05 -08:00
[ _loadingFrame showLoadingFrame : completion ] ;
2015-02-24 22:13:33 -05:00
}
2018-08-26 12:25:22 -07:00
- ( void ) hideLoadingFrame : ( void ( ^ ) ( void ) ) completion {
2018-12-28 16:45:05 -08:00
[ self enableNavigation ] ;
[ _loadingFrame dismissLoadingFrame : completion ] ;
2015-02-24 22:13:33 -05:00
}
2019-09-14 12:28:21 -07:00
- ( void ) adjustScrollViewForSafeArea : ( UIScrollView * ) view {
if ( @ available ( iOS 11.0 , * ) ) {
if ( self . view . safeAreaInsets . left >= 20 || self . view . safeAreaInsets . right >= 20 ) {
view . contentInset = UIEdgeInsetsMake ( 0 , 20 , 0 , 20 ) ;
}
}
}
// Adjust the subviews for the safe area on the iPhone X .
- ( void ) viewSafeAreaInsetsDidChange {
[ super viewSafeAreaInsetsDidChange ] ;
[ self adjustScrollViewForSafeArea : self . collectionView ] ;
[ self adjustScrollViewForSafeArea : self -> hostScrollView ] ;
}
2014-08-31 13:13:46 -04:00
- ( void ) viewDidLoad
{
[ super viewDidLoad ] ;
2014-10-29 00:47:51 -04:00
2019-08-30 21:15:11 -07:00
self . title = @ "Select Host" ;
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2015-02-01 19:46:28 -05:00
// Set the side bar button action . When it ' s tapped , it ' ll show the sidebar .
2019-08-30 21:15:11 -07:00
[ _settingsButton setTarget : self . revealViewController ] ;
[ _settingsButton setAction : @ selector ( revealToggle : ) ] ;
2014-10-29 00:47:51 -04:00
2015-02-01 19:46:28 -05:00
// Set the host name button action . When it ' s tapped , it ' ll show the host selection view .
2019-08-30 21:15:11 -07:00
[ _upButton setTarget : self ] ;
[ _upButton setAction : @ selector ( showHostSelectionView ) ] ;
2019-09-14 12:46:50 -07:00
[ self disableUpButton ] ;
2014-10-29 00:47:51 -04:00
// Set the gesture
[ self . view addGestureRecognizer : self . revealViewController . panGestureRecognizer ] ;
2014-11-27 13:56:39 -05:00
// Get callbacks associated with the viewController
2014-10-29 00:47:51 -04:00
[ self . revealViewController setDelegate : self ] ;
2019-09-18 19:59:24 -07:00
// Disable bounce - back on reveal VC otherwise the settings will snap closed
// if the user drags all the way off the screen opposite the settings pane .
self . revealViewController . bounceBackOnOverdraw = NO ;
2018-08-26 15:05:32 -07:00
# else
2019-09-24 18:56:37 -07:00
// The settings button will direct the user into the Settings app on tvOS
[ _settingsButton setTarget : self ] ;
[ _settingsButton setAction : @ selector ( openTvSettings : ) ] ;
2019-09-22 20:33:38 -07:00
// Restore focus on the selected app on view controller pop navigation
self . restoresFocusAfterTransition = NO ;
self . collectionView . remembersLastFocusedIndexPath = YES ;
2018-08-26 15:05:32 -07:00
_menuRecognizer = [ [ UITapGestureRecognizer alloc ] init ] ;
[ _menuRecognizer addTarget : self action : @ selector ( showHostSelectionView ) ] ;
_menuRecognizer . allowedPressTypes = [ [ NSArray alloc ] initWithObjects : [ NSNumber numberWithLong : UIPressTypeMenu ] , nil ] ;
2018-08-26 19:00:16 -07:00
self . navigationController . navigationBar . titleTextAttributes = [ NSDictionary dictionaryWithObject : [ UIColor whiteColor ] forKey : NSForegroundColorAttributeName ] ;
2018-08-26 11:56:27 -07:00
# endif
2014-10-29 00:47:51 -04:00
2018-08-26 12:25:22 -07:00
_loadingFrame = [ self . storyboard instantiateViewControllerWithIdentifier : @ "loadingFrame" ] ;
2014-11-27 11:36:28 -05:00
// Set the current position to the center
currentPosition = FrontViewPositionLeft ;
2014-10-19 04:19:54 -04:00
2014-11-27 13:56:39 -05:00
// Set up crypto
2018-08-27 00:34:34 -07:00
[ CryptoManager generateKeyPairUsingSSL ] ;
2015-10-31 23:03:21 -07:00
_uniqueId = [ IdManager getUniqueId ] ;
2018-12-22 00:05:48 -08:00
_clientCert = [ CryptoManager readCertFromFile ] ;
2015-01-31 20:10:14 -05:00
_appManager = [ [ AppAssetManager alloc ] initWithCallback : self ] ;
2015-02-01 19:46:28 -05:00
_opQueue = [ [ NSOperationQueue alloc ] init ] ;
2015-01-08 22:18:26 -05:00
2014-10-29 00:47:51 -04:00
// Only initialize the host picker list once
2014-10-26 02:15:53 -04:00
if ( hostList = = nil ) {
hostList = [ [ NSMutableSet alloc ] init ] ;
}
2014-10-24 16:02:10 -04:00
2015-11-10 21:23:00 -08:00
_boxArtCache = [ [ NSCache alloc ] init ] ;
2019-09-15 15:46:23 -07:00
2015-09-30 19:13:37 -07:00
hostScrollView = [ [ ComputerScrollView alloc ] init ] ;
2014-10-29 00:47:51 -04:00
hostScrollView . frame = CGRectMake ( 0 , self . navigationController . navigationBar . frame . origin . y + self . navigationController . navigationBar . frame . size . height , self . view . frame . size . width , self . view . frame . size . height / 2 ) ;
2014-10-24 16:02:10 -04:00
[ hostScrollView setShowsHorizontalScrollIndicator : NO ] ;
2015-09-30 19:13:37 -07:00
hostScrollView . delaysContentTouches = NO ;
self . collectionView . delaysContentTouches = NO ;
2015-10-18 16:52:14 -07:00
self . collectionView . allowsMultipleSelection = NO ;
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2015-10-18 16:52:14 -07:00
self . collectionView . multipleTouchEnabled = NO ;
2018-08-26 11:56:27 -07:00
# endif
2014-10-29 00:47:51 -04:00
2014-11-26 17:57:16 -05:00
[ self retrieveSavedHosts ] ;
2015-01-01 22:30:03 -05:00
_discMan = [ [ DiscoveryManager alloc ] initWithHosts : [ hostList allObjects ] andCallback : self ] ;
2014-10-24 16:02:10 -04:00
[ self . view addSubview : hostScrollView ] ;
2014-10-21 05:11:51 -04:00
}
2019-09-24 18:56:37 -07:00
# if TARGET_OS _TV
- ( void ) openTvSettings : ( id ) sender
{
[ [ UIApplication sharedApplication ] openURL : [ NSURL URLWithString : UIApplicationOpenSettingsURLString ] options : @ { } completionHandler : nil ] ;
}
# endif
2018-08-29 07:52:33 -07:00
- ( void ) beginForegroundRefresh : ( bool ) refreshAppList
2018-06-01 00:47:44 -07:00
{
if ( ! _background ) {
2018-06-02 16:31:16 -07:00
// This will kick off box art caching
[ self updateHosts ] ;
2018-06-01 00:47:44 -07:00
[ _discMan startDiscovery ] ;
// This will refresh the applist when a paired host is selected
2018-08-29 07:52:33 -07:00
if ( refreshAppList && _selectedHost ! = nil && _selectedHost . pairState = = PairStatePaired ) {
2018-06-01 00:47:44 -07:00
[ self hostClicked : _selectedHost view : nil ] ;
}
}
}
2019-11-03 13:38:52 -08:00
- ( void ) handlePendingShortcutAction
{
// Check if we have a pending shortcut action
AppDelegate * delegate = ( AppDelegate * ) [ UIApplication sharedApplication ] . delegate ;
if ( delegate . pcUuidToLoad ! = nil ) {
// Find the host it corresponds to
TemporaryHost * matchingHost = nil ;
for ( TemporaryHost * host in hostList ) {
if ( [ host . uuid isEqualToString : delegate . pcUuidToLoad ] ) {
matchingHost = host ;
break ;
}
}
// Clear the pending shortcut action
delegate . pcUuidToLoad = nil ;
// Complete the request
if ( delegate . shortcutCompletionHandler ! = nil ) {
delegate . shortcutCompletionHandler ( matchingHost ! = nil ) ;
delegate . shortcutCompletionHandler = nil ;
}
if ( matchingHost ! = nil && _selectedHost ! = matchingHost ) {
// Navigate to the host page
[ self hostClicked : matchingHost view : nil ] ;
}
}
}
2015-11-10 21:23:00 -08:00
- ( void ) handleReturnToForeground
{
2018-05-31 00:46:41 -07:00
_background = NO ;
2018-08-29 07:52:33 -07:00
[ self beginForegroundRefresh : YES ] ;
2019-11-03 13:38:52 -08:00
// Check for a pending shortcut action when returning to foreground
[ self handlePendingShortcutAction ] ;
2015-11-10 21:23:00 -08:00
}
2018-05-31 00:46:41 -07:00
- ( void ) handleEnterBackground
{
_background = YES ;
[ _discMan stopDiscovery ] ;
}
2014-10-21 05:11:51 -04:00
- ( void ) viewDidAppear : ( BOOL ) animated
{
2014-11-26 17:57:16 -05:00
[ super viewDidAppear : animated ] ;
2017-09-15 00:30:51 -07:00
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2017-09-15 00:30:51 -07:00
[ [ self revealViewController ] setPrimaryViewController : self ] ;
2018-08-26 11:56:27 -07:00
# endif
2017-09-15 00:30:51 -07:00
2014-10-29 00:47:51 -04:00
[ self . navigationController setNavigationBarHidden : NO animated : YES ] ;
2014-12-04 18:41:01 +09:00
// Hide 1 px border line
UIImage * fakeImage = [ [ UIImage alloc ] init ] ;
[ self . navigationController . navigationBar setShadowImage : fakeImage ] ;
[ self . navigationController . navigationBar setBackgroundImage : fakeImage forBarPosition : UIBarPositionAny barMetrics : UIBarMetricsDefault ] ;
2019-11-03 13:38:52 -08:00
// Check for a pending shortcut action when appearing
[ self handlePendingShortcutAction ] ;
2018-06-09 16:28:37 -07:00
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( handleReturnToForeground )
name : UIApplicationDidBecomeActiveNotification
object : nil ] ;
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( handleEnterBackground )
name : UIApplicationWillResignActiveNotification
object : nil ] ;
2018-08-26 12:25:22 -07:00
}
- ( void ) viewWillAppear : ( BOOL ) animated
{
[ super viewWillAppear : animated ] ;
2018-06-09 16:28:37 -07:00
2018-05-31 00:46:41 -07:00
// We can get here on home press while streaming
// since the stream view segues to us just before
// entering the background . We can ' t check the app
// state here ( since it ' s in transition ) , so we have
2018-06-01 00:47:44 -07:00
// to use this function that will use our internal
// state here to determine whether we ' re foreground .
//
// Note that this is neccessary here as we may enter
// this view via an error dialog from the stream
// view , so we won ' t get a return to active notification
// for that which would normally fire beginForegroundRefresh .
2018-08-26 12:25:22 -07:00
//
// On tvOS , we ' ll get a viewWillAppear when returning from the
// loading frame which will cause an infinite loop by starting
// another loading frame . To avoid this , just don ' t refresh
// if we ' re coming back from a loading frame view .
2018-12-28 16:45:05 -08:00
[ self beginForegroundRefresh : ! ( [ _loadingFrame isShown ] || [ _loadingFrame isBeingDismissed ] ) ] ;
2014-10-21 05:11:51 -04:00
}
- ( void ) viewDidDisappear : ( BOOL ) animated
{
2014-11-26 17:57:16 -05:00
[ super viewDidDisappear : animated ] ;
2018-06-09 16:28:37 -07:00
// when discovery stops , we must create a new instance because
// you cannot restart an NSOperation when it is finished
2015-01-01 22:30:03 -05:00
[ _discMan stopDiscovery ] ;
2015-01-31 01:57:23 -05:00
2015-11-10 21:23:00 -08:00
// Purge the box art cache
[ _boxArtCache removeAllObjects ] ;
2018-06-09 16:28:37 -07:00
// Remove our lifetime observers to avoid triggering them
// while streaming
[ [ NSNotificationCenter defaultCenter ] removeObserver : self ] ;
2014-10-14 19:21:48 -04:00
}
2014-11-26 17:57:16 -05:00
- ( void ) retrieveSavedHosts {
DataManager * dataMan = [ [ DataManager alloc ] init ] ;
2015-12-02 00:25:38 -08:00
NSArray * hosts = [ dataMan getHosts ] ;
2015-01-01 23:54:54 -05:00
@ synchronized ( hostList ) {
[ hostList addObjectsFromArray : hosts ] ;
2015-07-10 00:28:51 -07:00
// Initialize the non - persistent host state
2015-12-01 20:31:24 -08:00
for ( TemporaryHost * host in hostList ) {
2015-07-10 12:18:09 -07:00
if ( host . activeAddress = = nil ) {
host . activeAddress = host . localAddress ;
}
if ( host . activeAddress = = nil ) {
host . activeAddress = host . externalAddress ;
}
if ( host . activeAddress = = nil ) {
host . activeAddress = host . address ;
}
2019-07-14 19:17:17 -07:00
if ( host . activeAddress = = nil ) {
host . activeAddress = host . ipv6Address ;
}
2015-07-10 00:28:51 -07:00
}
2015-01-01 23:54:54 -05:00
}
2014-11-26 17:57:16 -05:00
}
2015-01-01 22:30:03 -05:00
- ( void ) updateAllHosts : ( NSArray * ) hosts {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2015-02-10 21:26:55 -05:00
Log ( LOG_D , @ "New host list:" ) ;
2015-12-01 20:31:24 -08:00
for ( TemporaryHost * host in hosts ) {
2019-08-30 19:19:23 -07:00
Log ( LOG_D , @ "Host: \n{\n\t name:%@ \n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \n\t ipv6Address:%@ \n\t uuid:%@ \n\t mac:%@ \n\t pairState:%d \n\t online:%d \n\t activeAddress:%@ \n}" , host . name , host . address , host . localAddress , host . externalAddress , host . ipv6Address , host . uuid , host . mac , host . pairState , host . state , host . activeAddress ) ;
2015-01-01 22:30:03 -05:00
}
2015-01-01 23:54:54 -05:00
@ synchronized ( hostList ) {
[ hostList removeAllObjects ] ;
[ hostList addObjectsFromArray : hosts ] ;
}
2015-01-01 22:30:03 -05:00
[ self updateHosts ] ;
} ) ;
}
2019-11-03 13:38:52 -08:00
- ( void ) updateHostShortcuts {
if ( @ available ( iOS 9.0 , * ) ) {
NSMutableArray * quickActions = [ [ NSMutableArray alloc ] init ] ;
@ synchronized ( hostList ) {
for ( TemporaryHost * host in hostList ) {
// Pair state may be unknown if we haven ' t polled it yet , but the app list
// count will persist from paired PCs
if ( [ host . appList count ] > 0 ) {
UIApplicationShortcutItem * shortcut = [ [ UIApplicationShortcutItem alloc ]
initWithType : @ "PC"
localizedTitle : host . name
localizedSubtitle : nil
icon : [ UIApplicationShortcutIcon iconWithType : UIApplicationShortcutIconTypePlay ]
userInfo : [ NSDictionary dictionaryWithObject : host . uuid forKey : @ "UUID" ] ] ;
[ quickActions addObject : shortcut ] ;
}
}
}
[ UIApplication sharedApplication ] . shortcutItems = quickActions ;
}
}
2015-01-01 22:30:03 -05:00
- ( void ) updateHosts {
2015-02-10 21:26:55 -05:00
Log ( LOG_I , @ "Updating hosts..." ) ;
2014-10-26 02:15:53 -04:00
[ [ hostScrollView subviews ] makeObjectsPerformSelector : @ selector ( removeFromSuperview ) ] ;
UIComputerView * addComp = [ [ UIComputerView alloc ] initForAddWithCallback : self ] ;
UIComputerView * compView ;
float prevEdge = -1 ;
2015-01-01 23:54:54 -05:00
@ synchronized ( hostList ) {
2015-11-13 19:55:18 -08:00
// Sort the host list in alphabetical order
NSArray * sortedHostList = [ [ hostList allObjects ] sortedArrayUsingSelector : @ selector ( compareName : ) ] ;
2015-12-01 20:31:24 -08:00
for ( TemporaryHost * comp in sortedHostList ) {
2015-01-01 23:54:54 -05:00
compView = [ [ UIComputerView alloc ] initWithComputer : comp andCallback : self ] ;
compView . center = CGPointMake ( [ self getCompViewX : compView addComp : addComp prevEdge : prevEdge ] , hostScrollView . frame . size . height / 2 ) ;
prevEdge = compView . frame . origin . x + compView . frame . size . width ;
[ hostScrollView addSubview : compView ] ;
2018-06-02 16:31:16 -07:00
// Start jobs to decode the box art in advance
for ( TemporaryApp * app in comp . appList ) {
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _LOW , 0 ) , ^ {
[ self updateBoxArtCacheForApp : app ] ;
} ) ;
}
2015-01-01 23:54:54 -05:00
}
2014-10-26 02:15:53 -04:00
}
2015-11-10 21:23:00 -08:00
2019-11-03 13:38:52 -08:00
// Create or delete host shortcuts as needed
[ self updateHostShortcuts ] ;
2014-10-26 02:15:53 -04:00
prevEdge = [ self getCompViewX : addComp addComp : addComp prevEdge : prevEdge ] ;
addComp . center = CGPointMake ( prevEdge , hostScrollView . frame . size . height / 2 ) ;
2014-10-21 13:39:14 -04:00
2014-10-26 02:15:53 -04:00
[ hostScrollView addSubview : addComp ] ;
[ hostScrollView setContentSize : CGSizeMake ( prevEdge + addComp . frame . size . width , hostScrollView . frame . size . height ) ] ;
}
- ( float ) getCompViewX : ( UIComputerView * ) comp addComp : ( UIComputerView * ) addComp prevEdge : ( float ) prevEdge {
2019-08-30 22:33:32 -07:00
float padding ;
# if TARGET_OS _TV
padding = 100 ;
# else
padding = addComp . frame . size . width / 2 ;
# endif
2014-10-26 02:15:53 -04:00
if ( prevEdge = = -1 ) {
2019-08-30 22:33:32 -07:00
return hostScrollView . frame . origin . x + comp . frame . size . width / 2 + padding ;
2014-10-26 02:15:53 -04:00
} else {
2019-08-30 22:33:32 -07:00
return prevEdge + comp . frame . size . width / 2 + padding ;
2014-10-26 02:15:53 -04:00
}
}
2015-11-10 21:23:00 -08:00
// This function forces immediate decoding of the UIImage , rather
// than the default lazy decoding that results in janky scrolling .
2015-12-01 20:31:24 -08:00
+ ( UIImage * ) loadBoxArtForCaching : ( TemporaryApp * ) app {
2018-06-02 16:31:16 -07:00
UIImage * boxArt ;
2015-11-10 21:23:00 -08:00
2018-08-27 01:54:58 -07:00
NSData * imageData = [ NSData dataWithContentsOfFile : [ AppAssetManager boxArtPathForApp : app ] ] ;
if ( imageData = = nil ) {
// No box art on disk
return nil ;
}
CGImageSourceRef source = CGImageSourceCreateWithData ( ( __bridge CFDataRef ) imageData , NULL ) ;
2018-06-02 16:31:16 -07:00
CGImageRef cgImage = CGImageSourceCreateImageAtIndex ( source , 0 , nil ) ;
2015-11-10 21:23:00 -08:00
2018-06-02 16:31:16 -07:00
size_t width = CGImageGetWidth ( cgImage ) ;
size_t height = CGImageGetHeight ( cgImage ) ;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB ( ) ;
CGContextRef imageContext = CGBitmapContextCreate ( NULL , width , height , 8 , width * 4 , colorSpace ,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little ) ;
CGColorSpaceRelease ( colorSpace ) ;
CGContextDrawImage ( imageContext , CGRectMake ( 0 , 0 , width , height ) , cgImage ) ;
CGImageRef outputImage = CGBitmapContextCreateImage ( imageContext ) ;
boxArt = [ UIImage imageWithCGImage : outputImage ] ;
CGImageRelease ( outputImage ) ;
CGContextRelease ( imageContext ) ;
2015-11-10 21:23:00 -08:00
CGImageRelease ( cgImage ) ;
CFRelease ( source ) ;
return boxArt ;
}
2015-12-01 20:31:24 -08:00
- ( void ) updateBoxArtCacheForApp : ( TemporaryApp * ) app {
2018-08-27 01:54:58 -07:00
if ( [ _boxArtCache objectForKey : app ] = = nil ) {
UIImage * image = [ MainFrameViewController loadBoxArtForCaching : app ] ;
if ( image ! = nil ) {
// Add the image to our cache if it was present
[ _boxArtCache setObject : image forKey : app ] ;
}
2015-11-10 21:23:00 -08:00
}
}
2015-12-01 20:31:24 -08:00
- ( void ) updateAppsForHost : ( TemporaryHost * ) host {
2015-07-11 23:59:09 -07:00
if ( host ! = _selectedHost ) {
Log ( LOG_W , @ "Mismatched host during app update" ) ;
return ;
}
_sortedAppList = [ host . appList allObjects ] ;
2015-07-11 01:28:52 -07:00
_sortedAppList = [ _sortedAppList sortedArrayUsingSelector : @ selector ( compareName : ) ] ;
2014-12-04 18:41:01 +09:00
[ hostScrollView removeFromSuperview ] ;
[ self . collectionView reloadData ] ;
2014-10-26 02:15:53 -04:00
}
2014-12-04 18:41:01 +09:00
- ( UICollectionViewCell * ) collectionView : ( UICollectionView * ) collectionView cellForItemAtIndexPath : ( NSIndexPath * ) indexPath {
UICollectionViewCell * cell = [ collectionView dequeueReusableCellWithReuseIdentifier : @ "AppCell" forIndexPath : indexPath ] ;
2015-12-01 20:31:24 -08:00
TemporaryApp * app = _sortedAppList [ indexPath . row ] ;
2015-11-10 21:23:00 -08:00
UIAppView * appView = [ [ UIAppView alloc ] initWithApp : app cache : _boxArtCache andCallback : self ] ;
2015-01-01 16:50:10 -05:00
2014-12-04 18:41:01 +09:00
if ( appView . bounds . size . width > 10.0 ) {
CGFloat scale = cell . bounds . size . width / appView . bounds . size . width ;
[ appView setCenter : CGPointMake ( appView . bounds . size . width / 2 * scale , appView . bounds . size . height / 2 * scale ) ] ;
appView . transform = CGAffineTransformMakeScale ( scale , scale ) ;
2014-10-21 13:39:14 -04:00
}
2015-01-01 16:50:10 -05:00
2014-12-04 18:41:01 +09:00
[ cell . subviews . firstObject removeFromSuperview ] ; // Remove a view that was previously added
[ cell addSubview : appView ] ;
2015-09-28 21:18:35 -07:00
UIBezierPath * shadowPath = [ UIBezierPath bezierPathWithRect : cell . bounds ] ;
cell . layer . masksToBounds = NO ;
cell . layer . shadowColor = [ UIColor blackColor ] . CGColor ;
cell . layer . shadowOffset = CGSizeMake ( 1.0 f , 5.0 f ) ;
cell . layer . shadowOpacity = 0.5 f ;
cell . layer . shadowPath = shadowPath . CGPath ;
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2018-10-19 00:03:14 -04:00
cell . layer . borderWidth = 1 ;
2018-08-26 11:56:27 -07:00
cell . layer . borderColor = [ [ UIColor colorWithRed : 0 green : 0 blue : 0 alpha : 0.3 f ] CGColor ] ;
2015-10-18 16:52:14 -07:00
cell . exclusiveTouch = YES ;
2018-08-26 11:56:27 -07:00
# endif
2015-10-18 16:52:14 -07:00
2014-12-04 18:41:01 +09:00
return cell ;
}
- ( NSInteger ) numberOfSectionsInCollectionView : ( UICollectionView * ) collectionView {
return 1 ; // App collection only
}
- ( NSInteger ) collectionView : ( UICollectionView * ) collectionView numberOfItemsInSection : ( NSInteger ) section {
2019-11-03 14:40:32 -08:00
if ( _selectedHost ! = nil && _sortedAppList ! = nil ) {
return _sortedAppList . count ;
2015-07-11 01:28:52 -07:00
}
else {
return 0 ;
}
2014-08-31 13:13:46 -04:00
}
- ( void ) didReceiveMemoryWarning
{
[ super didReceiveMemoryWarning ] ;
2018-08-29 09:24:47 -07:00
// Purge the box art cache on low memory
[ _boxArtCache removeAllObjects ] ;
2014-08-31 13:13:46 -04:00
}
2014-10-20 21:39:40 -04:00
2014-10-20 21:45:37 -04:00
- ( void ) touchesBegan : ( NSSet * ) touches withEvent : ( UIEvent * ) event {
[ self . view endEditing : YES ] ;
}
- ( BOOL ) textFieldShouldReturn : ( UITextField * ) textField {
[ textField resignFirstResponder ] ;
return YES ;
}
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2014-10-20 21:39:40 -04:00
- ( BOOL ) shouldAutorotate {
return YES ;
}
2018-08-26 11:56:27 -07:00
# endif
2015-02-01 19:46:28 -05:00
2015-07-11 23:59:09 -07:00
- ( void ) disableNavigation {
self . navigationController . navigationBar . topItem . rightBarButtonItem . enabled = NO ;
}
- ( void ) enableNavigation {
self . navigationController . navigationBar . topItem . rightBarButtonItem . enabled = YES ;
}
2018-10-19 00:03:14 -04:00
# if TARGET_OS _TV
- ( BOOL ) canBecomeFocused {
return YES ;
}
# endif
2018-08-26 11:56:27 -07:00
- ( void ) didUpdateFocusInContext : ( UIFocusUpdateContext * ) context withAnimationCoordinator : ( UIFocusAnimationCoordinator * ) coordinator {
2018-10-19 00:03:14 -04:00
# if ! TARGET_OS _TV
2018-08-26 11:56:27 -07:00
if ( context . nextFocusedView ! = nil ) {
[ context . nextFocusedView setAlpha : 0.8 ] ;
}
[ context . previouslyFocusedView setAlpha : 1.0 ] ;
2018-10-19 00:03:14 -04:00
# endif
2018-08-26 11:56:27 -07:00
}
2014-08-31 13:13:46 -04:00
@ end