/* * PhoneGap is available under *either* the terms of the modified BSD license *or* the * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text. * * Copyright (c) 2005-2010, Nitobi Software Inc. * Copyright (c) 2010, IBM Corporation */ #import #import #import "Location.h" #import "Sound.h" #import "DebugConsole.h" #import "Connection.h" #import "PGURLProtocol.h" #import "PGWhitelist.h" #import "InvokedUrlCommand.h" #import "PhoneGapDelegate.h" #import "PhoneGapViewController.h" #import "PGPlugin.h" #define SYMBOL_TO_NSSTRING_HELPER(x) @#x #define SYMBOL_TO_NSSTRING(x) SYMBOL_TO_NSSTRING_HELPER(x) #define degreesToRadian(x) (M_PI * (x) / 180.0) // class extension @interface PhoneGapDelegate () // readwrite access for self @property (nonatomic, readwrite, retain) IBOutlet PhoneGapViewController *viewController; @property (nonatomic, readwrite, retain) IBOutlet UIActivityIndicatorView *activityView; @property (nonatomic, readwrite, retain) UIImageView *imageView; @property (nonatomic, readwrite, retain) NSMutableDictionary *pluginObjects; @property (nonatomic, readwrite, retain) NSDictionary *pluginsMap; @property (nonatomic, readwrite, retain) NSDictionary *settings; @property (nonatomic, readwrite, retain) NSURL *invokedURL; @property (readwrite, assign) BOOL loadFromString; @property (readwrite, assign) UIInterfaceOrientation orientationType; @property (nonatomic, readwrite, retain) NSString *sessionKey; @property (nonatomic, readwrite, retain) PGWhitelist* whitelist; @end @implementation PhoneGapDelegate @synthesize window, webView, viewController, activityView, imageView; @synthesize settings, invokedURL, loadFromString, orientationType, sessionKey; @synthesize pluginObjects, pluginsMap, whitelist; - (id) init { self = [super init]; if (self != nil) { self.pluginObjects = [[[NSMutableDictionary alloc] initWithCapacity:4] autorelease]; self.imageView = nil; // Turn on cookie support ( shared with our app only! ) NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; [cookieStorage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; // Create the sessionKey to use throughout the lifetime of the application // to authenticate the source of the gap calls self.sessionKey = [NSString stringWithFormat:@"%d", arc4random()]; [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedOrientationChange) name:UIDeviceOrientationDidChangeNotification object:nil]; [PGURLProtocol registerPGHttpURLProtocol]; } return self; } + (NSString*) applicationDocumentsDirectory { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil; return basePath; } + (NSString*) wwwFolderName { return @"www"; } + (NSString*) startPage { return @"index.html"; } + (BOOL) isIPad { #ifdef UI_USER_INTERFACE_IDIOM return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad); #else return NO; #endif } + (NSString*) resolveImageResource:(NSString*)resource { NSString* systemVersion = [[UIDevice currentDevice] systemVersion]; BOOL isLessThaniOS4 = ([systemVersion compare:@"4.0" options:NSNumericSearch] == NSOrderedAscending); // the iPad image (nor retina) differentiation code was not in 3.x, and we have to explicitly set the path if (isLessThaniOS4) { if ([[self class] isIPad]) { return [NSString stringWithFormat:@"%@~ipad.png", resource]; } else { return [NSString stringWithFormat:@"%@.png", resource]; } } return resource; } + (NSString*) pathForResource:(NSString*)resourcepath { NSBundle * mainBundle = [NSBundle mainBundle]; NSMutableArray *directoryParts = [NSMutableArray arrayWithArray:[resourcepath componentsSeparatedByString:@"/"]]; NSString *filename = [directoryParts lastObject]; [directoryParts removeLastObject]; NSString* directoryPartsJoined =[directoryParts componentsJoinedByString:@"/"]; NSString* directoryStr = [self wwwFolderName]; if ([directoryPartsJoined length] > 0) { directoryStr = [NSString stringWithFormat:@"%@/%@", [self wwwFolderName], [directoryParts componentsJoinedByString:@"/"]]; } return [mainBundle pathForResource:filename ofType:@"" inDirectory:directoryStr]; } /** Returns the current version of phoneGap as read from the VERSION file This only touches the filesystem once and stores the result in the class variable gapVersion */ static NSString *gapVersion; + (NSString*) phoneGapVersion { #ifdef PG_VERSION gapVersion = SYMBOL_TO_NSSTRING(PG_VERSION); #else if (gapVersion == nil) { NSBundle *mainBundle = [NSBundle mainBundle]; NSString *filename = [mainBundle pathForResource:@"VERSION" ofType:nil]; // read from the filesystem and save in the variable // first, separate by new line NSString* fileContents = [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:NULL]; NSArray* all_lines = [fileContents componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; NSString* first_line = [all_lines objectAtIndex:0]; gapVersion = [first_line retain]; } #endif return gapVersion; } /** Returns an instance of a PhoneGapCommand object, based on its name. If one exists already, it is returned. */ -(id) getCommandInstance:(NSString*)pluginName { // first, we try to find the pluginName in the pluginsMap // (acts as a whitelist as well) if it does not exist, we return nil // NOTE: plugin names are matched as lowercase to avoid problems - however, a // possible issue is there can be duplicates possible if you had: // "com.phonegap.Foo" and "com.phonegap.foo" - only the lower-cased entry will match NSString* className = [self.pluginsMap objectForKey:[pluginName lowercaseString]]; if (className == nil) { return nil; } id obj = [self.pluginObjects objectForKey:className]; if (!obj) { // attempt to load the settings for this command class NSDictionary* classSettings = [self.settings objectForKey:className]; if (classSettings) { obj = [[NSClassFromString(className) alloc] initWithWebView:webView settings:classSettings]; } else { obj = [[NSClassFromString(className) alloc] initWithWebView:webView]; } if (obj != nil) { [self.pluginObjects setObject:obj forKey:className]; [obj release]; } else { NSLog(@"PGPlugin class %@ (pluginName: %@) does not exist.", className, pluginName); } } return obj; } - (NSArray*) parseInterfaceOrientations:(NSArray*)orientations { NSMutableArray* result = [[[NSMutableArray alloc] init] autorelease]; if (orientations != nil) { NSEnumerator* enumerator = [orientations objectEnumerator]; NSString* orientationString; while (orientationString = [enumerator nextObject]) { if ([orientationString isEqualToString:@"UIInterfaceOrientationPortrait"]) { [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationPortrait]]; } else if ([orientationString isEqualToString:@"UIInterfaceOrientationPortraitUpsideDown"]) { [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationPortraitUpsideDown]]; } else if ([orientationString isEqualToString:@"UIInterfaceOrientationLandscapeLeft"]) { [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationLandscapeLeft]]; } else if ([orientationString isEqualToString:@"UIInterfaceOrientationLandscapeRight"]) { [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationLandscapeRight]]; } } } // default if ([result count] == 0) { [result addObject:[NSNumber numberWithInt:UIInterfaceOrientationPortrait]]; } return result; } - (void) showSplashScreen { NSString* launchImageFile = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchImageFile"]; if (launchImageFile == nil) { // fallback if no launch image was specified launchImageFile = @"Default"; } NSString* orientedLaunchImageFile = nil; CGAffineTransform startupImageTransform = CGAffineTransformIdentity; UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; CGRect screenBounds = [[UIScreen mainScreen] bounds]; UIInterfaceOrientation statusBarOrientation = [UIApplication sharedApplication].statusBarOrientation; BOOL isIPad = [[self class] isIPad]; UIImage* launchImage = nil; if (isIPad) { if (!UIDeviceOrientationIsValidInterfaceOrientation(deviceOrientation)) { deviceOrientation = (UIDeviceOrientation)statusBarOrientation; } switch (deviceOrientation) { case UIDeviceOrientationLandscapeLeft: // this is where the home button is on the right (yeah, I know, confusing) { orientedLaunchImageFile = [NSString stringWithFormat:@"%@-Landscape", launchImageFile]; startupImageTransform = CGAffineTransformMakeRotation(degreesToRadian(90)); } break; case UIDeviceOrientationLandscapeRight: // this is where the home button is on the left (yeah, I know, confusing) { orientedLaunchImageFile = [NSString stringWithFormat:@"%@-Landscape", launchImageFile]; startupImageTransform = CGAffineTransformMakeRotation(degreesToRadian(-90)); } break; case UIDeviceOrientationPortraitUpsideDown: { orientedLaunchImageFile = [NSString stringWithFormat:@"%@-Portrait", launchImageFile]; startupImageTransform = CGAffineTransformMakeRotation(degreesToRadian(180)); } break; case UIDeviceOrientationPortrait: default: { orientedLaunchImageFile = [NSString stringWithFormat:@"%@-Portrait", launchImageFile]; startupImageTransform = CGAffineTransformIdentity; } break; } launchImage = [UIImage imageNamed:[[self class] resolveImageResource:orientedLaunchImageFile]]; } else // not iPad { orientedLaunchImageFile = @"Default"; launchImage = [UIImage imageNamed:[[self class] resolveImageResource:orientedLaunchImageFile]]; } if (launchImage == nil) { NSLog(@"WARNING: Splash-screen image '%@' was not found. Orientation: %d, iPad: %d", orientedLaunchImageFile, deviceOrientation, isIPad); } self.imageView = [[[UIImageView alloc] initWithImage:launchImage] autorelease]; self.imageView.tag = 1; self.imageView.center = CGPointMake((screenBounds.size.width / 2), (screenBounds.size.height / 2)); self.imageView.autoresizingMask = (UIViewAutoresizingFlexibleWidth & UIViewAutoresizingFlexibleHeight & UIViewAutoresizingFlexibleLeftMargin & UIViewAutoresizingFlexibleRightMargin); [self.imageView setTransform:startupImageTransform]; [self.window addSubview:self.imageView]; /* * The Activity View is the top spinning throbber in the status/battery bar. We init it with the default Grey Style. * * whiteLarge = UIActivityIndicatorViewStyleWhiteLarge * white = UIActivityIndicatorViewStyleWhite * gray = UIActivityIndicatorViewStyleGray * */ NSString *topActivityIndicator = [self.settings objectForKey:@"TopActivityIndicator"]; UIActivityIndicatorViewStyle topActivityIndicatorStyle = UIActivityIndicatorViewStyleGray; if ([topActivityIndicator isEqualToString:@"whiteLarge"]) { topActivityIndicatorStyle = UIActivityIndicatorViewStyleWhiteLarge; } else if ([topActivityIndicator isEqualToString:@"white"]) { topActivityIndicatorStyle = UIActivityIndicatorViewStyleWhite; } else if ([topActivityIndicator isEqualToString:@"gray"]) { topActivityIndicatorStyle = UIActivityIndicatorViewStyleGray; } self.activityView = [[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:topActivityIndicatorStyle] autorelease]; self.activityView.tag = 2; id showSplashScreenSpinnerValue = [self.settings objectForKey:@"ShowSplashScreenSpinner"]; // backwards compatibility - if key is missing, default to true if (showSplashScreenSpinnerValue == nil || [showSplashScreenSpinnerValue boolValue]) { [self.window addSubview:self.activityView]; } self.activityView.center = self.viewController.view.center; [self.activityView startAnimating]; [self.window layoutSubviews];//asking window to do layout AFTER imageView is created refer to line: 250 self.window.autoresizesSubviews = YES; } BOOL gSplashScreenShown = NO; - (void) receivedOrientationChange { if (self.imageView == nil) { gSplashScreenShown = YES; [self showSplashScreen]; } } /** * This is main kick off after the app inits, the views and Settings are setup here. */ // - (void)applicationDidFinishLaunching:(UIApplication *)application - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // read from UISupportedInterfaceOrientations (or UISupportedInterfaceOrientations~iPad, if its iPad) from -Info.plist NSArray* supportedOrientations = [self parseInterfaceOrientations: [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UISupportedInterfaceOrientations"]]; // read from PhoneGap.plist in the app bundle NSString* appPlistName = @"PhoneGap"; NSDictionary* phonegapPlist = [[self class] getBundlePlist:appPlistName]; if (phonegapPlist == nil) { NSLog(@"WARNING: %@.plist is missing.", appPlistName); return NO; } self.settings = [[[NSDictionary alloc] initWithDictionary:phonegapPlist] autorelease]; // read from Plugins dict in PhoneGap.plist in the app bundle NSString* pluginsKey = @"Plugins"; NSDictionary* pluginsDict = [self.settings objectForKey:@"Plugins"]; if (pluginsDict == nil) { NSLog(@"WARNING: %@ key in %@.plist is missing! PhoneGap will not work, you need to have this key.", pluginsKey, appPlistName); return NO; } // set the whitelist self.whitelist = [[[PGWhitelist alloc] initWithArray:[self.settings objectForKey:@"ExternalHosts"]] autorelease]; self.pluginsMap = [pluginsDict dictionaryWithLowercaseKeys]; self.viewController = [[[PhoneGapViewController alloc] init] autorelease]; NSNumber *enableLocation = [self.settings objectForKey:@"EnableLocation"]; NSString *enableViewportScale = [self.settings objectForKey:@"EnableViewportScale"]; NSNumber *allowInlineMediaPlayback = [self.settings objectForKey:@"AllowInlineMediaPlayback"]; NSNumber *mediaPlaybackRequiresUserAction = [self.settings objectForKey:@"MediaPlaybackRequiresUserAction"]; // The first item in the supportedOrientations array is the start orientation (guaranteed to be at least Portrait) [[UIApplication sharedApplication] setStatusBarOrientation:[[supportedOrientations objectAtIndex:0] intValue]]; // Set the supported orientations for rotation. If number of items in the array is > 1, autorotate is supported viewController.supportedOrientations = supportedOrientations; CGRect screenBounds = [ [ UIScreen mainScreen ] bounds ]; self.window = [ [ [ UIWindow alloc ] initWithFrame:screenBounds ] autorelease ]; self.window.autoresizesSubviews = YES; CGRect webViewBounds = [ [ UIScreen mainScreen ] applicationFrame ] ; webViewBounds.origin = screenBounds.origin; if (!self.webView) { self.webView = [[ [ UIWebView alloc ] initWithFrame:webViewBounds] autorelease]; } self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); self.webView.scalesPageToFit = [enableViewportScale boolValue]; viewController.webView = self.webView; [self.viewController.view addSubview:self.webView]; /* * Fire up the GPS Service right away as it takes a moment for data to come back. */ if ([allowInlineMediaPlayback boolValue] && [self.webView respondsToSelector:@selector(allowsInlineMediaPlayback)]) { self.webView.allowsInlineMediaPlayback = YES; } if ([mediaPlaybackRequiresUserAction boolValue] && [self.webView respondsToSelector:@selector(mediaPlaybackRequiresUserAction)]) { self.webView.mediaPlaybackRequiresUserAction = YES; } /* * This is for iOS 4.x, where you can allow inline