diff --git a/.gitignore b/.gitignore index bcede2daa..321ebfbd3 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ bin _site _tmp classes +.forge_settings diff --git a/cordova/android/README.md b/cordova/android/README.md new file mode 100644 index 000000000..934a56a28 --- /dev/null +++ b/cordova/android/README.md @@ -0,0 +1,36 @@ +# What is this? + +This the Cordova-Android project for TicketMonster. + +## Importing and running the project + +Prerequisites +------------- + +* The Android Developer Tools plug-in must be installed in JBDS/Eclipse. +* An Android Virtual Device having a minimum API level 8 must be available. The recommended API level is 17 (Jelly Bean). + +Import the ticket-monster Code +------------------------------ + +First we need to import the existing Android code to JBDS or Eclipse. + +1. On Eclipse, click File then import. +2. Select *Existing Android Code Into Workspace* and click *Next*. +3. On Root Directory, click on *Browse...* button and navigate to the `$TICKET-MONSTER_HOME/cordova/android/TicketMonster` project on your filesystem. +4. After selecting the TicketMonster project, you can click on *Finish* button to start the project import. +5. Make sure that `$TICKET-MONSTER_HOME/cordova/android/TicketMonster/assets/www` is a symbolic link to `../../../demo/src/main/webapp` + +#### Troubleshooting Windows Operating Systems + +As Windows doesn't support symbolic links you must copy `$TICKET-MONSTER_HOME/demo/src/main/webapp` folder to `$TICKET-MONSTER_HOME/cordova/android/TicketMonster/assets/www` + + +Start the Emulator and Deploy the application +-------------------------------------------- + +1. Start the emulator on Eclipse by clicking *Window* and select *AVD Manager*. +2. On Android Virtual Device Manager window, select the appropriate AVD and click on *Start* button. +3. On Launch Options window click on *Launch* button. +4. After Emulator started, select your project on Eclipse +5. Click on *Run*, then *Run As* and *Android Application* diff --git a/cordova/android/TicketMonster/.classpath b/cordova/android/TicketMonster/.classpath index a4763d1ee..f7b8a1f99 100644 --- a/cordova/android/TicketMonster/.classpath +++ b/cordova/android/TicketMonster/.classpath @@ -1,8 +1,9 @@ + + - - + diff --git a/cordova/android/TicketMonster/AndroidManifest.xml b/cordova/android/TicketMonster/AndroidManifest.xml index cf90acd1f..c3aada0d4 100644 --- a/cordova/android/TicketMonster/AndroidManifest.xml +++ b/cordova/android/TicketMonster/AndroidManifest.xml @@ -4,8 +4,8 @@ android:versionName="1.0" > + android:minSdkVersion="8" + android:targetSdkVersion="17" /> +#import "CDVPlugin.h" + +@interface CDVAccelerometer : CDVPlugin +{ + double x; + double y; + double z; + NSTimeInterval timestamp; +} + +@property (readonly, assign) BOOL isRunning; +@property (nonatomic, strong) NSString* callbackId; + +- (CDVAccelerometer*)init; + +- (void)start:(CDVInvokedUrlCommand*)command; +- (void)stop:(CDVInvokedUrlCommand*)command; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVAccelerometer.m b/cordova/ios/CordovaLib/Classes/CDVAccelerometer.m new file mode 100755 index 000000000..33093d0e1 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVAccelerometer.m @@ -0,0 +1,128 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVAccelerometer.h" + +@interface CDVAccelerometer () {} +@property (readwrite, assign) BOOL isRunning; +@end + +@implementation CDVAccelerometer + +@synthesize callbackId, isRunning; + +// defaults to 10 msec +#define kAccelerometerInterval 40 +// g constant: -9.81 m/s^2 +#define kGravitationalConstant -9.81 + +- (CDVAccelerometer*)init +{ + self = [super init]; + if (self) { + x = 0; + y = 0; + z = 0; + timestamp = 0; + self.callbackId = nil; + self.isRunning = NO; + } + return self; +} + +- (void)dealloc +{ + [self stop:nil]; +} + +- (void)start:(CDVInvokedUrlCommand*)command +{ + NSString* cbId = command.callbackId; + NSTimeInterval desiredFrequency_num = kAccelerometerInterval; + UIAccelerometer* pAccel = [UIAccelerometer sharedAccelerometer]; + + // accelerometer expects fractional seconds, but we have msecs + pAccel.updateInterval = desiredFrequency_num / 1000; + self.callbackId = cbId; + if (!self.isRunning) { + pAccel.delegate = self; + self.isRunning = YES; + } +} + +- (void)onReset +{ + [self stop:nil]; +} + +- (void)stop:(CDVInvokedUrlCommand*)command +{ + UIAccelerometer* theAccelerometer = [UIAccelerometer sharedAccelerometer]; + + theAccelerometer.delegate = nil; + self.isRunning = NO; +} + +/** + * Picks up accel updates from device and stores them in this class + */ +- (void)accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration +{ + if (self.isRunning) { + x = acceleration.x; + y = acceleration.y; + z = acceleration.z; + timestamp = ([[NSDate date] timeIntervalSince1970] * 1000); + [self returnAccelInfo]; + } +} + +- (void)returnAccelInfo +{ + // Create an acceleration object + NSMutableDictionary* accelProps = [NSMutableDictionary dictionaryWithCapacity:4]; + + [accelProps setValue:[NSNumber numberWithDouble:x * kGravitationalConstant] forKey:@"x"]; + [accelProps setValue:[NSNumber numberWithDouble:y * kGravitationalConstant] forKey:@"y"]; + [accelProps setValue:[NSNumber numberWithDouble:z * kGravitationalConstant] forKey:@"z"]; + [accelProps setValue:[NSNumber numberWithDouble:timestamp] forKey:@"timestamp"]; + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:accelProps]; + [result setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:result callbackId:self.callbackId]; +} + +// TODO: Consider using filtering to isolate instantaneous data vs. gravity data -jm + +/* + #define kFilteringFactor 0.1 + + // Use a basic low-pass filter to keep only the gravity component of each axis. + grav_accelX = (acceleration.x * kFilteringFactor) + ( grav_accelX * (1.0 - kFilteringFactor)); + grav_accelY = (acceleration.y * kFilteringFactor) + ( grav_accelY * (1.0 - kFilteringFactor)); + grav_accelZ = (acceleration.z * kFilteringFactor) + ( grav_accelZ * (1.0 - kFilteringFactor)); + + // Subtract the low-pass value from the current value to get a simplified high-pass filter + instant_accelX = acceleration.x - ( (acceleration.x * kFilteringFactor) + (instant_accelX * (1.0 - kFilteringFactor)) ); + instant_accelY = acceleration.y - ( (acceleration.y * kFilteringFactor) + (instant_accelY * (1.0 - kFilteringFactor)) ); + instant_accelZ = acceleration.z - ( (acceleration.z * kFilteringFactor) + (instant_accelZ * (1.0 - kFilteringFactor)) ); + + + */ +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVAvailability.h b/cordova/ios/CordovaLib/Classes/CDVAvailability.h new file mode 100755 index 000000000..b288522aa --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVAvailability.h @@ -0,0 +1,88 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#define __CORDOVA_IOS__ + +#define __CORDOVA_0_9_6 906 +#define __CORDOVA_1_0_0 10000 +#define __CORDOVA_1_1_0 10100 +#define __CORDOVA_1_2_0 10200 +#define __CORDOVA_1_3_0 10300 +#define __CORDOVA_1_4_0 10400 +#define __CORDOVA_1_4_1 10401 +#define __CORDOVA_1_5_0 10500 +#define __CORDOVA_1_6_0 10600 +#define __CORDOVA_1_6_1 10601 +#define __CORDOVA_1_7_0 10700 +#define __CORDOVA_1_8_0 10800 +#define __CORDOVA_1_8_1 10801 +#define __CORDOVA_1_9_0 10900 +#define __CORDOVA_2_0_0 20000 +#define __CORDOVA_2_1_0 20100 +#define __CORDOVA_2_2_0 20200 +#define __CORDOVA_2_3_0 20300 +#define __CORDOVA_2_4_0 20400 +#define __CORDOVA_2_5_0 20500 +#define __CORDOVA_2_6_0 20600 +#define __CORDOVA_2_7_0 20700 +#define __CORDOVA_NA 99999 /* not available */ + +/* + #if CORDOVA_VERSION_MIN_REQUIRED >= __CORDOVA_1_7_0 + // do something when its at least 1.7.0 + #else + // do something else (non 1.7.0) + #endif + */ +#ifndef CORDOVA_VERSION_MIN_REQUIRED + #define CORDOVA_VERSION_MIN_REQUIRED __CORDOVA_2_7_0 +#endif + +/* + Returns YES if it is at least version specified as NSString(X) + Usage: + if (IsAtLeastiOSVersion(@"5.1")) { + // do something for iOS 5.1 or greater + } + */ +#define IsAtLeastiOSVersion(X) ([[[UIDevice currentDevice] systemVersion] compare:X options:NSNumericSearch] != NSOrderedAscending) + +#define CDV_IsIPad() ([[UIDevice currentDevice] respondsToSelector:@selector(userInterfaceIdiom)] && ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)) + +#define CDV_IsIPhone5() ([[UIScreen mainScreen] bounds].size.height == 568 && [[UIScreen mainScreen] bounds].size.width == 320) + +/* Return the string version of the decimal version */ +#define CDV_VERSION [NSString stringWithFormat:@"%d.%d.%d", \ + (CORDOVA_VERSION_MIN_REQUIRED / 10000), \ + (CORDOVA_VERSION_MIN_REQUIRED % 10000) / 100, \ + (CORDOVA_VERSION_MIN_REQUIRED % 10000) % 100] + +#ifdef __clang__ + #define CDV_DEPRECATED(version, msg) __attribute__((deprecated("Deprecated in Cordova " #version ". " msg))) +#else + #define CDV_DEPRECATED(version, msg) __attribute__((deprecated())) +#endif + +// Enable this to log all exec() calls. +#define CDV_ENABLE_EXEC_LOGGING 0 +#if CDV_ENABLE_EXEC_LOGGING + #define CDV_EXEC_LOG NSLog +#else + #define CDV_EXEC_LOG(...) do {} while (NO) +#endif diff --git a/cordova/ios/CordovaLib/Classes/CDVBattery.h b/cordova/ios/CordovaLib/Classes/CDVBattery.h new file mode 100755 index 000000000..ba26c3a0a --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVBattery.h @@ -0,0 +1,40 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +@interface CDVBattery : CDVPlugin { + UIDeviceBatteryState state; + float level; + bool isPlugged; + NSString* callbackId; +} + +@property (nonatomic) UIDeviceBatteryState state; +@property (nonatomic) float level; +@property (nonatomic) bool isPlugged; +@property (strong) NSString* callbackId; + +- (void)updateBatteryStatus:(NSNotification*)notification; +- (NSDictionary*)getBatteryStatus; +- (void)start:(CDVInvokedUrlCommand*)command; +- (void)stop:(CDVInvokedUrlCommand*)command; +- (void)dealloc; +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVBattery.m b/cordova/ios/CordovaLib/Classes/CDVBattery.m new file mode 100755 index 000000000..681511c60 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVBattery.m @@ -0,0 +1,152 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVBattery.h" + +@interface CDVBattery (PrivateMethods) +- (void)updateOnlineStatus; +@end + +@implementation CDVBattery + +@synthesize state, level, callbackId, isPlugged; + +/* determining type of event occurs on JavaScript side +- (void) updateBatteryLevel:(NSNotification*)notification +{ + // send batterylow event for less than 25% battery + // send batterycritical event for less than 10% battery + // W3c says to send batteryStatus event when batterylevel changes by more than 1% (iOS seems to notify each 5%) + // always update the navigator.device.battery info + float currentLevel = [[UIDevice currentDevice] batteryLevel]; + NSString* type = @""; + // no check for level == -1 since this api is only called when monitoring is enabled so level should be valid + if (currentLevel < 0.10){ + type = @"batterycritical"; + } else if (currentLevel < 0.25) { + type = @"batterylow"; + } else { + float onePercent = 0.1; + if (self.level >= 0 ){ + onePercent = self.level * 0.01; + } + if (fabsf(currentLevel - self.level) > onePercent){ + // issue batteryStatus event + type = @"batterystatus"; + } + } + // update the battery info and fire event + NSString* jsString = [NSString stringWithFormat:@"navigator.device.battery._status(\"%@\", %@)", type,[[self getBatteryStatus] JSONRepresentation]]; + [super writeJavascript:jsString]; +} + */ + +- (void)updateBatteryStatus:(NSNotification*)notification +{ + NSDictionary* batteryData = [self getBatteryStatus]; + + if (self.callbackId) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:batteryData]; + [result setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:result callbackId:self.callbackId]; + } +} + +/* Get the current battery status and level. Status will be unknown and level will be -1.0 if + * monitoring is turned off. + */ +- (NSDictionary*)getBatteryStatus +{ + UIDevice* currentDevice = [UIDevice currentDevice]; + UIDeviceBatteryState currentState = [currentDevice batteryState]; + + isPlugged = FALSE; // UIDeviceBatteryStateUnknown or UIDeviceBatteryStateUnplugged + if ((currentState == UIDeviceBatteryStateCharging) || (currentState == UIDeviceBatteryStateFull)) { + isPlugged = TRUE; + } + float currentLevel = [currentDevice batteryLevel]; + + if ((currentLevel != self.level) || (currentState != self.state)) { + self.level = currentLevel; + self.state = currentState; + } + + // W3C spec says level must be null if it is unknown + NSObject* w3cLevel = nil; + if ((currentState == UIDeviceBatteryStateUnknown) || (currentLevel == -1.0)) { + w3cLevel = [NSNull null]; + } else { + w3cLevel = [NSNumber numberWithFloat:(currentLevel * 100)]; + } + NSMutableDictionary* batteryData = [NSMutableDictionary dictionaryWithCapacity:2]; + [batteryData setObject:[NSNumber numberWithBool:isPlugged] forKey:@"isPlugged"]; + [batteryData setObject:w3cLevel forKey:@"level"]; + return batteryData; +} + +/* turn on battery monitoring*/ +- (void)start:(CDVInvokedUrlCommand*)command +{ + self.callbackId = command.callbackId; + + if ([UIDevice currentDevice].batteryMonitoringEnabled == NO) { + [[UIDevice currentDevice] setBatteryMonitoringEnabled:YES]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateBatteryStatus:) + name:UIDeviceBatteryStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateBatteryStatus:) + name:UIDeviceBatteryLevelDidChangeNotification object:nil]; + } +} + +/* turn off battery monitoring */ +- (void)stop:(CDVInvokedUrlCommand*)command +{ + // callback one last time to clear the callback function on JS side + if (self.callbackId) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getBatteryStatus]]; + [result setKeepCallbackAsBool:NO]; + [self.commandDelegate sendPluginResult:result callbackId:self.callbackId]; + } + self.callbackId = nil; + [[UIDevice currentDevice] setBatteryMonitoringEnabled:NO]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceBatteryStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceBatteryLevelDidChangeNotification object:nil]; +} + +- (CDVPlugin*)initWithWebView:(UIWebView*)theWebView +{ + self = (CDVBattery*)[super initWithWebView:theWebView]; + if (self) { + self.state = UIDeviceBatteryStateUnknown; + self.level = -1.0; + } + return self; +} + +- (void)dealloc +{ + [self stop:nil]; +} + +- (void)onReset +{ + [self stop:nil]; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCamera.h b/cordova/ios/CordovaLib/Classes/CDVCamera.h new file mode 100755 index 000000000..65eac77d2 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCamera.h @@ -0,0 +1,92 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +enum CDVDestinationType { + DestinationTypeDataUrl = 0, + DestinationTypeFileUri, + DestinationTypeNativeUri +}; +typedef NSUInteger CDVDestinationType; + +enum CDVEncodingType { + EncodingTypeJPEG = 0, + EncodingTypePNG +}; +typedef NSUInteger CDVEncodingType; + +enum CDVMediaType { + MediaTypePicture = 0, + MediaTypeVideo, + MediaTypeAll +}; +typedef NSUInteger CDVMediaType; + +@interface CDVCameraPicker : UIImagePickerController +{} + +@property (assign) NSInteger quality; +@property (copy) NSString* callbackId; +@property (copy) NSString* postUrl; +@property (nonatomic) enum CDVDestinationType returnType; +@property (nonatomic) enum CDVEncodingType encodingType; +@property (strong) UIPopoverController* popoverController; +@property (assign) CGSize targetSize; +@property (assign) bool correctOrientation; +@property (assign) bool saveToPhotoAlbum; +@property (assign) bool cropToSize; +@property (strong) UIWebView* webView; +@property (assign) BOOL popoverSupported; + +@end + +// ======================================================================= // + +@interface CDVCamera : CDVPlugin +{} + +@property (strong) CDVCameraPicker* pickerController; + +/* + * getPicture + * + * arguments: + * 1: this is the javascript function that will be called with the results, the first parameter passed to the + * javascript function is the picture as a Base64 encoded string + * 2: this is the javascript function to be called if there was an error + * options: + * quality: integer between 1 and 100 + */ +- (void)takePicture:(CDVInvokedUrlCommand*)command; +- (void)postImage:(UIImage*)anImage withFilename:(NSString*)filename toUrl:(NSURL*)url; +- (void)cleanup:(CDVInvokedUrlCommand*)command; +- (void)repositionPopover:(CDVInvokedUrlCommand*)command; + +- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info; +- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo; +- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker; +- (UIImage*)imageByScalingAndCroppingForSize:(UIImage*)anImage toSize:(CGSize)targetSize; +- (UIImage*)imageByScalingNotCroppingForSize:(UIImage*)anImage toSize:(CGSize)frameSize; +- (UIImage*)imageCorrectedForCaptureOrientation:(UIImage*)anImage; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCamera.m b/cordova/ios/CordovaLib/Classes/CDVCamera.m new file mode 100755 index 000000000..823fde9cb --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCamera.m @@ -0,0 +1,570 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVCamera.h" +#import "CDVJpegHeaderWriter.h" +#import "NSArray+Comparisons.h" +#import "NSData+Base64.h" +#import "NSDictionary+Extensions.h" +#import + +#define CDV_PHOTO_PREFIX @"cdv_photo_" + +static NSSet* org_apache_cordova_validArrowDirections; + +@interface CDVCamera () + +@property (readwrite, assign) BOOL hasPendingOperation; + +@end + +@implementation CDVCamera + ++ (void)initialize +{ + org_apache_cordova_validArrowDirections = [[NSSet alloc] initWithObjects:[NSNumber numberWithInt:UIPopoverArrowDirectionUp], [NSNumber numberWithInt:UIPopoverArrowDirectionDown], [NSNumber numberWithInt:UIPopoverArrowDirectionLeft], [NSNumber numberWithInt:UIPopoverArrowDirectionRight], [NSNumber numberWithInt:UIPopoverArrowDirectionAny], nil]; +} + +@synthesize hasPendingOperation, pickerController; + +- (BOOL)popoverSupported +{ + return (NSClassFromString(@"UIPopoverController") != nil) && + (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad); +} + +/* takePicture arguments: + * INDEX ARGUMENT + * 0 quality + * 1 destination type + * 2 source type + * 3 targetWidth + * 4 targetHeight + * 5 encodingType + * 6 mediaType + * 7 allowsEdit + * 8 correctOrientation + * 9 saveToPhotoAlbum + * 10 popoverOptions + * 11 cameraDirection + */ +- (void)takePicture:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSArray* arguments = command.arguments; + + self.hasPendingOperation = NO; + + NSString* sourceTypeString = [arguments objectAtIndex:2]; + UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypeCamera; // default + if (sourceTypeString != nil) { + sourceType = (UIImagePickerControllerSourceType)[sourceTypeString intValue]; + } + + bool hasCamera = [UIImagePickerController isSourceTypeAvailable:sourceType]; + if (!hasCamera) { + NSLog(@"Camera.getPicture: source type %d not available.", sourceType); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no camera available"]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + return; + } + + bool allowEdit = [[arguments objectAtIndex:7] boolValue]; + NSNumber* targetWidth = [arguments objectAtIndex:3]; + NSNumber* targetHeight = [arguments objectAtIndex:4]; + NSNumber* mediaValue = [arguments objectAtIndex:6]; + CDVMediaType mediaType = (mediaValue) ? [mediaValue intValue] : MediaTypePicture; + + CGSize targetSize = CGSizeMake(0, 0); + if ((targetWidth != nil) && (targetHeight != nil)) { + targetSize = CGSizeMake([targetWidth floatValue], [targetHeight floatValue]); + } + + // If a popover is already open, close it; we only want one at a time. + if (([[self pickerController] popoverController] != nil) && [[[self pickerController] popoverController] isPopoverVisible]) { + [[[self pickerController] popoverController] dismissPopoverAnimated:YES]; + [[[self pickerController] popoverController] setDelegate:nil]; + [[self pickerController] setPopoverController:nil]; + } + + CDVCameraPicker* cameraPicker = [[CDVCameraPicker alloc] init]; + self.pickerController = cameraPicker; + + cameraPicker.delegate = self; + cameraPicker.sourceType = sourceType; + cameraPicker.allowsEditing = allowEdit; // THIS IS ALL IT TAKES FOR CROPPING - jm + cameraPicker.callbackId = callbackId; + cameraPicker.targetSize = targetSize; + cameraPicker.cropToSize = NO; + // we need to capture this state for memory warnings that dealloc this object + cameraPicker.webView = self.webView; + cameraPicker.popoverSupported = [self popoverSupported]; + + cameraPicker.correctOrientation = [[arguments objectAtIndex:8] boolValue]; + cameraPicker.saveToPhotoAlbum = [[arguments objectAtIndex:9] boolValue]; + + cameraPicker.encodingType = ([arguments objectAtIndex:5]) ? [[arguments objectAtIndex:5] intValue] : EncodingTypeJPEG; + + cameraPicker.quality = ([arguments objectAtIndex:0]) ? [[arguments objectAtIndex:0] intValue] : 50; + cameraPicker.returnType = ([arguments objectAtIndex:1]) ? [[arguments objectAtIndex:1] intValue] : DestinationTypeFileUri; + + if (sourceType == UIImagePickerControllerSourceTypeCamera) { + // We only allow taking pictures (no video) in this API. + cameraPicker.mediaTypes = [NSArray arrayWithObjects:(NSString*)kUTTypeImage, nil]; + + // We can only set the camera device if we're actually using the camera. + NSNumber* cameraDirection = [command argumentAtIndex:11 withDefault:[NSNumber numberWithInteger:UIImagePickerControllerCameraDeviceRear]]; + cameraPicker.cameraDevice = (UIImagePickerControllerCameraDevice)[cameraDirection intValue]; + } else if (mediaType == MediaTypeAll) { + cameraPicker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:sourceType]; + } else { + NSArray* mediaArray = [NSArray arrayWithObjects:(NSString*)(mediaType == MediaTypeVideo ? kUTTypeMovie : kUTTypeImage), nil]; + cameraPicker.mediaTypes = mediaArray; + } + + if ([self popoverSupported] && (sourceType != UIImagePickerControllerSourceTypeCamera)) { + if (cameraPicker.popoverController == nil) { + cameraPicker.popoverController = [[NSClassFromString(@"UIPopoverController")alloc] initWithContentViewController:cameraPicker]; + } + NSDictionary* options = [command.arguments objectAtIndex:10 withDefault:nil]; + [self displayPopover:options]; + } else { + if ([self.viewController respondsToSelector:@selector(presentViewController:::)]) { + [self.viewController presentViewController:cameraPicker animated:YES completion:nil]; + } else { + [self.viewController presentModalViewController:cameraPicker animated:YES]; + } + } + self.hasPendingOperation = YES; +} + +- (void)repositionPopover:(CDVInvokedUrlCommand*)command +{ + NSDictionary* options = [command.arguments objectAtIndex:0 withDefault:nil]; + + [self displayPopover:options]; +} + +- (void)displayPopover:(NSDictionary*)options +{ + int x = 0; + int y = 32; + int width = 320; + int height = 480; + UIPopoverArrowDirection arrowDirection = UIPopoverArrowDirectionAny; + + if (options) { + x = [options integerValueForKey:@"x" defaultValue:0]; + y = [options integerValueForKey:@"y" defaultValue:32]; + width = [options integerValueForKey:@"width" defaultValue:320]; + height = [options integerValueForKey:@"height" defaultValue:480]; + arrowDirection = [options integerValueForKey:@"arrowDir" defaultValue:UIPopoverArrowDirectionAny]; + if (![org_apache_cordova_validArrowDirections containsObject:[NSNumber numberWithInt:arrowDirection]]) { + arrowDirection = UIPopoverArrowDirectionAny; + } + } + + [[[self pickerController] popoverController] setDelegate:self]; + [[[self pickerController] popoverController] presentPopoverFromRect:CGRectMake(x, y, width, height) + inView:[self.webView superview] + permittedArrowDirections:arrowDirection + animated:YES]; +} + +- (void)cleanup:(CDVInvokedUrlCommand*)command +{ + // empty the tmp directory + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSError* err = nil; + BOOL hasErrors = NO; + + // clear contents of NSTemporaryDirectory + NSString* tempDirectoryPath = NSTemporaryDirectory(); + NSDirectoryEnumerator* directoryEnumerator = [fileMgr enumeratorAtPath:tempDirectoryPath]; + NSString* fileName = nil; + BOOL result; + + while ((fileName = [directoryEnumerator nextObject])) { + // only delete the files we created + if (![fileName hasPrefix:CDV_PHOTO_PREFIX]) { + continue; + } + NSString* filePath = [tempDirectoryPath stringByAppendingPathComponent:fileName]; + result = [fileMgr removeItemAtPath:filePath error:&err]; + if (!result && err) { + NSLog(@"Failed to delete: %@ (error: %@)", filePath, err); + hasErrors = YES; + } + } + + CDVPluginResult* pluginResult; + if (hasErrors) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:@"One or more files failed to be deleted."]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)popoverControllerDidDismissPopover:(id)popoverController +{ + // [ self imagePickerControllerDidCancel:self.pickerController ]; ' + UIPopoverController* pc = (UIPopoverController*)popoverController; + + [pc dismissPopoverAnimated:YES]; + pc.delegate = nil; + if (self.pickerController && self.pickerController.callbackId && self.pickerController.popoverController) { + self.pickerController.popoverController = nil; + NSString* callbackId = self.pickerController.callbackId; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no image selected"]; // error callback expects string ATM + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + self.hasPendingOperation = NO; +} + +- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info +{ + CDVCameraPicker* cameraPicker = (CDVCameraPicker*)picker; + + if (cameraPicker.popoverSupported && (cameraPicker.popoverController != nil)) { + [cameraPicker.popoverController dismissPopoverAnimated:YES]; + cameraPicker.popoverController.delegate = nil; + cameraPicker.popoverController = nil; + } else { + if ([cameraPicker respondsToSelector:@selector(presentingViewController)]) { + [[cameraPicker presentingViewController] dismissModalViewControllerAnimated:YES]; + } else { + [[cameraPicker parentViewController] dismissModalViewControllerAnimated:YES]; + } + } + + CDVPluginResult* result = nil; + + NSString* mediaType = [info objectForKey:UIImagePickerControllerMediaType]; + // IMAGE TYPE + if ([mediaType isEqualToString:(NSString*)kUTTypeImage]) { + if (cameraPicker.returnType == DestinationTypeNativeUri) { + NSString* nativeUri = [(NSURL*)[info objectForKey:UIImagePickerControllerReferenceURL] absoluteString]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:nativeUri]; + } else { + // get the image + UIImage* image = nil; + if (cameraPicker.allowsEditing && [info objectForKey:UIImagePickerControllerEditedImage]) { + image = [info objectForKey:UIImagePickerControllerEditedImage]; + } else { + image = [info objectForKey:UIImagePickerControllerOriginalImage]; + } + + if (cameraPicker.saveToPhotoAlbum) { + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); + } + + if (cameraPicker.correctOrientation) { + image = [self imageCorrectedForCaptureOrientation:image]; + } + + UIImage* scaledImage = nil; + + if ((cameraPicker.targetSize.width > 0) && (cameraPicker.targetSize.height > 0)) { + // if cropToSize, resize image and crop to target size, otherwise resize to fit target without cropping + if (cameraPicker.cropToSize) { + scaledImage = [self imageByScalingAndCroppingForSize:image toSize:cameraPicker.targetSize]; + } else { + scaledImage = [self imageByScalingNotCroppingForSize:image toSize:cameraPicker.targetSize]; + } + } + + NSData* data = nil; + + if (cameraPicker.encodingType == EncodingTypePNG) { + data = UIImagePNGRepresentation(scaledImage == nil ? image : scaledImage); + } else { + data = UIImageJPEGRepresentation(scaledImage == nil ? image : scaledImage, cameraPicker.quality / 100.0f); + + /* splice loc */ + CDVJpegHeaderWriter* exifWriter = [[CDVJpegHeaderWriter alloc] init]; + NSString* headerstring = [exifWriter createExifAPP1:[info objectForKey:@"UIImagePickerControllerMediaMetadata"]]; + data = [exifWriter spliceExifBlockIntoJpeg:data withExifBlock:headerstring]; + } + + if (cameraPicker.returnType == DestinationTypeFileUri) { + // write to temp directory and return URI + // get the temp directory path + NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; + NSError* err = nil; + NSFileManager* fileMgr = [[NSFileManager alloc] init]; // recommended by apple (vs [NSFileManager defaultManager]) to be threadsafe + // generate unique file name + NSString* filePath; + + int i = 1; + do { + filePath = [NSString stringWithFormat:@"%@/%@%03d.%@", docsPath, CDV_PHOTO_PREFIX, i++, cameraPicker.encodingType == EncodingTypePNG ? @"png":@"jpg"]; + } while ([fileMgr fileExistsAtPath:filePath]); + + // save file + if (![data writeToFile:filePath options:NSAtomicWrite error:&err]) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err localizedDescription]]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:[[NSURL fileURLWithPath:filePath] absoluteString]]; + } + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:[data base64EncodedString]]; + } + } + } + // NOT IMAGE TYPE (MOVIE) + else { + NSString* moviePath = [[info objectForKey:UIImagePickerControllerMediaURL] absoluteString]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:moviePath]; + } + + if (result) { + [self.commandDelegate sendPluginResult:result callbackId:cameraPicker.callbackId]; + } + + self.hasPendingOperation = NO; + self.pickerController = nil; +} + +// older api calls newer didFinishPickingMediaWithInfo +- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo +{ + NSDictionary* imageInfo = [NSDictionary dictionaryWithObject:image forKey:UIImagePickerControllerOriginalImage]; + + [self imagePickerController:picker didFinishPickingMediaWithInfo:imageInfo]; +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker +{ + CDVCameraPicker* cameraPicker = (CDVCameraPicker*)picker; + + if ([cameraPicker respondsToSelector:@selector(presentingViewController)]) { + [[cameraPicker presentingViewController] dismissModalViewControllerAnimated:YES]; + } else { + [[cameraPicker parentViewController] dismissModalViewControllerAnimated:YES]; + } + // popoverControllerDidDismissPopover:(id)popoverController is called if popover is cancelled + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no image selected"]; // error callback expects string ATM + [self.commandDelegate sendPluginResult:result callbackId:cameraPicker.callbackId]; + + self.hasPendingOperation = NO; + self.pickerController = nil; +} + +- (UIImage*)imageByScalingAndCroppingForSize:(UIImage*)anImage toSize:(CGSize)targetSize +{ + UIImage* sourceImage = anImage; + UIImage* newImage = nil; + CGSize imageSize = sourceImage.size; + CGFloat width = imageSize.width; + CGFloat height = imageSize.height; + CGFloat targetWidth = targetSize.width; + CGFloat targetHeight = targetSize.height; + CGFloat scaleFactor = 0.0; + CGFloat scaledWidth = targetWidth; + CGFloat scaledHeight = targetHeight; + CGPoint thumbnailPoint = CGPointMake(0.0, 0.0); + + if (CGSizeEqualToSize(imageSize, targetSize) == NO) { + CGFloat widthFactor = targetWidth / width; + CGFloat heightFactor = targetHeight / height; + + if (widthFactor > heightFactor) { + scaleFactor = widthFactor; // scale to fit height + } else { + scaleFactor = heightFactor; // scale to fit width + } + scaledWidth = width * scaleFactor; + scaledHeight = height * scaleFactor; + + // center the image + if (widthFactor > heightFactor) { + thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5; + } else if (widthFactor < heightFactor) { + thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5; + } + } + + UIGraphicsBeginImageContext(targetSize); // this will crop + + CGRect thumbnailRect = CGRectZero; + thumbnailRect.origin = thumbnailPoint; + thumbnailRect.size.width = scaledWidth; + thumbnailRect.size.height = scaledHeight; + + [sourceImage drawInRect:thumbnailRect]; + + newImage = UIGraphicsGetImageFromCurrentImageContext(); + if (newImage == nil) { + NSLog(@"could not scale image"); + } + + // pop the context to get back to the default + UIGraphicsEndImageContext(); + return newImage; +} + +- (UIImage*)imageCorrectedForCaptureOrientation:(UIImage*)anImage +{ + float rotation_radians = 0; + bool perpendicular = false; + + switch ([anImage imageOrientation]) { + case UIImageOrientationUp : + rotation_radians = 0.0; + break; + + case UIImageOrientationDown: + rotation_radians = M_PI; // don't be scared of radians, if you're reading this, you're good at math + break; + + case UIImageOrientationRight: + rotation_radians = M_PI_2; + perpendicular = true; + break; + + case UIImageOrientationLeft: + rotation_radians = -M_PI_2; + perpendicular = true; + break; + + default: + break; + } + + UIGraphicsBeginImageContext(CGSizeMake(anImage.size.width, anImage.size.height)); + CGContextRef context = UIGraphicsGetCurrentContext(); + + // Rotate around the center point + CGContextTranslateCTM(context, anImage.size.width / 2, anImage.size.height / 2); + CGContextRotateCTM(context, rotation_radians); + + CGContextScaleCTM(context, 1.0, -1.0); + float width = perpendicular ? anImage.size.height : anImage.size.width; + float height = perpendicular ? anImage.size.width : anImage.size.height; + CGContextDrawImage(context, CGRectMake(-width / 2, -height / 2, width, height), [anImage CGImage]); + + // Move the origin back since the rotation might've change it (if its 90 degrees) + if (perpendicular) { + CGContextTranslateCTM(context, -anImage.size.height / 2, -anImage.size.width / 2); + } + + UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return newImage; +} + +- (UIImage*)imageByScalingNotCroppingForSize:(UIImage*)anImage toSize:(CGSize)frameSize +{ + UIImage* sourceImage = anImage; + UIImage* newImage = nil; + CGSize imageSize = sourceImage.size; + CGFloat width = imageSize.width; + CGFloat height = imageSize.height; + CGFloat targetWidth = frameSize.width; + CGFloat targetHeight = frameSize.height; + CGFloat scaleFactor = 0.0; + CGSize scaledSize = frameSize; + + if (CGSizeEqualToSize(imageSize, frameSize) == NO) { + CGFloat widthFactor = targetWidth / width; + CGFloat heightFactor = targetHeight / height; + + // opposite comparison to imageByScalingAndCroppingForSize in order to contain the image within the given bounds + if (widthFactor > heightFactor) { + scaleFactor = heightFactor; // scale to fit height + } else { + scaleFactor = widthFactor; // scale to fit width + } + scaledSize = CGSizeMake(MIN(width * scaleFactor, targetWidth), MIN(height * scaleFactor, targetHeight)); + } + + UIGraphicsBeginImageContext(scaledSize); // this will resize + + [sourceImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)]; + + newImage = UIGraphicsGetImageFromCurrentImageContext(); + if (newImage == nil) { + NSLog(@"could not scale image"); + } + + // pop the context to get back to the default + UIGraphicsEndImageContext(); + return newImage; +} + +- (void)postImage:(UIImage*)anImage withFilename:(NSString*)filename toUrl:(NSURL*)url +{ + self.hasPendingOperation = YES; + + NSString* boundary = @"----BOUNDARY_IS_I"; + + NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url]; + [req setHTTPMethod:@"POST"]; + + NSString* contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; + [req setValue:contentType forHTTPHeaderField:@"Content-type"]; + + NSData* imageData = UIImagePNGRepresentation(anImage); + + // adding the body + NSMutableData* postBody = [NSMutableData data]; + + // first parameter an image + [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"upload\"; filename=\"%@\"\r\n", filename] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[@"Content-Type: image/png\r\n\r\n" dataUsingEncoding : NSUTF8StringEncoding]]; + [postBody appendData:imageData]; + + // // second parameter information + // [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + // [postBody appendData:[@"Content-Disposition: form-data; name=\"some_other_name\"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + // [postBody appendData:[@"some_other_value" dataUsingEncoding:NSUTF8StringEncoding]]; + // [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r \n",boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + [req setHTTPBody:postBody]; + + NSURLResponse* response; + NSError* error; + [NSURLConnection sendSynchronousRequest:req returningResponse:&response error:&error]; + + // NSData* result = [NSURLConnection sendSynchronousRequest:req returningResponse:&response error:&error]; + // NSString * resultStr = [[[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding] autorelease]; + + self.hasPendingOperation = NO; +} + +@end + +@implementation CDVCameraPicker + +@synthesize quality, postUrl; +@synthesize returnType; +@synthesize callbackId; +@synthesize popoverController; +@synthesize targetSize; +@synthesize correctOrientation; +@synthesize saveToPhotoAlbum; +@synthesize encodingType; +@synthesize cropToSize; +@synthesize webView; +@synthesize popoverSupported; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCapture.h b/cordova/ios/CordovaLib/Classes/CDVCapture.h new file mode 100755 index 000000000..afb82b49d --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCapture.h @@ -0,0 +1,118 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import "CDVPlugin.h" +#import "CDVFile.h" + +enum CDVCaptureError { + CAPTURE_INTERNAL_ERR = 0, + CAPTURE_APPLICATION_BUSY = 1, + CAPTURE_INVALID_ARGUMENT = 2, + CAPTURE_NO_MEDIA_FILES = 3, + CAPTURE_NOT_SUPPORTED = 20 +}; +typedef NSUInteger CDVCaptureError; + +@interface CDVImagePicker : UIImagePickerController +{ + NSString* callbackid; + NSInteger quality; + NSString* mimeType; +} +@property (assign) NSInteger quality; +@property (copy) NSString* callbackId; +@property (copy) NSString* mimeType; + +@end + +@interface CDVCapture : CDVPlugin +{ + CDVImagePicker* pickerController; + BOOL inUse; +} +@property BOOL inUse; +- (void)captureAudio:(CDVInvokedUrlCommand*)command; +- (void)captureImage:(CDVInvokedUrlCommand*)command; +- (CDVPluginResult*)processImage:(UIImage*)image type:(NSString*)mimeType forCallbackId:(NSString*)callbackId; +- (void)captureVideo:(CDVInvokedUrlCommand*)command; +- (CDVPluginResult*)processVideo:(NSString*)moviePath forCallbackId:(NSString*)callbackId; +- (void)getMediaModes:(CDVInvokedUrlCommand*)command; +- (void)getFormatData:(CDVInvokedUrlCommand*)command; +- (NSDictionary*)getMediaDictionaryFromPath:(NSString*)fullPath ofType:(NSString*)type; +- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info; +- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo; +- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker; + +@end + +@interface CDVAudioNavigationController : UINavigationController + +@end + +/* AudioRecorderViewController is used to create a simple view for audio recording. + * It is created from [Capture captureAudio]. It creates a very simple interface for + * recording by presenting just a record/stop button and a Done button to close the view. + * The recording time is displayed and recording for a specified duration is supported. When duration + * is specified there is no UI to the user - recording just stops when the specified + * duration is reached. The UI has been minimized to avoid localization. + */ +@interface CDVAudioRecorderViewController : UIViewController +{ + CDVCaptureError errorCode; + NSString* callbackId; + NSNumber* duration; + CDVCapture* captureCommand; + UIBarButtonItem* doneButton; + UIView* recordingView; + UIButton* recordButton; + UIImage* recordImage; + UIImage* stopRecordImage; + UILabel* timerLabel; + AVAudioRecorder* avRecorder; + AVAudioSession* avSession; + CDVPluginResult* pluginResult; + NSTimer* timer; + BOOL isTimed; +} +@property (nonatomic) CDVCaptureError errorCode; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSNumber* duration; +@property (nonatomic, strong) CDVCapture* captureCommand; +@property (nonatomic, strong) UIBarButtonItem* doneButton; +@property (nonatomic, strong) UIView* recordingView; +@property (nonatomic, strong) UIButton* recordButton; +@property (nonatomic, strong) UIImage* recordImage; +@property (nonatomic, strong) UIImage* stopRecordImage; +@property (nonatomic, strong) UILabel* timerLabel; +@property (nonatomic, strong) AVAudioRecorder* avRecorder; +@property (nonatomic, strong) AVAudioSession* avSession; +@property (nonatomic, strong) CDVPluginResult* pluginResult; +@property (nonatomic, strong) NSTimer* timer; +@property (nonatomic) BOOL isTimed; + +- (id)initWithCommand:(CDVPlugin*)theCommand duration:(NSNumber*)theDuration callbackId:(NSString*)theCallbackId; +- (void)processButton:(id)sender; +- (void)stopRecordingCleanup; +- (void)dismissAudioView:(id)sender; +- (NSString*)formatTime:(int)interval; +- (void)updateTime; +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCapture.m b/cordova/ios/CordovaLib/Classes/CDVCapture.m new file mode 100755 index 000000000..d89e3d389 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCapture.m @@ -0,0 +1,847 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVCapture.h" +#import "CDVJSON.h" +#import "CDVAvailability.h" + +#define kW3CMediaFormatHeight @"height" +#define kW3CMediaFormatWidth @"width" +#define kW3CMediaFormatCodecs @"codecs" +#define kW3CMediaFormatBitrate @"bitrate" +#define kW3CMediaFormatDuration @"duration" +#define kW3CMediaModeType @"type" + +@implementation CDVImagePicker + +@synthesize quality; +@synthesize callbackId; +@synthesize mimeType; + +- (uint64_t)accessibilityTraits +{ + NSString* systemVersion = [[UIDevice currentDevice] systemVersion]; + + if (([systemVersion compare:@"4.0" options:NSNumericSearch] != NSOrderedAscending)) { // this means system version is not less than 4.0 + return UIAccessibilityTraitStartsMediaSession; + } + + return UIAccessibilityTraitNone; +} + +@end + +@implementation CDVCapture +@synthesize inUse; + +- (id)initWithWebView:(UIWebView*)theWebView +{ + self = (CDVCapture*)[super initWithWebView:theWebView]; + if (self) { + self.inUse = NO; + } + return self; +} + +- (void)captureAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSDictionary* options = [command.arguments objectAtIndex:0]; + + if ([options isKindOfClass:[NSNull class]]) { + options = [NSDictionary dictionary]; + } + + NSNumber* duration = [options objectForKey:@"duration"]; + // the default value of duration is 0 so use nil (no duration) if default value + if (duration) { + duration = [duration doubleValue] == 0 ? nil : duration; + } + CDVPluginResult* result = nil; + + if (NSClassFromString(@"AVAudioRecorder") == nil) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED]; + } else if (self.inUse == YES) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_APPLICATION_BUSY]; + } else { + // all the work occurs here + CDVAudioRecorderViewController* audioViewController = [[CDVAudioRecorderViewController alloc] initWithCommand:self duration:duration callbackId:callbackId]; + + // Now create a nav controller and display the view... + CDVAudioNavigationController* navController = [[CDVAudioNavigationController alloc] initWithRootViewController:audioViewController]; + + self.inUse = YES; + + if ([self.viewController respondsToSelector:@selector(presentViewController:::)]) { + [self.viewController presentViewController:navController animated:YES completion:nil]; + } else { + [self.viewController presentModalViewController:navController animated:YES]; + } + } + + if (result) { + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (void)captureImage:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSDictionary* options = [command.arguments objectAtIndex:0]; + + if ([options isKindOfClass:[NSNull class]]) { + options = [NSDictionary dictionary]; + } + NSString* mode = [options objectForKey:@"mode"]; + + // options could contain limit and mode neither of which are supported at this time + // taking more than one picture (limit) is only supported if provide own controls via cameraOverlayView property + // can support mode in OS + + if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + NSLog(@"Capture.imageCapture: camera not available."); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + if (pickerController == nil) { + pickerController = [[CDVImagePicker alloc] init]; + } + + pickerController.delegate = self; + pickerController.sourceType = UIImagePickerControllerSourceTypeCamera; + pickerController.allowsEditing = NO; + if ([pickerController respondsToSelector:@selector(mediaTypes)]) { + // iOS 3.0 + pickerController.mediaTypes = [NSArray arrayWithObjects:(NSString*)kUTTypeImage, nil]; + } + + /*if ([pickerController respondsToSelector:@selector(cameraCaptureMode)]){ + // iOS 4.0 + pickerController.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto; + pickerController.cameraDevice = UIImagePickerControllerCameraDeviceRear; + pickerController.cameraFlashMode = UIImagePickerControllerCameraFlashModeAuto; + }*/ + // CDVImagePicker specific property + pickerController.callbackId = callbackId; + pickerController.mimeType = mode; + + if ([self.viewController respondsToSelector:@selector(presentViewController:::)]) { + [self.viewController presentViewController:pickerController animated:YES completion:nil]; + } else { + [self.viewController presentModalViewController:pickerController animated:YES]; + } + } +} + +/* Process a still image from the camera. + * IN: + * UIImage* image - the UIImage data returned from the camera + * NSString* callbackId + */ +- (CDVPluginResult*)processImage:(UIImage*)image type:(NSString*)mimeType forCallbackId:(NSString*)callbackId +{ + CDVPluginResult* result = nil; + + // save the image to photo album + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); + + NSData* data = nil; + if (mimeType && [mimeType isEqualToString:@"image/png"]) { + data = UIImagePNGRepresentation(image); + } else { + data = UIImageJPEGRepresentation(image, 0.5); + } + + // write to temp directory and return URI + NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; // use file system temporary directory + NSError* err = nil; + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + + // generate unique file name + NSString* filePath; + int i = 1; + do { + filePath = [NSString stringWithFormat:@"%@/photo_%03d.jpg", docsPath, i++]; + } while ([fileMgr fileExistsAtPath:filePath]); + + if (![data writeToFile:filePath options:NSAtomicWrite error:&err]) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR]; + if (err) { + NSLog(@"Error saving image: %@", [err localizedDescription]); + } + } else { + // create MediaFile object + + NSDictionary* fileDict = [self getMediaDictionaryFromPath:filePath ofType:mimeType]; + NSArray* fileArray = [NSArray arrayWithObject:fileDict]; + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray]; + } + + return result; +} + +- (void)captureVideo:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSDictionary* options = [command.arguments objectAtIndex:0]; + + if ([options isKindOfClass:[NSNull class]]) { + options = [NSDictionary dictionary]; + } + + // options could contain limit, duration and mode, only duration is supported (but is not due to apple bug) + // taking more than one video (limit) is only supported if provide own controls via cameraOverlayView property + // NSNumber* duration = [options objectForKey:@"duration"]; + NSString* mediaType = nil; + + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + // there is a camera, it is available, make sure it can do movies + pickerController = [[CDVImagePicker alloc] init]; + + NSArray* types = nil; + if ([UIImagePickerController respondsToSelector:@selector(availableMediaTypesForSourceType:)]) { + types = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; + // NSLog(@"MediaTypes: %@", [types description]); + + if ([types containsObject:(NSString*)kUTTypeMovie]) { + mediaType = (NSString*)kUTTypeMovie; + } else if ([types containsObject:(NSString*)kUTTypeVideo]) { + mediaType = (NSString*)kUTTypeVideo; + } + } + } + if (!mediaType) { + // don't have video camera return error + NSLog(@"Capture.captureVideo: video mode not available."); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + pickerController = nil; + } else { + pickerController.delegate = self; + pickerController.sourceType = UIImagePickerControllerSourceTypeCamera; + pickerController.allowsEditing = NO; + // iOS 3.0 + pickerController.mediaTypes = [NSArray arrayWithObjects:mediaType, nil]; + + /*if ([mediaType isEqualToString:(NSString*)kUTTypeMovie]){ + if (duration) { + pickerController.videoMaximumDuration = [duration doubleValue]; + } + //NSLog(@"pickerController.videoMaximumDuration = %f", pickerController.videoMaximumDuration); + }*/ + + // iOS 4.0 + if ([pickerController respondsToSelector:@selector(cameraCaptureMode)]) { + pickerController.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo; + // pickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; + // pickerController.cameraDevice = UIImagePickerControllerCameraDeviceRear; + // pickerController.cameraFlashMode = UIImagePickerControllerCameraFlashModeAuto; + } + // CDVImagePicker specific property + pickerController.callbackId = callbackId; + + if ([self.viewController respondsToSelector:@selector(presentViewController:::)]) { + [self.viewController presentViewController:pickerController animated:YES completion:nil]; + } else { + [self.viewController presentModalViewController:pickerController animated:YES]; + } + } +} + +- (CDVPluginResult*)processVideo:(NSString*)moviePath forCallbackId:(NSString*)callbackId +{ + // save the movie to photo album (only avail as of iOS 3.1) + + /* don't need, it should automatically get saved + NSLog(@"can save %@: %d ?", moviePath, UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(moviePath)); + if (&UIVideoAtPathIsCompatibleWithSavedPhotosAlbum != NULL && UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(moviePath) == YES) { + NSLog(@"try to save movie"); + UISaveVideoAtPathToSavedPhotosAlbum(moviePath, nil, nil, nil); + NSLog(@"finished saving movie"); + }*/ + // create MediaFile object + NSDictionary* fileDict = [self getMediaDictionaryFromPath:moviePath ofType:nil]; + NSArray* fileArray = [NSArray arrayWithObject:fileDict]; + + return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray]; +} + +- (void)getMediaModes:(CDVInvokedUrlCommand*)command +{ + // NSString* callbackId = [arguments objectAtIndex:0]; + // NSMutableDictionary* imageModes = nil; + NSArray* imageArray = nil; + NSArray* movieArray = nil; + NSArray* audioArray = nil; + + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + // there is a camera, find the modes + // can get image/jpeg or image/png from camera + + /* can't find a way to get the default height and width and other info + * for images/movies taken with UIImagePickerController + */ + NSDictionary* jpg = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:0], kW3CMediaFormatHeight, + [NSNumber numberWithInt:0], kW3CMediaFormatWidth, + @"image/jpeg", kW3CMediaModeType, + nil]; + NSDictionary* png = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:0], kW3CMediaFormatHeight, + [NSNumber numberWithInt:0], kW3CMediaFormatWidth, + @"image/png", kW3CMediaModeType, + nil]; + imageArray = [NSArray arrayWithObjects:jpg, png, nil]; + + if ([UIImagePickerController respondsToSelector:@selector(availableMediaTypesForSourceType:)]) { + NSArray* types = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; + + if ([types containsObject:(NSString*)kUTTypeMovie]) { + NSDictionary* mov = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:0], kW3CMediaFormatHeight, + [NSNumber numberWithInt:0], kW3CMediaFormatWidth, + @"video/quicktime", kW3CMediaModeType, + nil]; + movieArray = [NSArray arrayWithObject:mov]; + } + } + } + NSDictionary* modes = [NSDictionary dictionaryWithObjectsAndKeys: + imageArray ? (NSObject*) imageArray:[NSNull null], @"image", + movieArray ? (NSObject*) movieArray:[NSNull null], @"video", + audioArray ? (NSObject*) audioArray:[NSNull null], @"audio", + nil]; + NSString* jsString = [NSString stringWithFormat:@"navigator.device.capture.setSupportedModes(%@);", [modes JSONString]]; + [self.commandDelegate evalJs:jsString]; +} + +- (void)getFormatData:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + // existence of fullPath checked on JS side + NSString* fullPath = [command.arguments objectAtIndex:0]; + // mimeType could be null + NSString* mimeType = nil; + + if ([command.arguments count] > 1) { + mimeType = [command.arguments objectAtIndex:1]; + } + BOOL bError = NO; + CDVCaptureError errorCode = CAPTURE_INTERNAL_ERR; + CDVPluginResult* result = nil; + + if (!mimeType || [mimeType isKindOfClass:[NSNull class]]) { + // try to determine mime type if not provided + id command = [self.commandDelegate getCommandInstance:@"File"]; + bError = !([command isKindOfClass:[CDVFile class]]); + if (!bError) { + CDVFile* cdvFile = (CDVFile*)command; + mimeType = [cdvFile getMimeTypeFromPath:fullPath]; + if (!mimeType) { + // can't do much without mimeType, return error + bError = YES; + errorCode = CAPTURE_INVALID_ARGUMENT; + } + } + } + if (!bError) { + // create and initialize return dictionary + NSMutableDictionary* formatData = [NSMutableDictionary dictionaryWithCapacity:5]; + [formatData setObject:[NSNull null] forKey:kW3CMediaFormatCodecs]; + [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatBitrate]; + [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatHeight]; + [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatWidth]; + [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatDuration]; + + if ([mimeType rangeOfString:@"image/"].location != NSNotFound) { + UIImage* image = [UIImage imageWithContentsOfFile:fullPath]; + if (image) { + CGSize imgSize = [image size]; + [formatData setObject:[NSNumber numberWithInteger:imgSize.width] forKey:kW3CMediaFormatWidth]; + [formatData setObject:[NSNumber numberWithInteger:imgSize.height] forKey:kW3CMediaFormatHeight]; + } + } else if (([mimeType rangeOfString:@"video/"].location != NSNotFound) && (NSClassFromString(@"AVURLAsset") != nil)) { + NSURL* movieURL = [NSURL fileURLWithPath:fullPath]; + AVURLAsset* movieAsset = [[AVURLAsset alloc] initWithURL:movieURL options:nil]; + CMTime duration = [movieAsset duration]; + [formatData setObject:[NSNumber numberWithFloat:CMTimeGetSeconds(duration)] forKey:kW3CMediaFormatDuration]; + + NSArray* allVideoTracks = [movieAsset tracksWithMediaType:AVMediaTypeVideo]; + if ([allVideoTracks count] > 0) { + AVAssetTrack* track = [[movieAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; + CGSize size = [track naturalSize]; + + [formatData setObject:[NSNumber numberWithFloat:size.height] forKey:kW3CMediaFormatHeight]; + [formatData setObject:[NSNumber numberWithFloat:size.width] forKey:kW3CMediaFormatWidth]; + // not sure how to get codecs or bitrate??? + // AVMetadataItem + // AudioFile + } else { + NSLog(@"No video tracks found for %@", fullPath); + } + } else if ([mimeType rangeOfString:@"audio/"].location != NSNotFound) { + if (NSClassFromString(@"AVAudioPlayer") != nil) { + NSURL* fileURL = [NSURL fileURLWithPath:fullPath]; + NSError* err = nil; + + AVAudioPlayer* avPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&err]; + if (!err) { + // get the data + [formatData setObject:[NSNumber numberWithDouble:[avPlayer duration]] forKey:kW3CMediaFormatDuration]; + if ([avPlayer respondsToSelector:@selector(settings)]) { + NSDictionary* info = [avPlayer settings]; + NSNumber* bitRate = [info objectForKey:AVEncoderBitRateKey]; + if (bitRate) { + [formatData setObject:bitRate forKey:kW3CMediaFormatBitrate]; + } + } + } // else leave data init'ed to 0 + } + } + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:formatData]; + // NSLog(@"getFormatData: %@", [formatData description]); + } + if (bError) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:errorCode]; + } + if (result) { + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (NSDictionary*)getMediaDictionaryFromPath:(NSString*)fullPath ofType:(NSString*)type +{ + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSMutableDictionary* fileDict = [NSMutableDictionary dictionaryWithCapacity:5]; + + [fileDict setObject:[fullPath lastPathComponent] forKey:@"name"]; + [fileDict setObject:fullPath forKey:@"fullPath"]; + // determine type + if (!type) { + id command = [self.commandDelegate getCommandInstance:@"File"]; + if ([command isKindOfClass:[CDVFile class]]) { + CDVFile* cdvFile = (CDVFile*)command; + NSString* mimeType = [cdvFile getMimeTypeFromPath:fullPath]; + [fileDict setObject:(mimeType != nil ? (NSObject*)mimeType : [NSNull null]) forKey:@"type"]; + } + } + NSDictionary* fileAttrs = [fileMgr attributesOfItemAtPath:fullPath error:nil]; + [fileDict setObject:[NSNumber numberWithUnsignedLongLong:[fileAttrs fileSize]] forKey:@"size"]; + NSDate* modDate = [fileAttrs fileModificationDate]; + NSNumber* msDate = [NSNumber numberWithDouble:[modDate timeIntervalSince1970] * 1000]; + [fileDict setObject:msDate forKey:@"lastModifiedDate"]; + + return fileDict; +} + +- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo +{ + // older api calls new one + [self imagePickerController:picker didFinishPickingMediaWithInfo:editingInfo]; +} + +/* Called when image/movie is finished recording. + * Calls success or error code as appropriate + * if successful, result contains an array (with just one entry since can only get one image unless build own camera UI) of MediaFile object representing the image + * name + * fullPath + * type + * lastModifiedDate + * size + */ +- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info +{ + CDVImagePicker* cameraPicker = (CDVImagePicker*)picker; + NSString* callbackId = cameraPicker.callbackId; + + if ([picker respondsToSelector:@selector(presentingViewController)]) { + [[picker presentingViewController] dismissModalViewControllerAnimated:YES]; + } else { + [[picker parentViewController] dismissModalViewControllerAnimated:YES]; + } + + CDVPluginResult* result = nil; + + UIImage* image = nil; + NSString* mediaType = [info objectForKey:UIImagePickerControllerMediaType]; + if (!mediaType || [mediaType isEqualToString:(NSString*)kUTTypeImage]) { + // mediaType is nil then only option is UIImagePickerControllerOriginalImage + if ([UIImagePickerController respondsToSelector:@selector(allowsEditing)] && + (cameraPicker.allowsEditing && [info objectForKey:UIImagePickerControllerEditedImage])) { + image = [info objectForKey:UIImagePickerControllerEditedImage]; + } else { + image = [info objectForKey:UIImagePickerControllerOriginalImage]; + } + } + if (image != nil) { + // mediaType was image + result = [self processImage:image type:cameraPicker.mimeType forCallbackId:callbackId]; + } else if ([mediaType isEqualToString:(NSString*)kUTTypeMovie]) { + // process video + NSString* moviePath = [[info objectForKey:UIImagePickerControllerMediaURL] path]; + if (moviePath) { + result = [self processVideo:moviePath forCallbackId:callbackId]; + } + } + if (!result) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_INTERNAL_ERR]; + } + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + pickerController = nil; +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker +{ + CDVImagePicker* cameraPicker = (CDVImagePicker*)picker; + NSString* callbackId = cameraPicker.callbackId; + + if ([picker respondsToSelector:@selector(presentingViewController)]) { + [[picker presentingViewController] dismissModalViewControllerAnimated:YES]; + } else { + [[picker parentViewController] dismissModalViewControllerAnimated:YES]; + } + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NO_MEDIA_FILES]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + pickerController = nil; +} + +@end + +@implementation CDVAudioNavigationController + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000 + - (NSUInteger)supportedInterfaceOrientations + { + // delegate to CVDAudioRecorderViewController + return [self.topViewController supportedInterfaceOrientations]; + } +#endif + +@end + +@implementation CDVAudioRecorderViewController +@synthesize errorCode, callbackId, duration, captureCommand, doneButton, recordingView, recordButton, recordImage, stopRecordImage, timerLabel, avRecorder, avSession, pluginResult, timer, isTimed; + +- (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 user wants iPhone only app to run on iPad they must remove *~ipad.* images from capture.bundle + if (isLessThaniOS4) { + NSString* iPadResource = [NSString stringWithFormat:@"%@~ipad.png", resource]; + if (CDV_IsIPad() && [UIImage imageNamed:iPadResource]) { + return iPadResource; + } else { + return [NSString stringWithFormat:@"%@.png", resource]; + } + } + + return resource; +} + +- (id)initWithCommand:(CDVCapture*)theCommand duration:(NSNumber*)theDuration callbackId:(NSString*)theCallbackId +{ + if ((self = [super init])) { + self.captureCommand = theCommand; + self.duration = theDuration; + self.callbackId = theCallbackId; + self.errorCode = CAPTURE_NO_MEDIA_FILES; + self.isTimed = self.duration != nil; + + return self; + } + + return nil; +} + +- (void)loadView +{ + // create view and display + CGRect viewRect = [[UIScreen mainScreen] applicationFrame]; + UIView* tmp = [[UIView alloc] initWithFrame:viewRect]; + + // make backgrounds + NSString* microphoneResource = @"Capture.bundle/microphone"; + + if (CDV_IsIPhone5()) { + microphoneResource = @"Capture.bundle/microphone-568h"; + } + + UIImage* microphone = [UIImage imageNamed:[self resolveImageResource:microphoneResource]]; + UIView* microphoneView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, microphone.size.height)]; + [microphoneView setBackgroundColor:[UIColor colorWithPatternImage:microphone]]; + [microphoneView setUserInteractionEnabled:NO]; + [microphoneView setIsAccessibilityElement:NO]; + [tmp addSubview:microphoneView]; + + // add bottom bar view + UIImage* grayBkg = [UIImage imageNamed:[self resolveImageResource:@"Capture.bundle/controls_bg"]]; + UIView* controls = [[UIView alloc] initWithFrame:CGRectMake(0, microphone.size.height, viewRect.size.width, grayBkg.size.height)]; + [controls setBackgroundColor:[UIColor colorWithPatternImage:grayBkg]]; + [controls setUserInteractionEnabled:NO]; + [controls setIsAccessibilityElement:NO]; + [tmp addSubview:controls]; + + // make red recording background view + UIImage* recordingBkg = [UIImage imageNamed:[self resolveImageResource:@"Capture.bundle/recording_bg"]]; + UIColor* background = [UIColor colorWithPatternImage:recordingBkg]; + self.recordingView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, recordingBkg.size.height)]; + [self.recordingView setBackgroundColor:background]; + [self.recordingView setHidden:YES]; + [self.recordingView setUserInteractionEnabled:NO]; + [self.recordingView setIsAccessibilityElement:NO]; + [tmp addSubview:self.recordingView]; + + // add label + self.timerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, recordingBkg.size.height)]; + // timerLabel.autoresizingMask = reSizeMask; + [self.timerLabel setBackgroundColor:[UIColor clearColor]]; + [self.timerLabel setTextColor:[UIColor whiteColor]]; + [self.timerLabel setTextAlignment:UITextAlignmentCenter]; + [self.timerLabel setText:@"0:00"]; + [self.timerLabel setAccessibilityHint:NSLocalizedString(@"recorded time in minutes and seconds", nil)]; + self.timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently; + self.timerLabel.accessibilityTraits &= ~UIAccessibilityTraitStaticText; + [tmp addSubview:self.timerLabel]; + + // Add record button + + self.recordImage = [UIImage imageNamed:[self resolveImageResource:@"Capture.bundle/record_button"]]; + self.stopRecordImage = [UIImage imageNamed:[self resolveImageResource:@"Capture.bundle/stop_button"]]; + self.recordButton.accessibilityTraits |= [self accessibilityTraits]; + self.recordButton = [[UIButton alloc] initWithFrame:CGRectMake((viewRect.size.width - recordImage.size.width) / 2, (microphone.size.height + (grayBkg.size.height - recordImage.size.height) / 2), recordImage.size.width, recordImage.size.height)]; + [self.recordButton setAccessibilityLabel:NSLocalizedString(@"toggle audio recording", nil)]; + [self.recordButton setImage:recordImage forState:UIControlStateNormal]; + [self.recordButton addTarget:self action:@selector(processButton:) forControlEvents:UIControlEventTouchUpInside]; + [tmp addSubview:recordButton]; + + // make and add done button to navigation bar + self.doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismissAudioView:)]; + [self.doneButton setStyle:UIBarButtonItemStyleDone]; + self.navigationItem.rightBarButtonItem = self.doneButton; + + [self setView:tmp]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + NSError* error = nil; + + if (self.avSession == nil) { + // create audio session + self.avSession = [AVAudioSession sharedInstance]; + if (error) { + // return error if can't create recording audio session + NSLog(@"error creating audio session: %@", [[error userInfo] description]); + self.errorCode = CAPTURE_INTERNAL_ERR; + [self dismissAudioView:nil]; + } + } + + // create file to record to in temporary dir + + NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; // use file system temporary directory + NSError* err = nil; + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + + // generate unique file name + NSString* filePath; + int i = 1; + do { + filePath = [NSString stringWithFormat:@"%@/audio_%03d.wav", docsPath, i++]; + } while ([fileMgr fileExistsAtPath:filePath]); + + NSURL* fileURL = [NSURL fileURLWithPath:filePath isDirectory:NO]; + + // create AVAudioPlayer + self.avRecorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:nil error:&err]; + if (err) { + NSLog(@"Failed to initialize AVAudioRecorder: %@\n", [err localizedDescription]); + self.avRecorder = nil; + // return error + self.errorCode = CAPTURE_INTERNAL_ERR; + [self dismissAudioView:nil]; + } else { + self.avRecorder.delegate = self; + [self.avRecorder prepareToRecord]; + self.recordButton.enabled = YES; + self.doneButton.enabled = YES; + } +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000 + - (NSUInteger)supportedInterfaceOrientations + { + NSUInteger orientation = UIInterfaceOrientationMaskPortrait; // must support portrait + NSUInteger supported = [captureCommand.viewController supportedInterfaceOrientations]; + + orientation = orientation | (supported & UIInterfaceOrientationMaskPortraitUpsideDown); + return orientation; + } +#endif + +- (void)viewDidUnload +{ + [self setView:nil]; + [self.captureCommand setInUse:NO]; +} + +- (void)processButton:(id)sender +{ + if (self.avRecorder.recording) { + // stop recording + [self.avRecorder stop]; + self.isTimed = NO; // recording was stopped via button so reset isTimed + // view cleanup will occur in audioRecordingDidFinishRecording + } else { + // begin recording + [self.recordButton setImage:stopRecordImage forState:UIControlStateNormal]; + self.recordButton.accessibilityTraits &= ~[self accessibilityTraits]; + [self.recordingView setHidden:NO]; + NSError* error = nil; + [self.avSession setCategory:AVAudioSessionCategoryRecord error:&error]; + [self.avSession setActive:YES error:&error]; + if (error) { + // can't continue without active audio session + self.errorCode = CAPTURE_INTERNAL_ERR; + [self dismissAudioView:nil]; + } else { + if (self.duration) { + self.isTimed = true; + [self.avRecorder recordForDuration:[duration doubleValue]]; + } else { + [self.avRecorder record]; + } + [self.timerLabel setText:@"0.00"]; + self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:@selector(updateTime) userInfo:nil repeats:YES]; + self.doneButton.enabled = NO; + } + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + } +} + +/* + * helper method to clean up when stop recording + */ +- (void)stopRecordingCleanup +{ + if (self.avRecorder.recording) { + [self.avRecorder stop]; + } + [self.recordButton setImage:recordImage forState:UIControlStateNormal]; + self.recordButton.accessibilityTraits |= [self accessibilityTraits]; + [self.recordingView setHidden:YES]; + self.doneButton.enabled = YES; + if (self.avSession) { + // deactivate session so sounds can come through + [self.avSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; + [self.avSession setActive:NO error:nil]; + } + if (self.duration && self.isTimed) { + // VoiceOver announcement so user knows timed recording has finished + BOOL isUIAccessibilityAnnouncementNotification = (&UIAccessibilityAnnouncementNotification != NULL); + if (isUIAccessibilityAnnouncementNotification) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500ull * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString(@"timed recording complete", nil)); + }); + } + } else { + // issue a layout notification change so that VO will reannounce the button label when recording completes + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + } +} + +- (void)dismissAudioView:(id)sender +{ + // called when done button pressed or when error condition to do cleanup and remove view + if ([self.captureCommand.viewController.modalViewController respondsToSelector:@selector(presentingViewController)]) { + [[self.captureCommand.viewController.modalViewController presentingViewController] dismissModalViewControllerAnimated:YES]; + } else { + [[self.captureCommand.viewController.modalViewController parentViewController] dismissModalViewControllerAnimated:YES]; + } + + if (!self.pluginResult) { + // return error + self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:self.errorCode]; + } + + self.avRecorder = nil; + [self.avSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; + [self.avSession setActive:NO error:nil]; + [self.captureCommand setInUse:NO]; + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + // return result + [self.captureCommand.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; +} + +- (void)updateTime +{ + // update the label with the elapsed time + [self.timerLabel setText:[self formatTime:self.avRecorder.currentTime]]; +} + +- (NSString*)formatTime:(int)interval +{ + // is this format universal? + int secs = interval % 60; + int min = interval / 60; + + if (interval < 60) { + return [NSString stringWithFormat:@"0:%02d", interval]; + } else { + return [NSString stringWithFormat:@"%d:%02d", min, secs]; + } +} + +- (void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfully:(BOOL)flag +{ + // may be called when timed audio finishes - need to stop time and reset buttons + [self.timer invalidate]; + [self stopRecordingCleanup]; + + // generate success result + if (flag) { + NSString* filePath = [avRecorder.url path]; + // NSLog(@"filePath: %@", filePath); + NSDictionary* fileDict = [captureCommand getMediaDictionaryFromPath:filePath ofType:@"audio/wav"]; + NSArray* fileArray = [NSArray arrayWithObject:fileDict]; + + self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray]; + } else { + self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR]; + } +} + +- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder*)recorder error:(NSError*)error +{ + [self.timer invalidate]; + [self stopRecordingCleanup]; + + NSLog(@"error recording audio"); + self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR]; + [self dismissAudioView:nil]; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCommandDelegate.h b/cordova/ios/CordovaLib/Classes/CDVCommandDelegate.h new file mode 100755 index 000000000..040113667 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCommandDelegate.h @@ -0,0 +1,54 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVAvailability.h" +#import "CDVInvokedUrlCommand.h" + +@class CDVPlugin; +@class CDVPluginResult; +@class CDVWhitelist; + +@protocol CDVCommandDelegate + +@property (nonatomic, readonly) NSDictionary* settings; + +- (NSString*)pathForResource:(NSString*)resourcepath; +- (id)getCommandInstance:(NSString*)pluginName; + +// Plugins should not be using this interface to call other plugins since it +// will result in bogus callbacks being made. +- (BOOL)execute:(CDVInvokedUrlCommand*)command CDV_DEPRECATED(2.2, "Use direct method calls instead."); + +// Sends a plugin result to the JS. This is thread-safe. +- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId; +// Evaluates the given JS. This is thread-safe. +- (void)evalJs:(NSString*)js; +// Can be used to evaluate JS right away instead of scheduling it on the run-loop. +// This is required for dispatch resign and pause events, but should not be used +// without reason. Without the run-loop delay, alerts used in JS callbacks may result +// in dead-lock. This method must be called from the UI thread. +- (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop; +// Runs the given block on a background thread using a shared thread-pool. +- (void)runInBackground:(void (^)())block; +// Returns the User-Agent of the associated UIWebView. +- (NSString*)userAgent; +// Returns whether the given URL passes the white-list. +- (BOOL)URLIsWhitelisted:(NSURL*)url; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCommandDelegateImpl.h b/cordova/ios/CordovaLib/Classes/CDVCommandDelegateImpl.h new file mode 100755 index 000000000..673513607 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCommandDelegateImpl.h @@ -0,0 +1,33 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVCommandDelegate.h" + +@class CDVViewController; +@class CDVCommandQueue; + +@interface CDVCommandDelegateImpl : NSObject { + @private + __weak CDVViewController* _viewController; + @protected + __weak CDVCommandQueue* _commandQueue; +} +- (id)initWithViewController:(CDVViewController*)viewController; +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCommandDelegateImpl.m b/cordova/ios/CordovaLib/Classes/CDVCommandDelegateImpl.m new file mode 100755 index 000000000..fa0e5e056 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCommandDelegateImpl.m @@ -0,0 +1,145 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVCommandDelegateImpl.h" +#import "CDVJSON.h" +#import "CDVCommandQueue.h" +#import "CDVPluginResult.h" +#import "CDVViewController.h" + +@implementation CDVCommandDelegateImpl + +- (id)initWithViewController:(CDVViewController*)viewController +{ + self = [super init]; + if (self != nil) { + _viewController = viewController; + _commandQueue = _viewController.commandQueue; + } + return self; +} + +- (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 = _viewController.wwwFolderName; + + if ([directoryPartsJoined length] > 0) { + directoryStr = [NSString stringWithFormat:@"%@/%@", _viewController.wwwFolderName, [directoryParts componentsJoinedByString:@"/"]]; + } + + return [mainBundle pathForResource:filename ofType:@"" inDirectory:directoryStr]; +} + +- (void)evalJsHelper2:(NSString*)js +{ + CDV_EXEC_LOG(@"Exec: evalling: %@", [js substringToIndex:MIN([js length], 160)]); + NSString* commandsJSON = [_viewController.webView stringByEvaluatingJavaScriptFromString:js]; + if ([commandsJSON length] > 0) { + CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by chaining."); + } + + [_commandQueue enqueCommandBatch:commandsJSON]; +} + +- (void)evalJsHelper:(NSString*)js +{ + // Cycle the run-loop before executing the JS. + // This works around a bug where sometimes alerts() within callbacks can cause + // dead-lock. + // If the commandQueue is currently executing, then we know that it is safe to + // execute the callback immediately. + // Using (dispatch_get_main_queue()) does *not* fix deadlocks for some reaon, + // but performSelectorOnMainThread: does. + if (![NSThread isMainThread] || !_commandQueue.currentlyExecuting) { + [self performSelectorOnMainThread:@selector(evalJsHelper2:) withObject:js waitUntilDone:NO]; + } else { + [self evalJsHelper2:js]; + } +} + +- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId +{ + CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status); + // This occurs when there is are no win/fail callbacks for the call. + if ([@"INVALID" isEqualToString : callbackId]) { + return; + } + int status = [result.status intValue]; + BOOL keepCallback = [result.keepCallback boolValue]; + NSString* argumentsAsJSON = [result argumentsAsJSON]; + + NSString* js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d)", callbackId, status, argumentsAsJSON, keepCallback]; + + [self evalJsHelper:js]; +} + +- (void)evalJs:(NSString*)js +{ + [self evalJs:js scheduledOnRunLoop:YES]; +} + +- (void)evalJs:(NSString*)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop +{ + js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeEvalAndFetch(function(){%@})", js]; + if (scheduledOnRunLoop) { + [self evalJsHelper:js]; + } else { + [self evalJsHelper2:js]; + } +} + +- (BOOL)execute:(CDVInvokedUrlCommand*)command +{ + return [_commandQueue execute:command]; +} + +- (id)getCommandInstance:(NSString*)pluginName +{ + return [_viewController getCommandInstance:pluginName]; +} + +- (void)runInBackground:(void (^)())block +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); +} + +- (NSString*)userAgent +{ + return [_viewController userAgent]; +} + +- (BOOL)URLIsWhitelisted:(NSURL*)url +{ + return ![_viewController.whitelist schemeIsAllowed:[url scheme]] || + [_viewController.whitelist URLIsAllowed:url]; +} + +- (NSDictionary*)settings +{ + return _viewController.settings; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCommandQueue.h b/cordova/ios/CordovaLib/Classes/CDVCommandQueue.h new file mode 100755 index 000000000..27c47b59a --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCommandQueue.h @@ -0,0 +1,40 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@class CDVInvokedUrlCommand; +@class CDVViewController; + +@interface CDVCommandQueue : NSObject + +@property (nonatomic, readonly) BOOL currentlyExecuting; + +- (id)initWithViewController:(CDVViewController*)viewController; +- (void)dispose; + +- (void)resetRequestId; +- (void)enqueCommandBatch:(NSString*)batchJSON; + +- (void)maybeFetchCommandsFromJs:(NSNumber*)requestId; +- (void)fetchCommandsFromJs; +- (void)executePending; +- (BOOL)execute:(CDVInvokedUrlCommand*)command; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVCommandQueue.m b/cordova/ios/CordovaLib/Classes/CDVCommandQueue.m new file mode 100755 index 000000000..1a0dfa0d2 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVCommandQueue.m @@ -0,0 +1,169 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#include +#import "CDV.h" +#import "CDVCommandQueue.h" +#import "CDVViewController.h" +#import "CDVCommandDelegateImpl.h" + +@interface CDVCommandQueue () { + NSInteger _lastCommandQueueFlushRequestId; + __weak CDVViewController* _viewController; + NSMutableArray* _queue; + BOOL _currentlyExecuting; +} +@end + +@implementation CDVCommandQueue + +@synthesize currentlyExecuting = _currentlyExecuting; + +- (id)initWithViewController:(CDVViewController*)viewController +{ + self = [super init]; + if (self != nil) { + _viewController = viewController; + _queue = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dispose +{ + // TODO(agrieve): Make this a zeroing weak ref once we drop support for 4.3. + _viewController = nil; +} + +- (void)resetRequestId +{ + _lastCommandQueueFlushRequestId = 0; +} + +- (void)enqueCommandBatch:(NSString*)batchJSON +{ + if ([batchJSON length] > 0) { + [_queue addObject:batchJSON]; + [self executePending]; + } +} + +- (void)maybeFetchCommandsFromJs:(NSNumber*)requestId +{ + // Use the request ID to determine if we've already flushed for this request. + // This is required only because the NSURLProtocol enqueues the same request + // multiple times. + if ([requestId integerValue] > _lastCommandQueueFlushRequestId) { + _lastCommandQueueFlushRequestId = [requestId integerValue]; + [self fetchCommandsFromJs]; + } +} + +- (void)fetchCommandsFromJs +{ + // Grab all the queued commands from the JS side. + NSString* queuedCommandsJSON = [_viewController.webView stringByEvaluatingJavaScriptFromString: + @"cordova.require('cordova/exec').nativeFetchMessages()"]; + + [self enqueCommandBatch:queuedCommandsJSON]; + if ([queuedCommandsJSON length] > 0) { + CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by request."); + } +} + +- (void)executePending +{ + // Make us re-entrant-safe. + if (_currentlyExecuting) { + return; + } + @try { + _currentlyExecuting = YES; + + for (NSUInteger i = 0; i < [_queue count]; ++i) { + // Parse the returned JSON array. + NSArray* commandBatch = [[_queue objectAtIndex:i] JSONObject]; + + // Iterate over and execute all of the commands. + for (NSArray* jsonEntry in commandBatch) { + CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry]; + CDV_EXEC_LOG(@"Exec(%@): Calling %@.%@", command.callbackId, command.className, command.methodName); + + if (![self execute:command]) { +#ifdef DEBUG + NSString* commandJson = [jsonEntry JSONString]; + static NSUInteger maxLogLength = 1024; + NSString* commandString = ([commandJson length] > maxLogLength) ? + [NSString stringWithFormat:@"%@[...]", [commandJson substringToIndex:maxLogLength]] : + commandJson; + + DLog(@"FAILED pluginJSON = %@", commandString); +#endif + } + } + } + + [_queue removeAllObjects]; + } @finally + { + _currentlyExecuting = NO; + } +} + +- (BOOL)execute:(CDVInvokedUrlCommand*)command +{ + if ((command.className == nil) || (command.methodName == nil)) { + NSLog(@"ERROR: Classname and/or methodName not found for command."); + return NO; + } + + // Fetch an instance of this class + CDVPlugin* obj = [_viewController.commandDelegate getCommandInstance:command.className]; + + if (!([obj isKindOfClass:[CDVPlugin class]])) { + NSLog(@"ERROR: Plugin '%@' not found, or is not a CDVPlugin. Check your plugin mapping in config.xml.", command.className); + return NO; + } + BOOL retVal = YES; + + // Find the proper selector to call. + NSString* methodName = [NSString stringWithFormat:@"%@:", command.methodName]; + NSString* methodNameWithDict = [NSString stringWithFormat:@"%@:withDict:", command.methodName]; + SEL normalSelector = NSSelectorFromString(methodName); + SEL legacySelector = NSSelectorFromString(methodNameWithDict); + // Test for the legacy selector first in case they both exist. + if ([obj respondsToSelector:legacySelector]) { + NSMutableArray* arguments = nil; + NSMutableDictionary* dict = nil; + [command legacyArguments:&arguments andDict:&dict]; + // [obj performSelector:legacySelector withObject:arguments withObject:dict]; + objc_msgSend(obj, legacySelector, arguments, dict); + } else if ([obj respondsToSelector:normalSelector]) { + // [obj performSelector:normalSelector withObject:command]; + objc_msgSend(obj, normalSelector, command); + } else { + // There's no method to call, so throw an error. + NSLog(@"ERROR: Method '%@' not defined in Plugin '%@'", methodName, command.className); + retVal = NO; + } + + return retVal; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVConfigParser.h b/cordova/ios/CordovaLib/Classes/CDVConfigParser.h new file mode 100755 index 000000000..73925803c --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVConfigParser.h @@ -0,0 +1,28 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +@interface CDVConfigParser : NSObject {} + +@property (nonatomic, readonly, strong) NSMutableDictionary* pluginsDict; +@property (nonatomic, readonly, strong) NSMutableDictionary* settings; +@property (nonatomic, readonly, strong) NSMutableArray* whitelistHosts; +@property (nonatomic, readonly, strong) NSMutableArray* startupPluginNames; +@property (nonatomic, readonly, strong) NSString* startPage; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVConfigParser.m b/cordova/ios/CordovaLib/Classes/CDVConfigParser.m new file mode 100755 index 000000000..ffc8edeba --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVConfigParser.m @@ -0,0 +1,70 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVConfigParser.h" + +@interface CDVConfigParser () + +@property (nonatomic, readwrite, strong) NSMutableDictionary* pluginsDict; +@property (nonatomic, readwrite, strong) NSMutableDictionary* settings; +@property (nonatomic, readwrite, strong) NSMutableArray* whitelistHosts; +@property (nonatomic, readwrite, strong) NSMutableArray* startupPluginNames; +@property (nonatomic, readwrite, strong) NSString* startPage; + +@end + +@implementation CDVConfigParser + +@synthesize pluginsDict, settings, whitelistHosts, startPage, startupPluginNames; + +- (id)init +{ + self = [super init]; + if (self != nil) { + self.pluginsDict = [[NSMutableDictionary alloc] initWithCapacity:30]; + self.settings = [[NSMutableDictionary alloc] initWithCapacity:30]; + self.whitelistHosts = [[NSMutableArray alloc] initWithCapacity:30]; + self.startupPluginNames = [[NSMutableArray alloc] initWithCapacity:8]; + } + return self; +} + +- (void)parser:(NSXMLParser*)parser didStartElement:(NSString*)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString*)qualifiedName attributes:(NSDictionary*)attributeDict +{ + if ([elementName isEqualToString:@"preference"]) { + settings[attributeDict[@"name"]] = attributeDict[@"value"]; + } else if ([elementName isEqualToString:@"plugin"]) { + NSString* name = [attributeDict[@"name"] lowercaseString]; + pluginsDict[name] = attributeDict[@"value"]; + if ([@"true" isEqualToString : attributeDict[@"onload"]]) { + [self.startupPluginNames addObject:name]; + } + } else if ([elementName isEqualToString:@"access"]) { + [whitelistHosts addObject:attributeDict[@"origin"]]; + } else if ([elementName isEqualToString:@"content"]) { + self.startPage = attributeDict[@"src"]; + } +} + +- (void)parser:(NSXMLParser*)parser parseErrorOccurred:(NSError*)parseError +{ + NSAssert(NO, @"config.xml parse error line %d col %d", [parser lineNumber], [parser columnNumber]); +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVConnection.h b/cordova/ios/CordovaLib/Classes/CDVConnection.h new file mode 100755 index 000000000..d3e8c5d9b --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVConnection.h @@ -0,0 +1,34 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" +#import "CDVReachability.h" + +@interface CDVConnection : CDVPlugin { + NSString* type; + NSString* _callbackId; + + CDVReachability* internetReach; +} + +@property (copy) NSString* connectionType; +@property (strong) CDVReachability* internetReach; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVConnection.m b/cordova/ios/CordovaLib/Classes/CDVConnection.m new file mode 100755 index 000000000..b3f5cab9b --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVConnection.m @@ -0,0 +1,132 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVConnection.h" +#import "CDVReachability.h" + +@interface CDVConnection (PrivateMethods) +- (void)updateOnlineStatus; +- (void)sendPluginResult; +@end + +@implementation CDVConnection + +@synthesize connectionType, internetReach; + +- (void)getConnectionInfo:(CDVInvokedUrlCommand*)command +{ + _callbackId = command.callbackId; + [self sendPluginResult]; +} + +- (void)sendPluginResult +{ + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:self.connectionType]; + + [result setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:result callbackId:_callbackId]; +} + +- (NSString*)w3cConnectionTypeFor:(CDVReachability*)reachability +{ + NetworkStatus networkStatus = [reachability currentReachabilityStatus]; + + switch (networkStatus) { + case NotReachable: + return @"none"; + + case ReachableViaWWAN: + // Return value of '2g' is deprecated as of 2.6.0 and will be replaced with 'cellular' in 3.0.0 + return @"2g"; + + case ReachableViaWiFi: + return @"wifi"; + + default: + return @"unknown"; + } +} + +- (BOOL)isCellularConnection:(NSString*)theConnectionType +{ + return [theConnectionType isEqualToString:@"2g"] || + [theConnectionType isEqualToString:@"3g"] || + [theConnectionType isEqualToString:@"4g"] || + [theConnectionType isEqualToString:@"cellular"]; +} + +- (void)updateReachability:(CDVReachability*)reachability +{ + if (reachability) { + // check whether the connection type has changed + NSString* newConnectionType = [self w3cConnectionTypeFor:reachability]; + if ([newConnectionType isEqualToString:self.connectionType]) { // the same as before, remove dupes + return; + } else { + self.connectionType = [self w3cConnectionTypeFor:reachability]; + } + } + [self sendPluginResult]; +} + +- (void)updateConnectionType:(NSNotification*)note +{ + CDVReachability* curReach = [note object]; + + if ((curReach != nil) && [curReach isKindOfClass:[CDVReachability class]]) { + [self updateReachability:curReach]; + } +} + +- (void)onPause +{ + [self.internetReach stopNotifier]; +} + +- (void)onResume +{ + [self.internetReach startNotifier]; + [self updateReachability:self.internetReach]; +} + +- (CDVPlugin*)initWithWebView:(UIWebView*)theWebView +{ + self = [super initWithWebView:theWebView]; + if (self) { + self.connectionType = @"none"; + self.internetReach = [CDVReachability reachabilityForInternetConnection]; + self.connectionType = [self w3cConnectionTypeFor:self.internetReach]; + [self.internetReach startNotifier]; + [self printDeprecationNotice]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateConnectionType:) + name:kReachabilityChangedNotification object:nil]; + if (&UIApplicationDidEnterBackgroundNotification && &UIApplicationWillEnterForegroundNotification) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPause) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume) name:UIApplicationWillEnterForegroundNotification object:nil]; + } + } + return self; +} + +- (void)printDeprecationNotice +{ + NSLog(@"DEPRECATION NOTICE: The Connection ReachableViaWWAN return value of '2g' is deprecated as of Cordova version 2.6.0 and will be changed to 'cellular' in a future release. "); +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVContact.h b/cordova/ios/CordovaLib/Classes/CDVContact.h new file mode 100755 index 000000000..5187efcdf --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVContact.h @@ -0,0 +1,136 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import + +enum CDVContactError { + UNKNOWN_ERROR = 0, + INVALID_ARGUMENT_ERROR = 1, + TIMEOUT_ERROR = 2, + PENDING_OPERATION_ERROR = 3, + IO_ERROR = 4, + NOT_SUPPORTED_ERROR = 5, + PERMISSION_DENIED_ERROR = 20 +}; +typedef NSUInteger CDVContactError; + +@interface CDVContact : NSObject { + ABRecordRef record; // the ABRecord associated with this contact + NSDictionary* returnFields; // dictionary of fields to return when performing search +} + +@property (nonatomic, assign) ABRecordRef record; +@property (nonatomic, strong) NSDictionary* returnFields; + ++ (NSDictionary*)defaultABtoW3C; ++ (NSDictionary*)defaultW3CtoAB; ++ (NSSet*)defaultW3CtoNull; ++ (NSDictionary*)defaultObjectAndProperties; ++ (NSDictionary*)defaultFields; + ++ (NSDictionary*)calcReturnFields:(NSArray*)fields; +- (id)init; +- (id)initFromABRecord:(ABRecordRef)aRecord; +- (bool)setFromContactDict:(NSDictionary*)aContact asUpdate:(BOOL)bUpdate; + ++ (BOOL)needsConversion:(NSString*)W3Label; ++ (CFStringRef)convertContactTypeToPropertyLabel:(NSString*)label; ++ (NSString*)convertPropertyLabelToContactType:(NSString*)label; ++ (BOOL)isValidW3ContactType:(NSString*)label; +- (bool)setValue:(id)aValue forProperty:(ABPropertyID)aProperty inRecord:(ABRecordRef)aRecord asUpdate:(BOOL)bUpdate; + +- (NSDictionary*)toDictionary:(NSDictionary*)withFields; +- (NSNumber*)getDateAsNumber:(ABPropertyID)datePropId; +- (NSObject*)extractName; +- (NSObject*)extractMultiValue:(NSString*)propertyId; +- (NSObject*)extractAddresses; +- (NSObject*)extractIms; +- (NSObject*)extractOrganizations; +- (NSObject*)extractPhotos; + +- (NSMutableDictionary*)translateW3Dict:(NSDictionary*)dict forProperty:(ABPropertyID)prop; +- (bool)setMultiValueStrings:(NSArray*)fieldArray forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate; +- (bool)setMultiValueDictionary:(NSArray*)array forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate; +- (ABMultiValueRef)allocStringMultiValueFromArray:array; +- (ABMultiValueRef)allocDictMultiValueFromArray:array forProperty:(ABPropertyID)prop; +- (BOOL)foundValue:(NSString*)testValue inFields:(NSDictionary*)searchFields; +- (BOOL)testStringValue:(NSString*)testValue forW3CProperty:(NSString*)property; +- (BOOL)testDateValue:(NSString*)testValue forW3CProperty:(NSString*)property; +- (BOOL)searchContactFields:(NSArray*)fields forMVStringProperty:(ABPropertyID)propId withValue:testValue; +- (BOOL)testMultiValueStrings:(NSString*)testValue forProperty:(ABPropertyID)propId ofType:(NSString*)type; +- (NSArray*)valuesForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord; +- (NSArray*)labelsForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord; +- (BOOL)searchContactFields:(NSArray*)fields forMVDictionaryProperty:(ABPropertyID)propId withValue:(NSString*)testValue; + +@end + +// generic ContactField types +#define kW3ContactFieldType @"type" +#define kW3ContactFieldValue @"value" +#define kW3ContactFieldPrimary @"pref" +// Various labels for ContactField types +#define kW3ContactWorkLabel @"work" +#define kW3ContactHomeLabel @"home" +#define kW3ContactOtherLabel @"other" +#define kW3ContactPhoneFaxLabel @"fax" +#define kW3ContactPhoneMobileLabel @"mobile" +#define kW3ContactPhonePagerLabel @"pager" +#define kW3ContactUrlBlog @"blog" +#define kW3ContactUrlProfile @"profile" +#define kW3ContactImAIMLabel @"aim" +#define kW3ContactImICQLabel @"icq" +#define kW3ContactImMSNLabel @"msn" +#define kW3ContactImYahooLabel @"yahoo" +#define kW3ContactFieldId @"id" +// special translation for IM field value and type +#define kW3ContactImType @"type" +#define kW3ContactImValue @"value" + +// Contact object +#define kW3ContactId @"id" +#define kW3ContactName @"name" +#define kW3ContactFormattedName @"formatted" +#define kW3ContactGivenName @"givenName" +#define kW3ContactFamilyName @"familyName" +#define kW3ContactMiddleName @"middleName" +#define kW3ContactHonorificPrefix @"honorificPrefix" +#define kW3ContactHonorificSuffix @"honorificSuffix" +#define kW3ContactDisplayName @"displayName" +#define kW3ContactNickname @"nickname" +#define kW3ContactPhoneNumbers @"phoneNumbers" +#define kW3ContactAddresses @"addresses" +#define kW3ContactAddressFormatted @"formatted" +#define kW3ContactStreetAddress @"streetAddress" +#define kW3ContactLocality @"locality" +#define kW3ContactRegion @"region" +#define kW3ContactPostalCode @"postalCode" +#define kW3ContactCountry @"country" +#define kW3ContactEmails @"emails" +#define kW3ContactIms @"ims" +#define kW3ContactOrganizations @"organizations" +#define kW3ContactOrganizationName @"name" +#define kW3ContactTitle @"title" +#define kW3ContactDepartment @"department" +#define kW3ContactBirthday @"birthday" +#define kW3ContactNote @"note" +#define kW3ContactPhotos @"photos" +#define kW3ContactCategories @"categories" +#define kW3ContactUrls @"urls" diff --git a/cordova/ios/CordovaLib/Classes/CDVContact.m b/cordova/ios/CordovaLib/Classes/CDVContact.m new file mode 100755 index 000000000..3844525fa --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVContact.m @@ -0,0 +1,1752 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVContact.h" +#import "NSDictionary+Extensions.h" + +#define DATE_OR_NULL(dateObj) ((aDate != nil) ? (id)([aDate descriptionWithLocale:[NSLocale currentLocale]]) : (id)([NSNull null])) +#define IS_VALID_VALUE(value) ((value != nil) && (![value isKindOfClass:[NSNull class]])) + +static NSDictionary* org_apache_cordova_contacts_W3CtoAB = nil; +static NSDictionary* org_apache_cordova_contacts_ABtoW3C = nil; +static NSSet* org_apache_cordova_contacts_W3CtoNull = nil; +static NSDictionary* org_apache_cordova_contacts_objectAndProperties = nil; +static NSDictionary* org_apache_cordova_contacts_defaultFields = nil; + +@implementation CDVContact : NSObject + + @synthesize returnFields; + +- (id)init +{ + if ((self = [super init]) != nil) { + ABRecordRef rec = ABPersonCreate(); + self.record = rec; + if (rec) { + CFRelease(rec); + } + } + return self; +} + +- (id)initFromABRecord:(ABRecordRef)aRecord +{ + if ((self = [super init]) != nil) { + self.record = aRecord; + } + return self; +} + +/* synthesize 'record' ourselves to have retain properties for CF types */ + +- (void)setRecord:(ABRecordRef)aRecord +{ + if (record != NULL) { + CFRelease(record); + } + if (aRecord != NULL) { + record = CFRetain(aRecord); + } +} + +- (ABRecordRef)record +{ + return record; +} + +/* Rather than creating getters and setters for each AddressBook (AB) Property, generic methods are used to deal with + * simple properties, MultiValue properties( phone numbers and emails) and MultiValueDictionary properties (Ims and addresses). + * The dictionaries below are used to translate between the W3C identifiers and the AB properties. Using the dictionaries, + * allows looping through sets of properties to extract from or set into the W3C dictionary to/from the ABRecord. + */ + +/* The two following dictionaries translate between W3C properties and AB properties. It currently mixes both + * Properties (kABPersonAddressProperty for example) and Strings (kABPersonAddressStreetKey) so users should be aware of + * what types of values are expected. + * a bit. +*/ ++ (NSDictionary*)defaultABtoW3C +{ + if (org_apache_cordova_contacts_ABtoW3C == nil) { + org_apache_cordova_contacts_ABtoW3C = [NSDictionary dictionaryWithObjectsAndKeys: + kW3ContactNickname, [NSNumber numberWithInt:kABPersonNicknameProperty], + kW3ContactGivenName, [NSNumber numberWithInt:kABPersonFirstNameProperty], + kW3ContactFamilyName, [NSNumber numberWithInt:kABPersonLastNameProperty], + kW3ContactMiddleName, [NSNumber numberWithInt:kABPersonMiddleNameProperty], + kW3ContactHonorificPrefix, [NSNumber numberWithInt:kABPersonPrefixProperty], + kW3ContactHonorificSuffix, [NSNumber numberWithInt:kABPersonSuffixProperty], + kW3ContactPhoneNumbers, [NSNumber numberWithInt:kABPersonPhoneProperty], + kW3ContactAddresses, [NSNumber numberWithInt:kABPersonAddressProperty], + kW3ContactStreetAddress, kABPersonAddressStreetKey, + kW3ContactLocality, kABPersonAddressCityKey, + kW3ContactRegion, kABPersonAddressStateKey, + kW3ContactPostalCode, kABPersonAddressZIPKey, + kW3ContactCountry, kABPersonAddressCountryKey, + kW3ContactEmails, [NSNumber numberWithInt:kABPersonEmailProperty], + kW3ContactIms, [NSNumber numberWithInt:kABPersonInstantMessageProperty], + kW3ContactOrganizations, [NSNumber numberWithInt:kABPersonOrganizationProperty], + kW3ContactOrganizationName, [NSNumber numberWithInt:kABPersonOrganizationProperty], + kW3ContactTitle, [NSNumber numberWithInt:kABPersonJobTitleProperty], + kW3ContactDepartment, [NSNumber numberWithInt:kABPersonDepartmentProperty], + kW3ContactBirthday, [NSNumber numberWithInt:kABPersonBirthdayProperty], + kW3ContactUrls, [NSNumber numberWithInt:kABPersonURLProperty], + kW3ContactNote, [NSNumber numberWithInt:kABPersonNoteProperty], + nil]; + } + + return org_apache_cordova_contacts_ABtoW3C; +} + ++ (NSDictionary*)defaultW3CtoAB +{ + if (org_apache_cordova_contacts_W3CtoAB == nil) { + org_apache_cordova_contacts_W3CtoAB = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:kABPersonNicknameProperty], kW3ContactNickname, + [NSNumber numberWithInt:kABPersonFirstNameProperty], kW3ContactGivenName, + [NSNumber numberWithInt:kABPersonLastNameProperty], kW3ContactFamilyName, + [NSNumber numberWithInt:kABPersonMiddleNameProperty], kW3ContactMiddleName, + [NSNumber numberWithInt:kABPersonPrefixProperty], kW3ContactHonorificPrefix, + [NSNumber numberWithInt:kABPersonSuffixProperty], kW3ContactHonorificSuffix, + [NSNumber numberWithInt:kABPersonPhoneProperty], kW3ContactPhoneNumbers, + [NSNumber numberWithInt:kABPersonAddressProperty], kW3ContactAddresses, + kABPersonAddressStreetKey, kW3ContactStreetAddress, + kABPersonAddressCityKey, kW3ContactLocality, + kABPersonAddressStateKey, kW3ContactRegion, + kABPersonAddressZIPKey, kW3ContactPostalCode, + kABPersonAddressCountryKey, kW3ContactCountry, + [NSNumber numberWithInt:kABPersonEmailProperty], kW3ContactEmails, + [NSNumber numberWithInt:kABPersonInstantMessageProperty], kW3ContactIms, + [NSNumber numberWithInt:kABPersonOrganizationProperty], kW3ContactOrganizations, + [NSNumber numberWithInt:kABPersonJobTitleProperty], kW3ContactTitle, + [NSNumber numberWithInt:kABPersonDepartmentProperty], kW3ContactDepartment, + [NSNumber numberWithInt:kABPersonBirthdayProperty], kW3ContactBirthday, + [NSNumber numberWithInt:kABPersonNoteProperty], kW3ContactNote, + [NSNumber numberWithInt:kABPersonURLProperty], kW3ContactUrls, + kABPersonInstantMessageUsernameKey, kW3ContactImValue, + kABPersonInstantMessageServiceKey, kW3ContactImType, + [NSNull null], kW3ContactFieldType, /* include entries in dictionary to indicate ContactField properties */ + [NSNull null], kW3ContactFieldValue, + [NSNull null], kW3ContactFieldPrimary, + [NSNull null], kW3ContactFieldId, + [NSNumber numberWithInt:kABPersonOrganizationProperty], kW3ContactOrganizationName, /* careful, name is used multiple times*/ + nil]; + } + return org_apache_cordova_contacts_W3CtoAB; +} + ++ (NSSet*)defaultW3CtoNull +{ + // these are values that have no AddressBook Equivalent OR have not been implemented yet + if (org_apache_cordova_contacts_W3CtoNull == nil) { + org_apache_cordova_contacts_W3CtoNull = [NSSet setWithObjects:kW3ContactDisplayName, + kW3ContactCategories, kW3ContactFormattedName, nil]; + } + return org_apache_cordova_contacts_W3CtoNull; +} + +/* + * The objectAndProperties dictionary contains the all of the properties of the W3C Contact Objects specified by the key + * Used in calcReturnFields, and various extract methods + */ ++ (NSDictionary*)defaultObjectAndProperties +{ + if (org_apache_cordova_contacts_objectAndProperties == nil) { + org_apache_cordova_contacts_objectAndProperties = [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects:kW3ContactGivenName, kW3ContactFamilyName, + kW3ContactMiddleName, kW3ContactHonorificPrefix, kW3ContactHonorificSuffix, kW3ContactFormattedName, nil], kW3ContactName, + [NSArray arrayWithObjects:kW3ContactStreetAddress, kW3ContactLocality, kW3ContactRegion, + kW3ContactPostalCode, kW3ContactCountry, /*kW3ContactAddressFormatted,*/ nil], kW3ContactAddresses, + [NSArray arrayWithObjects:kW3ContactOrganizationName, kW3ContactTitle, kW3ContactDepartment, nil], kW3ContactOrganizations, + [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactPhoneNumbers, + [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactEmails, + [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactPhotos, + [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactUrls, + [NSArray arrayWithObjects:kW3ContactImValue, kW3ContactImType, nil], kW3ContactIms, + nil]; + } + return org_apache_cordova_contacts_objectAndProperties; +} + ++ (NSDictionary*)defaultFields +{ + if (org_apache_cordova_contacts_defaultFields == nil) { + org_apache_cordova_contacts_defaultFields = [NSDictionary dictionaryWithObjectsAndKeys: + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactName], kW3ContactName, + [NSNull null], kW3ContactNickname, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactAddresses], kW3ContactAddresses, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactOrganizations], kW3ContactOrganizations, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactPhoneNumbers], kW3ContactPhoneNumbers, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactEmails], kW3ContactEmails, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactIms], kW3ContactIms, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactPhotos], kW3ContactPhotos, + [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactUrls], kW3ContactUrls, + [NSNull null], kW3ContactBirthday, + [NSNull null], kW3ContactNote, + nil]; + } + return org_apache_cordova_contacts_defaultFields; +} + +/* Translate W3C Contact data into ABRecordRef + * + * New contact information comes in as a NSMutableDictionary. All Null entries in Contact object are set + * as [NSNull null] in the dictionary when translating from the JSON input string of Contact data. However, if + * user did not set a value within a Contact object or sub-object (by not using the object constructor) some data + * may not exist. + * bUpdate = YES indicates this is a save of an existing record + */ +- (bool)setFromContactDict:(NSDictionary*)aContact asUpdate:(BOOL)bUpdate +{ + if (![aContact isKindOfClass:[NSDictionary class]]) { + return FALSE; // can't do anything if no dictionary! + } + + ABRecordRef person = self.record; + bool bSuccess = TRUE; + CFErrorRef error; + + // set name info + // iOS doesn't have displayName - might have to pull parts from it to create name + bool bName = false; + NSDictionary* dict = [aContact valueForKey:kW3ContactName]; + if ([dict isKindOfClass:[NSDictionary class]]) { + bName = true; + NSArray* propArray = [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactName]; + + for (id i in propArray) { + if (![(NSString*)i isEqualToString : kW3ContactFormattedName]) { // kW3ContactFormattedName is generated from ABRecordCopyCompositeName() and can't be set + [self setValue:[dict valueForKey:i] forProperty:(ABPropertyID)[(NSNumber*)[[CDVContact defaultW3CtoAB] objectForKey:i] intValue] + inRecord:person asUpdate:bUpdate]; + } + } + } + + id nn = [aContact valueForKey:kW3ContactNickname]; + if (![nn isKindOfClass:[NSNull class]]) { + bName = true; + [self setValue:nn forProperty:kABPersonNicknameProperty inRecord:person asUpdate:bUpdate]; + } + if (!bName) { + // if no name or nickname - try and use displayName as W3Contact must have displayName or ContactName + [self setValue:[aContact valueForKey:kW3ContactDisplayName] forProperty:kABPersonNicknameProperty + inRecord:person asUpdate:bUpdate]; + } + + // set phoneNumbers + // NSLog(@"setting phoneNumbers"); + NSArray* array = [aContact valueForKey:kW3ContactPhoneNumbers]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueStrings:array forProperty:kABPersonPhoneProperty inRecord:person asUpdate:bUpdate]; + } + // set Emails + // NSLog(@"setting emails"); + array = [aContact valueForKey:kW3ContactEmails]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueStrings:array forProperty:kABPersonEmailProperty inRecord:person asUpdate:bUpdate]; + } + // set Urls + // NSLog(@"setting urls"); + array = [aContact valueForKey:kW3ContactUrls]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueStrings:array forProperty:kABPersonURLProperty inRecord:person asUpdate:bUpdate]; + } + + // set multivalue dictionary properties + // set addresses: streetAddress, locality, region, postalCode, country + // set ims: value = username, type = servicetype + // iOS addresses and im are a MultiValue Properties with label, value=dictionary of info, and id + // NSLog(@"setting addresses"); + error = nil; + array = [aContact valueForKey:kW3ContactAddresses]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueDictionary:array forProperty:kABPersonAddressProperty inRecord:person asUpdate:bUpdate]; + } + // ims + // NSLog(@"setting ims"); + array = [aContact valueForKey:kW3ContactIms]; + if ([array isKindOfClass:[NSArray class]]) { + [self setMultiValueDictionary:array forProperty:kABPersonInstantMessageProperty inRecord:person asUpdate:bUpdate]; + } + + // organizations + // W3C ContactOrganization has pref, type, name, title, department + // iOS only supports name, title, department + // NSLog(@"setting organizations"); + // TODO this may need work - should Organization information be removed when array is empty?? + array = [aContact valueForKey:kW3ContactOrganizations]; // iOS only supports one organization - use first one + if ([array isKindOfClass:[NSArray class]]) { + BOOL bRemove = NO; + NSDictionary* dict = nil; + if ([array count] > 0) { + dict = [array objectAtIndex:0]; + } else { + // remove the organization info entirely + bRemove = YES; + } + if ([dict isKindOfClass:[NSDictionary class]] || (bRemove == YES)) { + [self setValue:(bRemove ? @"" : [dict valueForKey:@"name"]) forProperty:kABPersonOrganizationProperty inRecord:person asUpdate:bUpdate]; + [self setValue:(bRemove ? @"" : [dict valueForKey:kW3ContactTitle]) forProperty:kABPersonJobTitleProperty inRecord:person asUpdate:bUpdate]; + [self setValue:(bRemove ? @"" : [dict valueForKey:kW3ContactDepartment]) forProperty:kABPersonDepartmentProperty inRecord:person asUpdate:bUpdate]; + } + } + // add dates + // Dates come in as milliseconds in NSNumber Object + id ms = [aContact valueForKey:kW3ContactBirthday]; + NSDate* aDate = nil; + if (ms && [ms isKindOfClass:[NSNumber class]]) { + double msValue = [ms doubleValue]; + msValue = msValue / 1000; + aDate = [NSDate dateWithTimeIntervalSince1970:msValue]; + } + if ((aDate != nil) || [ms isKindOfClass:[NSString class]]) { + [self setValue:aDate != nil ? aDate:ms forProperty:kABPersonBirthdayProperty inRecord:person asUpdate:bUpdate]; + } + // don't update creation date + // modification date will get updated when save + // anniversary is removed from W3C Contact api Dec 9, 2010 spec - don't waste time on it yet + + // kABPersonDateProperty + + // kABPersonAnniversaryLabel + + // iOS doesn't have gender - ignore + // note + [self setValue:[aContact valueForKey:kW3ContactNote] forProperty:kABPersonNoteProperty inRecord:person asUpdate:bUpdate]; + + // iOS doesn't have preferredName- ignore + + // photo + array = [aContact valueForKey:kW3ContactPhotos]; + if ([array isKindOfClass:[NSArray class]]) { + if (bUpdate && ([array count] == 0)) { + // remove photo + bSuccess = ABPersonRemoveImageData(person, &error); + } else if ([array count] > 0) { + NSDictionary* dict = [array objectAtIndex:0]; // currently only support one photo + if ([dict isKindOfClass:[NSDictionary class]]) { + id value = [dict objectForKey:kW3ContactFieldValue]; + if ([value isKindOfClass:[NSString class]]) { + if (bUpdate && ([value length] == 0)) { + // remove the current image + bSuccess = ABPersonRemoveImageData(person, &error); + } else { + // use this image + // don't know if string is encoded or not so first unencode it then encode it again + NSString* cleanPath = [value stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSURL* photoUrl = [NSURL URLWithString:[cleanPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + // caller is responsible for checking for a connection, if no connection this will fail + NSError* err = nil; + NSData* data = nil; + if (photoUrl) { + data = [NSData dataWithContentsOfURL:photoUrl options:NSDataReadingUncached error:&err]; + } + if (data && ([data length] > 0)) { + bSuccess = ABPersonSetImageData(person, (__bridge CFDataRef)data, &error); + } + if (!data || !bSuccess) { + NSLog(@"error setting contact image: %@", (err != nil ? [err localizedDescription] : @"")); + } + } + } + } + } + } + + // TODO WebURLs + + // TODO timezone + + return bSuccess; +} + +/* Set item into an AddressBook Record for the specified property. + * aValue - the value to set into the address book (code checks for null or [NSNull null] + * aProperty - AddressBook property ID + * aRecord - the record to update + * bUpdate - whether this is a possible update vs a new entry + * RETURN + * true - property was set (or input value as null) + * false - property was not set + */ +- (bool)setValue:(id)aValue forProperty:(ABPropertyID)aProperty inRecord:(ABRecordRef)aRecord asUpdate:(BOOL)bUpdate +{ + bool bSuccess = true; // if property was null, just ignore and return success + CFErrorRef error; + + if (aValue && ![aValue isKindOfClass:[NSNull class]]) { + if (bUpdate && ([aValue isKindOfClass:[NSString class]] && ([aValue length] == 0))) { // if updating, empty string means to delete + aValue = NULL; + } // really only need to set if different - more efficient to just update value or compare and only set if necessary??? + bSuccess = ABRecordSetValue(aRecord, aProperty, (__bridge CFTypeRef)aValue, &error); + if (!bSuccess) { + NSLog(@"error setting %d property", aProperty); + } + } + + return bSuccess; +} + +- (bool)removeProperty:(ABPropertyID)aProperty inRecord:(ABRecordRef)aRecord +{ + CFErrorRef err; + bool bSuccess = ABRecordRemoveValue(aRecord, aProperty, &err); + + if (!bSuccess) { + CFStringRef errDescription = CFErrorCopyDescription(err); + NSLog(@"Unable to remove property %d: %@", aProperty, errDescription); + CFRelease(errDescription); + } + return bSuccess; +} + +- (bool)addToMultiValue:(ABMultiValueRef)multi fromDictionary:dict +{ + bool bSuccess = FALSE; + id value = [dict valueForKey:kW3ContactFieldValue]; + + if (IS_VALID_VALUE(value)) { + CFStringRef label = [CDVContact convertContactTypeToPropertyLabel:[dict valueForKey:kW3ContactFieldType]]; + bSuccess = ABMultiValueAddValueAndLabel(multi, (__bridge CFTypeRef)value, label, NULL); + if (!bSuccess) { + NSLog(@"Error setting Value: %@ and label: %@", value, label); + } + } + return bSuccess; +} + +- (ABMultiValueRef)allocStringMultiValueFromArray:array +{ + ABMutableMultiValueRef multi = ABMultiValueCreateMutable(kABMultiStringPropertyType); + + for (NSDictionary* dict in array) { + [self addToMultiValue:multi fromDictionary:dict]; + } + + return multi; // caller is responsible for releasing multi +} + +- (bool)setValue:(CFTypeRef)value forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person +{ + CFErrorRef error; + bool bSuccess = ABRecordSetValue(person, prop, value, &error); + + if (!bSuccess) { + NSLog(@"Error setting value for property: %d", prop); + } + return bSuccess; +} + +/* Set MultiValue string properties into Address Book Record. + * NSArray* fieldArray - array of dictionaries containing W3C properties to be set into record + * ABPropertyID prop - the property to be set (generally used for phones and emails) + * ABRecordRef person - the record to set values into + * BOOL bUpdate - whether or not to update date or set as new. + * When updating: + * empty array indicates to remove entire property + * empty string indicates to remove + * [NSNull null] do not modify (keep existing record value) + * RETURNS + * bool false indicates error + * + * used for phones and emails + */ +- (bool)setMultiValueStrings:(NSArray*)fieldArray forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate +{ + bool bSuccess = TRUE; + ABMutableMultiValueRef multi = nil; + + if (!bUpdate) { + multi = [self allocStringMultiValueFromArray:fieldArray]; + bSuccess = [self setValue:multi forProperty:prop inRecord:person]; + } else if (bUpdate && ([fieldArray count] == 0)) { + // remove entire property + bSuccess = [self removeProperty:prop inRecord:person]; + } else { // check for and apply changes + ABMultiValueRef copy = ABRecordCopyValue(person, prop); + if (copy != nil) { + multi = ABMultiValueCreateMutableCopy(copy); + CFRelease(copy); + + for (NSDictionary* dict in fieldArray) { + id val; + NSString* label = nil; + val = [dict valueForKey:kW3ContactFieldValue]; + label = (__bridge NSString*)[CDVContact convertContactTypeToPropertyLabel:[dict valueForKey:kW3ContactFieldType]]; + if (IS_VALID_VALUE(val)) { + // is an update, find index of entry with matching id, if values are different, update. + id idValue = [dict valueForKey:kW3ContactFieldId]; + int identifier = [idValue isKindOfClass:[NSNumber class]] ? [idValue intValue] : -1; + CFIndex i = identifier >= 0 ? ABMultiValueGetIndexForIdentifier(multi, identifier) : kCFNotFound; + if (i != kCFNotFound) { + if ([val length] == 0) { + // remove both value and label + ABMultiValueRemoveValueAndLabelAtIndex(multi, i); + } else { + NSString* valueAB = (__bridge_transfer NSString*)ABMultiValueCopyValueAtIndex(multi, i); + NSString* labelAB = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, i); + if ((valueAB == nil) || ![val isEqualToString:valueAB]) { + ABMultiValueReplaceValueAtIndex(multi, (__bridge CFTypeRef)val, i); + } + if ((labelAB == nil) || ![label isEqualToString:labelAB]) { + ABMultiValueReplaceLabelAtIndex(multi, (__bridge CFStringRef)label, i); + } + } + } else { + // is a new value - insert + [self addToMultiValue:multi fromDictionary:dict]; + } + } // end of if value + } // end of for + } else { // adding all new value(s) + multi = [self allocStringMultiValueFromArray:fieldArray]; + } + // set the (updated) copy as the new value + bSuccess = [self setValue:multi forProperty:prop inRecord:person]; + } + + if (multi) { + CFRelease(multi); + } + + return bSuccess; +} + +// used for ims and addresses +- (ABMultiValueRef)allocDictMultiValueFromArray:array forProperty:(ABPropertyID)prop +{ + ABMutableMultiValueRef multi = ABMultiValueCreateMutable(kABMultiDictionaryPropertyType); + NSMutableDictionary* newDict; + NSMutableDictionary* addDict; + + for (NSDictionary* dict in array) { + newDict = [self translateW3Dict:dict forProperty:prop]; + addDict = [NSMutableDictionary dictionaryWithCapacity:2]; + if (newDict) { // create a new dictionary with a Label and Value, value is the dictionary previously created + // June, 2011 W3C Contact spec adds type into ContactAddress book + // get the type out of the original dictionary for address + NSString* addrType = (NSString*)[dict valueForKey:kW3ContactFieldType]; + if (!addrType) { + addrType = (NSString*)kABOtherLabel; + } + NSObject* typeValue = ((prop == kABPersonInstantMessageProperty) ? (NSObject*)kABOtherLabel : addrType); + // NSLog(@"typeValue: %@", typeValue); + [addDict setObject:typeValue forKey:kW3ContactFieldType]; // im labels will be set as Other and address labels as type from dictionary + [addDict setObject:newDict forKey:kW3ContactFieldValue]; + [self addToMultiValue:multi fromDictionary:addDict]; + } + } + + return multi; // caller is responsible for releasing +} + +// used for ims and addresses to convert W3 dictionary of values to AB Dictionary +// got messier when June, 2011 W3C Contact spec added type field into ContactAddress +- (NSMutableDictionary*)translateW3Dict:(NSDictionary*)dict forProperty:(ABPropertyID)prop +{ + NSArray* propArray = [[CDVContact defaultObjectAndProperties] valueForKey:[[CDVContact defaultABtoW3C] objectForKey:[NSNumber numberWithInt:prop]]]; + + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:1]; + id value; + + for (NSString* key in propArray) { // for each W3 Contact key get the value + if (((value = [dict valueForKey:key]) != nil) && ![value isKindOfClass:[NSNull class]]) { + // if necessary convert the W3 value to AB Property label + NSString* setValue = value; + if ([CDVContact needsConversion:key]) { // IM types must be converted + setValue = (NSString*)[CDVContact convertContactTypeToPropertyLabel:value]; + // IMs must have a valid AB value! + if ((prop == kABPersonInstantMessageProperty) && [setValue isEqualToString:(NSString*)kABOtherLabel]) { + setValue = @""; // try empty string + } + } + // set the AB value into the dictionary + [newDict setObject:setValue forKey:(NSString*)[[CDVContact defaultW3CtoAB] valueForKey:(NSString*)key]]; + } + } + + if ([newDict count] == 0) { + newDict = nil; // no items added + } + return newDict; +} + +/* set multivalue dictionary properties into an AddressBook Record + * NSArray* array - array of dictionaries containing the W3C properties to set into the record + * ABPropertyID prop - the property id for the multivalue dictionary (addresses and ims) + * ABRecordRef person - the record to set the values into + * BOOL bUpdate - YES if this is an update to an existing record + * When updating: + * empty array indicates to remove entire property + * value/label == "" indicates to remove + * value/label == [NSNull null] do not modify (keep existing record value) + * RETURN + * bool false indicates fatal error + * + * iOS addresses and im are a MultiValue Properties with label, value=dictionary of info, and id + * set addresses: streetAddress, locality, region, postalCode, country + * set ims: value = username, type = servicetype + * there are some special cases in here for ims - needs cleanup / simplification + * + */ +- (bool)setMultiValueDictionary:(NSArray*)array forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate +{ + bool bSuccess = FALSE; + ABMutableMultiValueRef multi = nil; + + if (!bUpdate) { + multi = [self allocDictMultiValueFromArray:array forProperty:prop]; + bSuccess = [self setValue:multi forProperty:prop inRecord:person]; + } else if (bUpdate && ([array count] == 0)) { + // remove property + bSuccess = [self removeProperty:prop inRecord:person]; + } else { // check for and apply changes + ABMultiValueRef copy = ABRecordCopyValue(person, prop); + if (copy) { + multi = ABMultiValueCreateMutableCopy(copy); + CFRelease(copy); + // get the W3C values for this property + NSArray* propArray = [[CDVContact defaultObjectAndProperties] valueForKey:[[CDVContact defaultABtoW3C] objectForKey:[NSNumber numberWithInt:prop]]]; + id value; + id valueAB; + + for (NSDictionary* field in array) { + NSMutableDictionary* dict; + // find the index for the current property + id idValue = [field valueForKey:kW3ContactFieldId]; + int identifier = [idValue isKindOfClass:[NSNumber class]] ? [idValue intValue] : -1; + CFIndex idx = identifier >= 0 ? ABMultiValueGetIndexForIdentifier(multi, identifier) : kCFNotFound; + BOOL bUpdateLabel = NO; + if (idx != kCFNotFound) { + dict = [NSMutableDictionary dictionaryWithCapacity:1]; + // NSDictionary* existingDictionary = (NSDictionary*)ABMultiValueCopyValueAtIndex(multi, idx); + CFTypeRef existingDictionary = ABMultiValueCopyValueAtIndex(multi, idx); + NSString* existingABLabel = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, idx); + NSString* testLabel = [field valueForKey:kW3ContactFieldType]; + // fixes cb-143 where setting empty label could cause address to not be removed + // (because empty label would become 'other' in convertContactTypeToPropertyLabel + // which may not have matched existing label thus resulting in an incorrect updating of the label + // and the address not getting removed at the end of the for loop) + if (testLabel && [testLabel isKindOfClass:[NSString class]] && ([testLabel length] > 0)) { + CFStringRef w3cLabel = [CDVContact convertContactTypeToPropertyLabel:testLabel]; + if (w3cLabel && ![existingABLabel isEqualToString:(__bridge NSString*)w3cLabel]) { + // replace the label + ABMultiValueReplaceLabelAtIndex(multi, w3cLabel, idx); + bUpdateLabel = YES; + } + } // else was invalid or empty label string so do not update + + for (id k in propArray) { + value = [field valueForKey:k]; + bool bSet = (value != nil && ![value isKindOfClass:[NSNull class]] && ([value isKindOfClass:[NSString class]] && [value length] > 0)); + // if there is a contact value, put it into dictionary + if (bSet) { + NSString* setValue = [CDVContact needsConversion:(NSString*)k] ? (NSString*)[CDVContact convertContactTypeToPropertyLabel:value] : value; + [dict setObject:setValue forKey:(NSString*)[[CDVContact defaultW3CtoAB] valueForKey:(NSString*)k]]; + } else if ((value == nil) || ([value isKindOfClass:[NSString class]] && ([value length] != 0))) { + // value not provided in contact dictionary - if prop exists in AB dictionary, preserve it + valueAB = [(__bridge NSDictionary*)existingDictionary valueForKey : [[CDVContact defaultW3CtoAB] valueForKey:k]]; + if (valueAB != nil) { + [dict setValue:valueAB forKey:[[CDVContact defaultW3CtoAB] valueForKey:k]]; + } + } // else if value == "" it will not be added into updated dict and thus removed + } // end of for loop (moving here fixes cb-143, need to end for loop before replacing or removing multivalue) + + if ([dict count] > 0) { + // something was added into new dict, + ABMultiValueReplaceValueAtIndex(multi, (__bridge CFTypeRef)dict, idx); + } else if (!bUpdateLabel) { + // nothing added into new dict and no label change so remove this property entry + ABMultiValueRemoveValueAndLabelAtIndex(multi, idx); + } + + CFRelease(existingDictionary); + } else { + // not found in multivalue so add it + dict = [self translateW3Dict:field forProperty:prop]; + if (dict) { + NSMutableDictionary* addDict = [NSMutableDictionary dictionaryWithCapacity:2]; + // get the type out of the original dictionary for address + NSObject* typeValue = ((prop == kABPersonInstantMessageProperty) ? (NSObject*)kABOtherLabel : (NSString*)[field valueForKey:kW3ContactFieldType]); + // NSLog(@"typeValue: %@", typeValue); + [addDict setObject:typeValue forKey:kW3ContactFieldType]; // im labels will be set as Other and address labels as type from dictionary + [addDict setObject:dict forKey:kW3ContactFieldValue]; + [self addToMultiValue:multi fromDictionary:addDict]; + } + } + } // end of looping through dictionaries + + // set the (updated) copy as the new value + bSuccess = [self setValue:multi forProperty:prop inRecord:person]; + } + } // end of copy and apply changes + if (multi) { + CFRelease(multi); + } + + return bSuccess; +} + +/* Determine which W3C labels need to be converted + */ ++ (BOOL)needsConversion:(NSString*)W3Label +{ + BOOL bConvert = NO; + + if ([W3Label isEqualToString:kW3ContactFieldType] || [W3Label isEqualToString:kW3ContactImType]) { + bConvert = YES; + } + return bConvert; +} + +/* Translation of property type labels contact API ---> iPhone + * + * phone: work, home, other, mobile, fax, pager --> + * kABWorkLabel, kABHomeLabel, kABOtherLabel, kABPersonPhoneMobileLabel, kABPersonHomeFAXLabel || kABPersonHomeFAXLabel, kABPersonPhonePagerLabel + * emails: work, home, other ---> kABWorkLabel, kABHomeLabel, kABOtherLabel + * ims: aim, gtalk, icq, xmpp, msn, skype, qq, yahoo --> kABPersonInstantMessageService + (AIM, ICG, MSN, Yahoo). No support for gtalk, xmpp, skype, qq + * addresses: work, home, other --> kABWorkLabel, kABHomeLabel, kABOtherLabel + * + * + */ ++ (CFStringRef)convertContactTypeToPropertyLabel:(NSString*)label +{ + CFStringRef type; + + if ([label isKindOfClass:[NSNull class]] || ![label isKindOfClass:[NSString class]]) { + type = NULL; // no label + } else if ([label caseInsensitiveCompare:kW3ContactWorkLabel] == NSOrderedSame) { + type = kABWorkLabel; + } else if ([label caseInsensitiveCompare:kW3ContactHomeLabel] == NSOrderedSame) { + type = kABHomeLabel; + } else if ([label caseInsensitiveCompare:kW3ContactOtherLabel] == NSOrderedSame) { + type = kABOtherLabel; + } else if ([label caseInsensitiveCompare:kW3ContactPhoneMobileLabel] == NSOrderedSame) { + type = kABPersonPhoneMobileLabel; + } else if ([label caseInsensitiveCompare:kW3ContactPhonePagerLabel] == NSOrderedSame) { + type = kABPersonPhonePagerLabel; + } else if ([label caseInsensitiveCompare:kW3ContactImAIMLabel] == NSOrderedSame) { + type = kABPersonInstantMessageServiceAIM; + } else if ([label caseInsensitiveCompare:kW3ContactImICQLabel] == NSOrderedSame) { + type = kABPersonInstantMessageServiceICQ; + } else if ([label caseInsensitiveCompare:kW3ContactImMSNLabel] == NSOrderedSame) { + type = kABPersonInstantMessageServiceMSN; + } else if ([label caseInsensitiveCompare:kW3ContactImYahooLabel] == NSOrderedSame) { + type = kABPersonInstantMessageServiceYahoo; + } else if ([label caseInsensitiveCompare:kW3ContactUrlProfile] == NSOrderedSame) { + type = kABPersonHomePageLabel; + } else { + type = kABOtherLabel; + } + + return type; +} + ++ (NSString*)convertPropertyLabelToContactType:(NSString*)label +{ + NSString* type = nil; + + if (label != nil) { // improve efficiency...... + if ([label isEqualToString:(NSString*)kABPersonPhoneMobileLabel]) { + type = kW3ContactPhoneMobileLabel; + } else if ([label isEqualToString:(NSString*)kABPersonPhoneHomeFAXLabel] || + [label isEqualToString:(NSString*)kABPersonPhoneWorkFAXLabel]) { + type = kW3ContactPhoneFaxLabel; + } else if ([label isEqualToString:(NSString*)kABPersonPhonePagerLabel]) { + type = kW3ContactPhonePagerLabel; + } else if ([label isEqualToString:(NSString*)kABHomeLabel]) { + type = kW3ContactHomeLabel; + } else if ([label isEqualToString:(NSString*)kABWorkLabel]) { + type = kW3ContactWorkLabel; + } else if ([label isEqualToString:(NSString*)kABOtherLabel]) { + type = kW3ContactOtherLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceAIM]) { + type = kW3ContactImAIMLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceICQ]) { + type = kW3ContactImICQLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceJabber]) { + type = kW3ContactOtherLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceMSN]) { + type = kW3ContactImMSNLabel; + } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceYahoo]) { + type = kW3ContactImYahooLabel; + } else if ([label isEqualToString:(NSString*)kABPersonHomePageLabel]) { + type = kW3ContactUrlProfile; + } else { + type = kW3ContactOtherLabel; + } + } + return type; +} + +/* Check if the input label is a valid W3C ContactField.type. This is used when searching, + * only search field types if the search string is a valid type. If we converted any search + * string to a ABPropertyLabel it could convert to kABOtherLabel which is probably not want + * the user wanted to search for and could skew the results. + */ ++ (BOOL)isValidW3ContactType:(NSString*)label +{ + BOOL isValid = NO; + + if ([label isKindOfClass:[NSNull class]] || ![label isKindOfClass:[NSString class]]) { + isValid = NO; // no label + } else if ([label caseInsensitiveCompare:kW3ContactWorkLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactHomeLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactOtherLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactPhoneMobileLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactPhonePagerLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactImAIMLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactImICQLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactImMSNLabel] == NSOrderedSame) { + isValid = YES; + } else if ([label caseInsensitiveCompare:kW3ContactImYahooLabel] == NSOrderedSame) { + isValid = YES; + } else { + isValid = NO; + } + + return isValid; +} + +/* Create a new Contact Dictionary object from an ABRecordRef that contains information in a format such that + * it can be returned to JavaScript callback as JSON object string. + * Uses: + * ABRecordRef set into Contact Object + * NSDictionary withFields indicates which fields to return from the AddressBook Record + * + * JavaScript Contact: + * @param {DOMString} id unique identifier + * @param {DOMString} displayName + * @param {ContactName} name + * @param {DOMString} nickname + * @param {ContactField[]} phoneNumbers array of phone numbers + * @param {ContactField[]} emails array of email addresses + * @param {ContactAddress[]} addresses array of addresses + * @param {ContactField[]} ims instant messaging user ids + * @param {ContactOrganization[]} organizations + * @param {DOMString} published date contact was first created + * @param {DOMString} updated date contact was last updated + * @param {DOMString} birthday contact's birthday + * @param (DOMString} anniversary contact's anniversary + * @param {DOMString} gender contact's gender + * @param {DOMString} note user notes about contact + * @param {DOMString} preferredUsername + * @param {ContactField[]} photos + * @param {ContactField[]} tags + * @param {ContactField[]} relationships + * @param {ContactField[]} urls contact's web sites + * @param {ContactAccounts[]} accounts contact's online accounts + * @param {DOMString} timezone UTC time zone offset + * @param {DOMString} connected + */ + +- (NSDictionary*)toDictionary:(NSDictionary*)withFields +{ + // if not a person type record bail out for now + if (ABRecordGetRecordType(self.record) != kABPersonType) { + return NULL; + } + id value = nil; + self.returnFields = withFields; + + NSMutableDictionary* nc = [NSMutableDictionary dictionaryWithCapacity:1]; // new contact dictionary to fill in from ABRecordRef + // id + [nc setObject:[NSNumber numberWithInt:ABRecordGetRecordID(self.record)] forKey:kW3ContactId]; + if (self.returnFields == nil) { + // if no returnFields specified, W3C says to return empty contact (but Cordova will at least return id) + return nc; + } + if ([self.returnFields objectForKey:kW3ContactDisplayName]) { + // displayname requested - iOS doesn't have so return null + [nc setObject:[NSNull null] forKey:kW3ContactDisplayName]; + // may overwrite below if requested ContactName and there are no values + } + // nickname + if ([self.returnFields valueForKey:kW3ContactNickname]) { + value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, kABPersonNicknameProperty); + [nc setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactNickname]; + } + + // name dictionary + // NSLog(@"getting name info"); + NSObject* data = [self extractName]; + if (data != nil) { + [nc setObject:data forKey:kW3ContactName]; + } + if ([self.returnFields objectForKey:kW3ContactDisplayName] && ((data == nil) || ([(NSDictionary*)data objectForKey : kW3ContactFormattedName] == [NSNull null]))) { + // user asked for displayName which iOS doesn't support but there is no other name data being returned + // try and use Composite Name so some name is returned + id tryName = (__bridge_transfer NSString*)ABRecordCopyCompositeName(self.record); + if (tryName != nil) { + [nc setObject:tryName forKey:kW3ContactDisplayName]; + } else { + // use nickname or empty string + value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, kABPersonNicknameProperty); + [nc setObject:(value != nil) ? value:@"" forKey:kW3ContactDisplayName]; + } + } + // phoneNumbers array + // NSLog(@"getting phoneNumbers"); + value = [self extractMultiValue:kW3ContactPhoneNumbers]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactPhoneNumbers]; + } + // emails array + // NSLog(@"getting emails"); + value = [self extractMultiValue:kW3ContactEmails]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactEmails]; + } + // urls array + value = [self extractMultiValue:kW3ContactUrls]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactUrls]; + } + // addresses array + // NSLog(@"getting addresses"); + value = [self extractAddresses]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactAddresses]; + } + // im array + // NSLog(@"getting ims"); + value = [self extractIms]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactIms]; + } + // organization array (only info for one organization in iOS) + // NSLog(@"getting organizations"); + value = [self extractOrganizations]; + if (value != nil) { + [nc setObject:value forKey:kW3ContactOrganizations]; + } + + // for simple properties, could make this a bit more efficient by storing all simple properties in a single + // array in the returnFields dictionary and setting them via a for loop through the array + + // add dates + // NSLog(@"getting dates"); + NSNumber* ms; + + /** Contact Revision field removed from June 16, 2011 version of specification + + if ([self.returnFields valueForKey:kW3ContactUpdated]){ + ms = [self getDateAsNumber: kABPersonModificationDateProperty]; + if (!ms){ + // try and get published date + ms = [self getDateAsNumber: kABPersonCreationDateProperty]; + } + if (ms){ + [nc setObject: ms forKey:kW3ContactUpdated]; + } + + } + */ + + if ([self.returnFields valueForKey:kW3ContactBirthday]) { + ms = [self getDateAsNumber:kABPersonBirthdayProperty]; + if (ms) { + [nc setObject:ms forKey:kW3ContactBirthday]; + } + } + + /* Anniversary removed from 12-09-2010 W3C Contacts api spec + if ([self.returnFields valueForKey:kW3ContactAnniversary]){ + // Anniversary date is stored in a multivalue property + ABMultiValueRef multi = ABRecordCopyValue(self.record, kABPersonDateProperty); + if (multi){ + CFStringRef label = nil; + CFIndex count = ABMultiValueGetCount(multi); + // see if contains an Anniversary date + for(CFIndex i=0; i 0) { // ?? this will always be true since we set id,label,primary field?? + [(NSMutableArray*)addresses addObject : newAddress]; + } + CFRelease(dict); + } // end of loop through addresses + } else { + addresses = [NSNull null]; + } + if (multi) { + CFRelease(multi); + } + + return addresses; +} + +/* Create array of Dictionaries to match JavaScript ContactField object for ims + * type one of [aim, gtalk, icq, xmpp, msn, skype, qq, yahoo] needs other as well + * value + * (bool) primary + * id + * + * iOS IMs are a MultiValue Properties with label, value=dictionary of IM details (service, username), and id + */ +- (NSObject*)extractIms +{ + NSArray* fields = [self.returnFields objectForKey:kW3ContactIms]; + + if (fields == nil) { // no name fields requested + return nil; + } + NSObject* imArray; + ABMultiValueRef multi = ABRecordCopyValue(self.record, kABPersonInstantMessageProperty); + CFIndex count = multi ? ABMultiValueGetCount(multi) : 0; + if (count) { + imArray = [NSMutableArray arrayWithCapacity:count]; + + for (CFIndex i = 0; i < ABMultiValueGetCount(multi); i++) { + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:3]; + // iOS has label property (work, home, other) for each IM but W3C contact API doesn't use + CFDictionaryRef dict = (CFDictionaryRef)ABMultiValueCopyValueAtIndex(multi, i); + CFStringRef value; // all values should be CFStringRefs / NSString* + bool bFound; + if ([fields containsObject:kW3ContactFieldValue]) { + // value = user name + bFound = CFDictionaryGetValueIfPresent(dict, kABPersonInstantMessageUsernameKey, (void*)&value); + if (bFound && (value != NULL)) { + CFRetain(value); + [newDict setObject:(__bridge id)value forKey:kW3ContactFieldValue]; + CFRelease(value); + } else { + [newDict setObject:[NSNull null] forKey:kW3ContactFieldValue]; + } + } + if ([fields containsObject:kW3ContactFieldType]) { + bFound = CFDictionaryGetValueIfPresent(dict, kABPersonInstantMessageServiceKey, (void*)&value); + if (bFound && (value != NULL)) { + CFRetain(value); + [newDict setObject:(id)[[CDVContact class] convertPropertyLabelToContactType : (__bridge NSString*)value] forKey:kW3ContactFieldType]; + CFRelease(value); + } else { + [newDict setObject:[NSNull null] forKey:kW3ContactFieldType]; + } + } + // always set ID + id identifier = [NSNumber numberWithUnsignedInt:ABMultiValueGetIdentifierAtIndex(multi, i)]; + [newDict setObject:(identifier != nil) ? identifier:[NSNull null] forKey:kW3ContactFieldId]; + + [(NSMutableArray*)imArray addObject : newDict]; + CFRelease(dict); + } + } else { + imArray = [NSNull null]; + } + + if (multi) { + CFRelease(multi); + } + return imArray; +} + +/* Create array of Dictionaries to match JavaScript ContactOrganization object + * pref - not supported in iOS + * type - not supported in iOS + * name + * department + * title + */ + +- (NSObject*)extractOrganizations +{ + NSArray* fields = [self.returnFields objectForKey:kW3ContactOrganizations]; + + if (fields == nil) { // no name fields requested + return nil; + } + NSObject* array = nil; + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:5]; + id value; + int validValueCount = 0; + + for (id i in fields) { + id key = [[CDVContact defaultW3CtoAB] valueForKey:i]; + if (key && [key isKindOfClass:[NSNumber class]]) { + value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, (ABPropertyID)[[[CDVContact defaultW3CtoAB] valueForKey:i] intValue]); + if (value != nil) { + // if there are no organization values we should return null for organization + // this counter keeps indicates if any organization values have been set + validValueCount++; + } + [newDict setObject:(value != nil) ? value:[NSNull null] forKey:i]; + } else { // not a key iOS supports, set to null + [newDict setObject:[NSNull null] forKey:i]; + } + } + + if (([newDict count] > 0) && (validValueCount > 0)) { + // add pref and type + // they are not supported by iOS and thus these values never change + [newDict setObject:@"false" forKey:kW3ContactFieldPrimary]; + [newDict setObject:[NSNull null] forKey:kW3ContactFieldType]; + array = [NSMutableArray arrayWithCapacity:1]; + [(NSMutableArray*)array addObject : newDict]; + } else { + array = [NSNull null]; + } + return array; +} + +// W3C Contacts expects an array of photos. Can return photos in more than one format, currently +// just returning the default format +// Save the photo data into tmp directory and return FileURI - temp directory is deleted upon application exit +- (NSObject*)extractPhotos +{ + NSMutableArray* photos = nil; + + if (ABPersonHasImageData(self.record)) { + CFDataRef photoData = ABPersonCopyImageData(self.record); + NSData* data = (__bridge NSData*)photoData; + // write to temp directory and store URI in photos array + // get the temp directory path + NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; + NSError* err = nil; + NSString* filePath = [NSString stringWithFormat:@"%@/photo_XXXXX", docsPath]; + char template[filePath.length + 1]; + strcpy(template, [filePath cStringUsingEncoding:NSASCIIStringEncoding]); + mkstemp(template); + filePath = [[NSFileManager defaultManager] + stringWithFileSystemRepresentation:template + length:strlen(template)]; + + // save file + if ([data writeToFile:filePath options:NSAtomicWrite error:&err]) { + photos = [NSMutableArray arrayWithCapacity:1]; + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:2]; + [newDict setObject:filePath forKey:kW3ContactFieldValue]; + [newDict setObject:@"url" forKey:kW3ContactFieldType]; + [newDict setObject:@"false" forKey:kW3ContactFieldPrimary]; + [photos addObject:newDict]; + } + + CFRelease(photoData); + } + return photos; +} + +/** + * given an array of W3C Contact field names, create a dictionary of field names to extract + * if field name represents an object, return all properties for that object: "name" - returns all properties in ContactName + * if field name is an explicit property, return only those properties: "name.givenName - returns a ContactName with only ContactName.givenName + * if field contains ONLY ["*"] return all fields + * dictionary format: + * key is W3Contact #define + * value is NSMutableArray* for complex keys: name,addresses,organizations, phone, emails, ims + * value is [NSNull null] for simple keys +*/ ++ (NSDictionary*)calcReturnFields:(NSArray*)fieldsArray // NSLog(@"getting self.returnFields"); +{ + NSMutableDictionary* d = [NSMutableDictionary dictionaryWithCapacity:1]; + + if ((fieldsArray != nil) && [fieldsArray isKindOfClass:[NSArray class]]) { + if (([fieldsArray count] == 1) && [[fieldsArray objectAtIndex:0] isEqualToString:@"*"]) { + return [CDVContact defaultFields]; // return all fields + } + + for (id i in fieldsArray) { + NSMutableArray* keys = nil; + NSString* fieldStr = nil; + if ([i isKindOfClass:[NSNumber class]]) { + fieldStr = [i stringValue]; + } else { + fieldStr = i; + } + + // see if this is specific property request in object - object.property + NSArray* parts = [fieldStr componentsSeparatedByString:@"."]; // returns original string if no separator found + NSString* name = [parts objectAtIndex:0]; + NSString* property = nil; + if ([parts count] > 1) { + property = [parts objectAtIndex:1]; + } + // see if this is a complex field by looking for its array of properties in objectAndProperties dictionary + id fields = [[CDVContact defaultObjectAndProperties] objectForKey:name]; + + // if find complex name (name,addresses,organizations, phone, emails, ims) in fields, add name as key + // with array of associated properties as the value + if ((fields != nil) && (property == nil)) { // request was for full object + keys = [NSMutableArray arrayWithArray:fields]; + if (keys != nil) { + [d setObject:keys forKey:name]; // will replace if prop array already exists + } + } else if ((fields != nil) && (property != nil)) { + // found an individual property request in form of name.property + // verify is real property name by using it as key in W3CtoAB + id abEquiv = [[CDVContact defaultW3CtoAB] objectForKey:property]; + if (abEquiv || [[CDVContact defaultW3CtoNull] containsObject:property]) { + // if existing array add to it + if ((keys = [d objectForKey:name]) != nil) { + [keys addObject:property]; + } else { + keys = [NSMutableArray arrayWithObject:property]; + [d setObject:keys forKey:name]; + } + } else { + NSLog(@"Contacts.find -- request for invalid property ignored: %@.%@", name, property); + } + } else { // is an individual property, verify is real property name by using it as key in W3CtoAB + id valid = [[CDVContact defaultW3CtoAB] objectForKey:name]; + if (valid || [[CDVContact defaultW3CtoNull] containsObject:name]) { + [d setObject:[NSNull null] forKey:name]; + } + } + } + } + if ([d count] == 0) { + // no array or nothing in the array. W3C spec says to return nothing + return nil; // [Contact defaultFields]; + } + return d; +} + +/* + * Search for the specified value in each of the fields specified in the searchFields dictionary. + * NSString* value - the string value to search for (need clarification from W3C on how to search for dates) + * NSDictionary* searchFields - a dictionary created via calcReturnFields where the key is the top level W3C + * object and the object is the array of specific fields within that object or null if it is a single property + * RETURNS + * YES as soon as a match is found in any of the fields + * NO - the specified value does not exist in any of the fields in this contact + * + * Note: I'm not a fan of returning in the middle of methods but have done it some in this method in order to + * keep the code simpler. bgibson + */ +- (BOOL)foundValue:(NSString*)testValue inFields:(NSDictionary*)searchFields +{ + BOOL bFound = NO; + + if ((testValue == nil) || ![testValue isKindOfClass:[NSString class]] || ([testValue length] == 0)) { + // nothing to find so return NO + return NO; + } + NSInteger valueAsInt = [testValue integerValue]; + + // per W3C spec, always include id in search + int recordId = ABRecordGetRecordID(self.record); + if (valueAsInt && (recordId == valueAsInt)) { + return YES; + } + + if (searchFields == nil) { + // no fields to search + return NO; + } + + if ([searchFields valueForKey:kW3ContactNickname]) { + bFound = [self testStringValue:testValue forW3CProperty:kW3ContactNickname]; + if (bFound == YES) { + return bFound; + } + } + + if ([searchFields valueForKeyIsArray:kW3ContactName]) { + // test name fields. All are string properties obtained via ABRecordCopyValue except kW3ContactFormattedName + NSArray* fields = [searchFields valueForKey:kW3ContactName]; + + for (NSString* testItem in fields) { + if ([testItem isEqualToString:kW3ContactFormattedName]) { + NSString* propValue = (__bridge_transfer NSString*)ABRecordCopyCompositeName(self.record); + if ((propValue != nil) && ([propValue length] > 0)) { + NSRange range = [propValue rangeOfString:testValue options:NSCaseInsensitiveSearch]; + bFound = (range.location != NSNotFound); + propValue = nil; + } + } else { + bFound = [self testStringValue:testValue forW3CProperty:testItem]; + } + + if (bFound) { + break; + } + } + } + if (!bFound && [searchFields valueForKeyIsArray:kW3ContactPhoneNumbers]) { + bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactPhoneNumbers] + forMVStringProperty:kABPersonPhoneProperty withValue:testValue]; + } + if (!bFound && [searchFields valueForKeyIsArray:kW3ContactEmails]) { + bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactEmails] + forMVStringProperty:kABPersonEmailProperty withValue:testValue]; + } + + if (!bFound && [searchFields valueForKeyIsArray:kW3ContactAddresses]) { + bFound = [self searchContactFields:[searchFields valueForKey:kW3ContactAddresses] + forMVDictionaryProperty:kABPersonAddressProperty withValue:testValue]; + } + + if (!bFound && [searchFields valueForKeyIsArray:kW3ContactIms]) { + bFound = [self searchContactFields:[searchFields valueForKey:kW3ContactIms] + forMVDictionaryProperty:kABPersonInstantMessageProperty withValue:testValue]; + } + + if (!bFound && [searchFields valueForKeyIsArray:kW3ContactOrganizations]) { + NSArray* fields = [searchFields valueForKey:kW3ContactOrganizations]; + + for (NSString* testItem in fields) { + bFound = [self testStringValue:testValue forW3CProperty:testItem]; + if (bFound == YES) { + break; + } + } + } + if (!bFound && [searchFields valueForKey:kW3ContactNote]) { + bFound = [self testStringValue:testValue forW3CProperty:kW3ContactNote]; + } + + // if searching for a date field is requested, get the date field as a localized string then look for match against testValue in date string + // searching for photos is not supported + if (!bFound && [searchFields valueForKey:kW3ContactBirthday]) { + bFound = [self testDateValue:testValue forW3CProperty:kW3ContactBirthday]; + } + if (!bFound && [searchFields valueForKeyIsArray:kW3ContactUrls]) { + bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactUrls] + forMVStringProperty:kABPersonURLProperty withValue:testValue]; + } + + return bFound; +} + +/* + * Test for the existence of a given string within the value of a ABPersonRecord string property based on the W3c property name. + * + * IN: + * NSString* testValue - the value to find - search is case insensitive + * NSString* property - the W3c property string + * OUT: + * BOOL YES if the given string was found within the property value + * NO if the testValue was not found, W3C property string was invalid or the AddressBook property was not a string + */ +- (BOOL)testStringValue:(NSString*)testValue forW3CProperty:(NSString*)property +{ + BOOL bFound = NO; + + if ([[CDVContact defaultW3CtoAB] valueForKeyIsNumber:property]) { + ABPropertyID propId = [[[CDVContact defaultW3CtoAB] objectForKey:property] intValue]; + if (ABPersonGetTypeOfProperty(propId) == kABStringPropertyType) { + NSString* propValue = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, propId); + if ((propValue != nil) && ([propValue length] > 0)) { + NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue]; + bFound = [containPred evaluateWithObject:propValue]; + // NSRange range = [propValue rangeOfString:testValue options: NSCaseInsensitiveSearch]; + // bFound = (range.location != NSNotFound); + } + } + } + return bFound; +} + +/* + * Test for the existence of a given Date string within the value of a ABPersonRecord datetime property based on the W3c property name. + * + * IN: + * NSString* testValue - the value to find - search is case insensitive + * NSString* property - the W3c property string + * OUT: + * BOOL YES if the given string was found within the localized date string value + * NO if the testValue was not found, W3C property string was invalid or the AddressBook property was not a DateTime + */ +- (BOOL)testDateValue:(NSString*)testValue forW3CProperty:(NSString*)property +{ + BOOL bFound = NO; + + if ([[CDVContact defaultW3CtoAB] valueForKeyIsNumber:property]) { + ABPropertyID propId = [[[CDVContact defaultW3CtoAB] objectForKey:property] intValue]; + if (ABPersonGetTypeOfProperty(propId) == kABDateTimePropertyType) { + NSDate* date = (__bridge_transfer NSDate*)ABRecordCopyValue(self.record, propId); + if (date != nil) { + NSString* dateString = [date descriptionWithLocale:[NSLocale currentLocale]]; + NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue]; + bFound = [containPred evaluateWithObject:dateString]; + } + } + } + return bFound; +} + +/* + * Search the specified fields within an AddressBook multivalue string property for the specified test value. + * Used for phoneNumbers, emails and urls. + * IN: + * NSArray* fields - the fields to search for within the multistring property (value and/or type) + * ABPropertyID - the property to search + * NSString* testValue - the value to search for. Will convert between W3C types and AB types. Will only + * search for types if the testValue is a valid ContactField type. + * OUT: + * YES if the test value was found in one of the specified fields + * NO if the test value was not found + */ +- (BOOL)searchContactFields:(NSArray*)fields forMVStringProperty:(ABPropertyID)propId withValue:testValue +{ + BOOL bFound = NO; + + for (NSString* type in fields) { + NSString* testString = nil; + if ([type isEqualToString:kW3ContactFieldType]) { + if ([CDVContact isValidW3ContactType:testValue]) { + // only search types if the filter string is a valid ContactField.type + testString = (NSString*)[CDVContact convertContactTypeToPropertyLabel:testValue]; + } + } else { + testString = testValue; + } + + if (testString != nil) { + bFound = [self testMultiValueStrings:testString forProperty:propId ofType:type]; + } + if (bFound == YES) { + break; + } + } + + return bFound; +} + +/* + * Searches a multiString value of the specified type for the specified test value. + * + * IN: + * NSString* testValue - the value to test for + * ABPropertyID propId - the property id of the multivalue property to search + * NSString* type - the W3C contact type to search for (value or type) + * OUT: + * YES is the test value was found + * NO if the test value was not found + */ +- (BOOL)testMultiValueStrings:(NSString*)testValue forProperty:(ABPropertyID)propId ofType:(NSString*)type +{ + BOOL bFound = NO; + + if (ABPersonGetTypeOfProperty(propId) == kABMultiStringPropertyType) { + NSArray* valueArray = nil; + if ([type isEqualToString:kW3ContactFieldType]) { + valueArray = [self labelsForProperty:propId inRecord:self.record]; + } else if ([type isEqualToString:kW3ContactFieldValue]) { + valueArray = [self valuesForProperty:propId inRecord:self.record]; + } + if (valueArray) { + NSString* valuesAsString = [valueArray componentsJoinedByString:@" "]; + NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue]; + bFound = [containPred evaluateWithObject:valuesAsString]; + } + } + return bFound; +} + +/* + * Returns the array of values for a multivalue string property of the specified property id + */ +- (__autoreleasing NSArray*)valuesForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord +{ + ABMultiValueRef multi = ABRecordCopyValue(aRecord, propId); + NSArray* values = (__bridge_transfer NSArray*)ABMultiValueCopyArrayOfAllValues(multi); + + CFRelease(multi); + return values; +} + +/* + * Returns the array of labels for a multivalue string property of the specified property id + */ +- (NSArray*)labelsForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord +{ + ABMultiValueRef multi = ABRecordCopyValue(aRecord, propId); + CFIndex count = ABMultiValueGetCount(multi); + NSMutableArray* labels = [NSMutableArray arrayWithCapacity:count]; + + for (int i = 0; i < count; i++) { + NSString* label = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, i); + if (label) { + [labels addObject:label]; + } + } + + CFRelease(multi); + return labels; +} + +/* search for values within MultiValue Dictionary properties Address or IM property + * IN: + * (NSArray*) fields - the array of W3C field names to search within + * (ABPropertyID) propId - the AddressBook property that returns a multivalue dictionary + * (NSString*) testValue - the string to search for within the specified fields + * + */ +- (BOOL)searchContactFields:(NSArray*)fields forMVDictionaryProperty:(ABPropertyID)propId withValue:(NSString*)testValue +{ + BOOL bFound = NO; + + NSArray* values = [self valuesForProperty:propId inRecord:self.record]; // array of dictionaries (as CFDictionaryRef) + int dictCount = [values count]; + + // for ims dictionary contains with service (w3C type) and username (W3c value) + // for addresses dictionary contains street, city, state, zip, country + for (int i = 0; i < dictCount; i++) { + CFDictionaryRef dict = (__bridge CFDictionaryRef)[values objectAtIndex:i]; + + for (NSString* member in fields) { + NSString* abKey = [[CDVContact defaultW3CtoAB] valueForKey:member]; // im and address fields are all strings + CFStringRef abValue = nil; + if (abKey) { + NSString* testString = nil; + if ([member isEqualToString:kW3ContactImType]) { + if ([CDVContact isValidW3ContactType:testValue]) { + // only search service/types if the filter string is a valid ContactField.type + testString = (NSString*)[CDVContact convertContactTypeToPropertyLabel:testValue]; + } + } else { + testString = testValue; + } + if (testString != nil) { + BOOL bExists = CFDictionaryGetValueIfPresent(dict, (__bridge const void*)abKey, (void*)&abValue); + if (bExists) { + CFRetain(abValue); + NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testString]; + bFound = [containPred evaluateWithObject:(__bridge id)abValue]; + CFRelease(abValue); + } + } + } + if (bFound == YES) { + break; + } + } // end of for each member in fields + + if (bFound == YES) { + break; + } + } // end of for each dictionary + + return bFound; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVContacts.h b/cordova/ios/CordovaLib/Classes/CDVContacts.h new file mode 100755 index 000000000..0342f5b31 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVContacts.h @@ -0,0 +1,151 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import "CDVPlugin.h" +#import "CDVContact.h" + +@interface CDVContacts : CDVPlugin +{ + ABAddressBookRef addressBook; +} + +/* + * newContact - create a new contact via the GUI + * + * arguments: + * 1: successCallback: this is the javascript function that will be called with the newly created contactId + */ +- (void)newContact:(CDVInvokedUrlCommand*)command; + +/* + * displayContact - IN PROGRESS + * + * arguments: + * 1: recordID of the contact to display in the iPhone contact display + * 2: successCallback - currently not used + * 3: error callback + * options: + * allowsEditing: set to true to allow the user to edit the contact - currently not supported + */ +- (void)displayContact:(CDVInvokedUrlCommand*)command; + +/* + * chooseContact + * + * arguments: + * 1: this is the javascript function that will be called with the contact data as a JSON object (as the first param) + * options: + * allowsEditing: set to true to not choose the contact, but to edit it in the iPhone contact editor + */ +- (void)chooseContact:(CDVInvokedUrlCommand*)command; + +- (void)newPersonViewController:(ABNewPersonViewController*)newPersonViewController didCompleteWithNewPerson:(ABRecordRef)person; +- (BOOL)personViewController:(ABPersonViewController*)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person + property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifierForValue; + +/* + * search - searches for contacts. Only person records are currently supported. + * + * arguments: + * 1: successcallback - this is the javascript function that will be called with the array of found contacts + * 2: errorCallback - optional javascript function to be called in the event of an error with an error code. + * options: dictionary containing ContactFields and ContactFindOptions + * fields - ContactFields array + * findOptions - ContactFindOptions object as dictionary + * + */ +- (void)search:(CDVInvokedUrlCommand*)command; + +/* + * save - saves a new contact or updates and existing contact + * + * arguments: + * 1: success callback - this is the javascript function that will be called with the JSON representation of the saved contact + * search calls a fixed navigator.service.contacts._findCallback which then calls the success callback stored before making the call into obj-c + */ +- (void)save:(CDVInvokedUrlCommand*)command; + +/* + * remove - removes a contact from the address book + * + * arguments: + * 1: 1: successcallback - this is the javascript function that will be called with a (now) empty contact object + * + * options: dictionary containing Contact object to remove + * contact - Contact object as dictionary + */ +- (void)remove:(CDVInvokedUrlCommand*)command; + +// - (void) dealloc; + +@end + +@interface CDVContactsPicker : ABPeoplePickerNavigationController +{ + BOOL allowsEditing; + NSString* callbackId; + NSDictionary* options; + NSDictionary* pickedContactDictionary; +} + +@property BOOL allowsEditing; +@property (copy) NSString* callbackId; +@property (nonatomic, strong) NSDictionary* options; +@property (nonatomic, strong) NSDictionary* pickedContactDictionary; + +@end + +@interface CDVNewContactsController : ABNewPersonViewController +{ + NSString* callbackId; +} +@property (copy) NSString* callbackId; +@end + +/* ABPersonViewController does not have any UI to dismiss. Adding navigationItems to it does not work properly, the navigationItems are lost when the app goes into the background. + The solution was to create an empty NavController in front of the ABPersonViewController. This + causes the ABPersonViewController to have a back button. By subclassing the ABPersonViewController, + we can override viewWillDisappear and take down the entire NavigationController at that time. + */ +@interface CDVDisplayContactViewController : ABPersonViewController +{} +@property (nonatomic, strong) CDVPlugin* contactsPlugin; + +@end +@interface CDVAddressBookAccessError : NSObject +{} +@property (assign) CDVContactError errorCode; +- (CDVAddressBookAccessError*)initWithCode:(CDVContactError)code; +@end + +typedef void (^ CDVAddressBookWorkerBlock)( + ABAddressBookRef addressBook, + CDVAddressBookAccessError* error + ); +@interface CDVAddressBookHelper : NSObject +{} + +- (void)createAddressBook:(CDVAddressBookWorkerBlock)workerBlock; +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVContacts.m b/cordova/ios/CordovaLib/Classes/CDVContacts.m new file mode 100755 index 000000000..6cb9f089e --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVContacts.m @@ -0,0 +1,593 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVContacts.h" +#import +#import "NSArray+Comparisons.h" +#import "NSDictionary+Extensions.h" +#import "CDVNotification.h" + +@implementation CDVContactsPicker + +@synthesize allowsEditing; +@synthesize callbackId; +@synthesize options; +@synthesize pickedContactDictionary; + +@end +@implementation CDVNewContactsController + +@synthesize callbackId; + +@end + +@implementation CDVContacts + +// no longer used since code gets AddressBook for each operation. +// If address book changes during save or remove operation, may get error but not much we can do about it +// If address book changes during UI creation, display or edit, we don't control any saves so no need for callback + +/*void addressBookChanged(ABAddressBookRef addressBook, CFDictionaryRef info, void* context) +{ + // note that this function is only called when another AddressBook instance modifies + // the address book, not the current one. For example, through an OTA MobileMe sync + Contacts* contacts = (Contacts*)context; + [contacts addressBookDirty]; +}*/ + +- (CDVPlugin*)initWithWebView:(UIWebView*)theWebView +{ + self = (CDVContacts*)[super initWithWebView:(UIWebView*)theWebView]; + + /*if (self) { + addressBook = ABAddressBookCreate(); + ABAddressBookRegisterExternalChangeCallback(addressBook, addressBookChanged, self); + }*/ + + return self; +} + +// overridden to clean up Contact statics +- (void)onAppTerminate +{ + // NSLog(@"Contacts::onAppTerminate"); +} + +// iPhone only method to create a new contact through the GUI +- (void)newContact:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + + CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init]; + CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles + + [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errCode) { + if (addrBook == NULL) { + // permission was denied or other error just return (no error callback) + return; + } + CDVNewContactsController* npController = [[CDVNewContactsController alloc] init]; + npController.addressBook = addrBook; // a CF retaining assign + CFRelease(addrBook); + + npController.newPersonViewDelegate = self; + npController.callbackId = callbackId; + + UINavigationController* navController = [[UINavigationController alloc] initWithRootViewController:npController]; + + if ([weakSelf.viewController respondsToSelector:@selector(presentViewController:::)]) { + [weakSelf.viewController presentViewController:navController animated:YES completion:nil]; + } else { + [weakSelf.viewController presentModalViewController:navController animated:YES]; + } + }]; +} + +- (void)newPersonViewController:(ABNewPersonViewController*)newPersonViewController didCompleteWithNewPerson:(ABRecordRef)person +{ + ABRecordID recordId = kABRecordInvalidID; + CDVNewContactsController* newCP = (CDVNewContactsController*)newPersonViewController; + NSString* callbackId = newCP.callbackId; + + if (person != NULL) { + // return the contact id + recordId = ABRecordGetRecordID(person); + } + + if ([newPersonViewController respondsToSelector:@selector(presentingViewController)]) { + [[newPersonViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[newPersonViewController parentViewController] dismissModalViewControllerAnimated:YES]; + } + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:recordId]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)displayContact:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + ABRecordID recordID = [[command.arguments objectAtIndex:0] intValue]; + NSDictionary* options = [command.arguments objectAtIndex:1 withDefault:[NSNull null]]; + bool bEdit = [options isKindOfClass:[NSNull class]] ? false : [options existsValue:@"true" forKey:@"allowsEditing"]; + + CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init]; + CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles + + [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errCode) { + if (addrBook == NULL) { + // permission was denied or other error - return error + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:errCode ? errCode.errorCode:UNKNOWN_ERROR]; + [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId]; + return; + } + ABRecordRef rec = ABAddressBookGetPersonWithRecordID(addrBook, recordID); + + if (rec) { + CDVDisplayContactViewController* personController = [[CDVDisplayContactViewController alloc] init]; + personController.displayedPerson = rec; + personController.personViewDelegate = self; + personController.allowsEditing = NO; + + // create this so DisplayContactViewController will have a "back" button. + UIViewController* parentController = [[UIViewController alloc] init]; + UINavigationController* navController = [[UINavigationController alloc] initWithRootViewController:parentController]; + + [navController pushViewController:personController animated:YES]; + + if ([self.viewController respondsToSelector:@selector(presentViewController:::)]) { + [self.viewController presentViewController:navController animated:YES completion:nil]; + } else { + [self.viewController presentModalViewController:navController animated:YES]; + } + + if (bEdit) { + // create the editing controller and push it onto the stack + ABPersonViewController* editPersonController = [[ABPersonViewController alloc] init]; + editPersonController.displayedPerson = rec; + editPersonController.personViewDelegate = self; + editPersonController.allowsEditing = YES; + [navController pushViewController:editPersonController animated:YES]; + } + } else { + // no record, return error + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:UNKNOWN_ERROR]; + [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + CFRelease(addrBook); + }]; +} + +- (BOOL)personViewController:(ABPersonViewController*)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person + property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifierForValue +{ + return YES; +} + +- (void)chooseContact:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSDictionary* options = [command.arguments objectAtIndex:0 withDefault:[NSNull null]]; + + CDVContactsPicker* pickerController = [[CDVContactsPicker alloc] init]; + + pickerController.peoplePickerDelegate = self; + pickerController.callbackId = callbackId; + pickerController.options = options; + pickerController.pickedContactDictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kABRecordInvalidID], kW3ContactId, nil]; + pickerController.allowsEditing = (BOOL)[options existsValue : @"true" forKey : @"allowsEditing"]; + + if ([self.viewController respondsToSelector:@selector(presentViewController:::)]) { + [self.viewController presentViewController:pickerController animated:YES completion:nil]; + } else { + [self.viewController presentModalViewController:pickerController animated:YES]; + } +} + +- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController*)peoplePicker + shouldContinueAfterSelectingPerson:(ABRecordRef)person +{ + CDVContactsPicker* picker = (CDVContactsPicker*)peoplePicker; + NSNumber* pickedId = [NSNumber numberWithInt:ABRecordGetRecordID(person)]; + + if (picker.allowsEditing) { + ABPersonViewController* personController = [[ABPersonViewController alloc] init]; + personController.displayedPerson = person; + personController.personViewDelegate = self; + personController.allowsEditing = picker.allowsEditing; + // store id so can get info in peoplePickerNavigationControllerDidCancel + picker.pickedContactDictionary = [NSDictionary dictionaryWithObjectsAndKeys:pickedId, kW3ContactId, nil]; + + [peoplePicker pushViewController:personController animated:YES]; + } else { + // Retrieve and return pickedContact information + CDVContact* pickedContact = [[CDVContact alloc] initFromABRecord:(ABRecordRef)person]; + NSArray* fields = [picker.options objectForKey:@"fields"]; + NSDictionary* returnFields = [[CDVContact class] calcReturnFields:fields]; + picker.pickedContactDictionary = [pickedContact toDictionary:returnFields]; + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:picker.pickedContactDictionary]; + [self.commandDelegate sendPluginResult:result callbackId:picker.callbackId]; + + if ([picker respondsToSelector:@selector(presentingViewController)]) { + [[picker presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[picker parentViewController] dismissModalViewControllerAnimated:YES]; + } + } + return NO; +} + +- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController*)peoplePicker + shouldContinueAfterSelectingPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier +{ + return YES; +} + +- (void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController*)peoplePicker +{ + // return contactId or invalid if none picked + CDVContactsPicker* picker = (CDVContactsPicker*)peoplePicker; + + if (picker.allowsEditing) { + // get the info after possible edit + // if we got this far, user has already approved/ disapproved addressBook access + ABAddressBookRef addrBook = nil; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000 + if (&ABAddressBookCreateWithOptions != NULL) { + addrBook = ABAddressBookCreateWithOptions(NULL, NULL); + } else +#endif + { + // iOS 4 & 5 + addrBook = ABAddressBookCreate(); + } + ABRecordRef person = ABAddressBookGetPersonWithRecordID(addrBook, [[picker.pickedContactDictionary objectForKey:kW3ContactId] integerValue]); + if (person) { + CDVContact* pickedContact = [[CDVContact alloc] initFromABRecord:(ABRecordRef)person]; + NSArray* fields = [picker.options objectForKey:@"fields"]; + NSDictionary* returnFields = [[CDVContact class] calcReturnFields:fields]; + picker.pickedContactDictionary = [pickedContact toDictionary:returnFields]; + } + CFRelease(addrBook); + } + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:picker.pickedContactDictionary]; + [self.commandDelegate sendPluginResult:result callbackId:picker.callbackId]; + + if ([peoplePicker respondsToSelector:@selector(presentingViewController)]) { + [[peoplePicker presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[peoplePicker parentViewController] dismissModalViewControllerAnimated:YES]; + } +} + +- (void)search:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSArray* fields = [command.arguments objectAtIndex:0]; + NSDictionary* findOptions = [command.arguments objectAtIndex:1 withDefault:[NSNull null]]; + + [self.commandDelegate runInBackground:^{ + // from Apple: Important You must ensure that an instance of ABAddressBookRef is used by only one thread. + // which is why address book is created within the dispatch queue. + // more details here: http: //blog.byadrian.net/2012/05/05/ios-addressbook-framework-and-gcd/ + CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init]; + CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles + // it gets uglier, block within block..... + [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errCode) { + if (addrBook == NULL) { + // permission was denied or other error - return error + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:errCode ? errCode.errorCode:UNKNOWN_ERROR]; + [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId]; + return; + } + + NSArray* foundRecords = nil; + // get the findOptions values + BOOL multiple = NO; // default is false + NSString* filter = nil; + if (![findOptions isKindOfClass:[NSNull class]]) { + id value = nil; + filter = (NSString*)[findOptions objectForKey:@"filter"]; + value = [findOptions objectForKey:@"multiple"]; + if ([value isKindOfClass:[NSNumber class]]) { + // multiple is a boolean that will come through as an NSNumber + multiple = [(NSNumber*)value boolValue]; + // NSLog(@"multiple is: %d", multiple); + } + } + + NSDictionary* returnFields = [[CDVContact class] calcReturnFields:fields]; + + NSMutableArray* matches = nil; + if (!filter || [filter isEqualToString:@""]) { + // get all records + foundRecords = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addrBook); + if (foundRecords && ([foundRecords count] > 0)) { + // create Contacts and put into matches array + // doesn't make sense to ask for all records when multiple == NO but better check + int xferCount = multiple == YES ? [foundRecords count] : 1; + matches = [NSMutableArray arrayWithCapacity:xferCount]; + + for (int k = 0; k < xferCount; k++) { + CDVContact* xferContact = [[CDVContact alloc] initFromABRecord:(__bridge ABRecordRef)[foundRecords objectAtIndex:k]]; + [matches addObject:xferContact]; + xferContact = nil; + } + } + } else { + foundRecords = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addrBook); + matches = [NSMutableArray arrayWithCapacity:1]; + BOOL bFound = NO; + int testCount = [foundRecords count]; + + for (int j = 0; j < testCount; j++) { + CDVContact* testContact = [[CDVContact alloc] initFromABRecord:(__bridge ABRecordRef)[foundRecords objectAtIndex:j]]; + if (testContact) { + bFound = [testContact foundValue:filter inFields:returnFields]; + if (bFound) { + [matches addObject:testContact]; + } + testContact = nil; + } + } + } + NSMutableArray* returnContacts = [NSMutableArray arrayWithCapacity:1]; + + if ((matches != nil) && ([matches count] > 0)) { + // convert to JS Contacts format and return in callback + // - returnFields determines what properties to return + @autoreleasepool { + int count = multiple == YES ? [matches count] : 1; + + for (int i = 0; i < count; i++) { + CDVContact* newContact = [matches objectAtIndex:i]; + NSDictionary* aContact = [newContact toDictionary:returnFields]; + [returnContacts addObject:aContact]; + } + } + } + // return found contacts (array is empty if no contacts found) + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:returnContacts]; + [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId]; + // NSLog(@"findCallback string: %@", jsString); + + if (addrBook) { + CFRelease(addrBook); + } + }]; + }]; // end of workQueue block + + return; +} + +- (void)save:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSDictionary* contactDict = [command.arguments objectAtIndex:0]; + + [self.commandDelegate runInBackground:^{ + CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init]; + CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles + + [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errorCode) { + CDVPluginResult* result = nil; + if (addrBook == NULL) { + // permission was denied or other error - return error + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errorCode ? errorCode.errorCode:UNKNOWN_ERROR]; + [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId]; + return; + } + + bool bIsError = FALSE, bSuccess = FALSE; + BOOL bUpdate = NO; + CDVContactError errCode = UNKNOWN_ERROR; + CFErrorRef error; + NSNumber* cId = [contactDict valueForKey:kW3ContactId]; + CDVContact* aContact = nil; + ABRecordRef rec = nil; + if (cId && ![cId isKindOfClass:[NSNull class]]) { + rec = ABAddressBookGetPersonWithRecordID(addrBook, [cId intValue]); + if (rec) { + aContact = [[CDVContact alloc] initFromABRecord:rec]; + bUpdate = YES; + } + } + if (!aContact) { + aContact = [[CDVContact alloc] init]; + } + + bSuccess = [aContact setFromContactDict:contactDict asUpdate:bUpdate]; + if (bSuccess) { + if (!bUpdate) { + bSuccess = ABAddressBookAddRecord(addrBook, [aContact record], &error); + } + if (bSuccess) { + bSuccess = ABAddressBookSave(addrBook, &error); + } + if (!bSuccess) { // need to provide error codes + bIsError = TRUE; + errCode = IO_ERROR; + } else { + // give original dictionary back? If generate dictionary from saved contact, have no returnFields specified + // so would give back all fields (which W3C spec. indicates is not desired) + // for now (while testing) give back saved, full contact + NSDictionary* newContact = [aContact toDictionary:[CDVContact defaultFields]]; + // NSString* contactStr = [newContact JSONRepresentation]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:newContact]; + } + } else { + bIsError = TRUE; + errCode = IO_ERROR; + } + CFRelease(addrBook); + + if (bIsError) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errCode]; + } + + if (result) { + [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + }]; + }]; // end of queue +} + +- (void)remove:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSNumber* cId = [command.arguments objectAtIndex:0]; + + CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init]; + CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles + + [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errorCode) { + CDVPluginResult* result = nil; + if (addrBook == NULL) { + // permission was denied or other error - return error + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errorCode ? errorCode.errorCode:UNKNOWN_ERROR]; + [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId]; + return; + } + + bool bIsError = FALSE, bSuccess = FALSE; + CDVContactError errCode = UNKNOWN_ERROR; + CFErrorRef error; + ABRecordRef rec = nil; + if (cId && ![cId isKindOfClass:[NSNull class]] && ([cId intValue] != kABRecordInvalidID)) { + rec = ABAddressBookGetPersonWithRecordID(addrBook, [cId intValue]); + if (rec) { + bSuccess = ABAddressBookRemoveRecord(addrBook, rec, &error); + if (!bSuccess) { + bIsError = TRUE; + errCode = IO_ERROR; + } else { + bSuccess = ABAddressBookSave(addrBook, &error); + if (!bSuccess) { + bIsError = TRUE; + errCode = IO_ERROR; + } else { + // set id to null + // [contactDict setObject:[NSNull null] forKey:kW3ContactId]; + // result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary: contactDict]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + // NSString* contactStr = [contactDict JSONRepresentation]; + } + } + } else { + // no record found return error + bIsError = TRUE; + errCode = UNKNOWN_ERROR; + } + } else { + // invalid contact id provided + bIsError = TRUE; + errCode = INVALID_ARGUMENT_ERROR; + } + + if (addrBook) { + CFRelease(addrBook); + } + if (bIsError) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errCode]; + } + if (result) { + [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + }]; + return; +} + +@end + +/* ABPersonViewController does not have any UI to dismiss. Adding navigationItems to it does not work properly + * The navigationItems are lost when the app goes into the background. The solution was to create an empty + * NavController in front of the ABPersonViewController. This will cause the ABPersonViewController to have a back button. By subclassing the ABPersonViewController, we can override viewDidDisappear and take down the entire NavigationController. + */ +@implementation CDVDisplayContactViewController +@synthesize contactsPlugin; + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + if ([self respondsToSelector:@selector(presentingViewController)]) { + [[self presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[self parentViewController] dismissModalViewControllerAnimated:YES]; + } +} + +@end +@implementation CDVAddressBookAccessError + +@synthesize errorCode; + +- (CDVAddressBookAccessError*)initWithCode:(CDVContactError)code +{ + self = [super init]; + if (self) { + self.errorCode = code; + } + return self; +} + +@end + +@implementation CDVAddressBookHelper + +/** + * NOTE: workerBlock is responsible for releasing the addressBook that is passed to it + */ +- (void)createAddressBook:(CDVAddressBookWorkerBlock)workerBlock +{ + // TODO: this probably should be reworked - seems like the workerBlock can just create and release its own AddressBook, + // and also this important warning from (http://developer.apple.com/library/ios/#documentation/ContactData/Conceptual/AddressBookProgrammingGuideforiPhone/Chapters/BasicObjects.html): + // "Important: Instances of ABAddressBookRef cannot be used by multiple threads. Each thread must make its own instance." + ABAddressBookRef addressBook; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000 + if (&ABAddressBookCreateWithOptions != NULL) { + CFErrorRef error = nil; + // CFIndex status = ABAddressBookGetAuthorizationStatus(); + addressBook = ABAddressBookCreateWithOptions(NULL, &error); + // NSLog(@"addressBook access: %lu", status); + ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) { + // callback can occur in background, address book must be accessed on thread it was created on + dispatch_sync(dispatch_get_main_queue(), ^{ + if (error) { + workerBlock(NULL, [[CDVAddressBookAccessError alloc] initWithCode:UNKNOWN_ERROR]); + } else if (!granted) { + workerBlock(NULL, [[CDVAddressBookAccessError alloc] initWithCode:PERMISSION_DENIED_ERROR]); + } else { + // access granted + workerBlock(addressBook, [[CDVAddressBookAccessError alloc] initWithCode:UNKNOWN_ERROR]); + } + }); + }); + } else +#endif + { + // iOS 4 or 5 no checks needed + addressBook = ABAddressBookCreate(); + workerBlock(addressBook, NULL); + } +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVDebug.h b/cordova/ios/CordovaLib/Classes/CDVDebug.h new file mode 100755 index 000000000..4a0d9f929 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVDebug.h @@ -0,0 +1,25 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#ifdef DEBUG + #define DLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) +#else + #define DLog(...) +#endif +#define ALog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) diff --git a/cordova/ios/CordovaLib/Classes/CDVDevice.h b/cordova/ios/CordovaLib/Classes/CDVDevice.h new file mode 100755 index 000000000..fd6ea122a --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVDevice.h @@ -0,0 +1,30 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +@interface CDVDevice : CDVPlugin +{} + ++ (NSString*)cordovaVersion; + +- (void)getDeviceInfo:(CDVInvokedUrlCommand*)command; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVDevice.m b/cordova/ios/CordovaLib/Classes/CDVDevice.m new file mode 100755 index 000000000..cc7ad89ff --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVDevice.m @@ -0,0 +1,90 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#include +#include + +#import "CDV.h" + +@implementation UIDevice (ModelVersion) + +- (NSString*)modelVersion +{ + size_t size; + + sysctlbyname("hw.machine", NULL, &size, NULL, 0); + char* machine = malloc(size); + sysctlbyname("hw.machine", machine, &size, NULL, 0); + NSString* platform = [NSString stringWithUTF8String:machine]; + free(machine); + + return platform; +} + +@end + +@interface CDVDevice () {} +@end + +@implementation CDVDevice + +- (void)getDeviceInfo:(CDVInvokedUrlCommand*)command +{ + NSDictionary* deviceProperties = [self deviceProperties]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:deviceProperties]; + + /* Settings.plist + * Read the optional Settings.plist file and push these user-defined settings down into the web application. + * This can be useful for supplying build-time configuration variables down to the app to change its behavior, + * such as specifying Full / Lite version, or localization (English vs German, for instance). + */ + // TODO: turn this into an iOS only plugin + NSDictionary* temp = [CDVViewController getBundlePlist:@"Settings"]; + + if ([temp respondsToSelector:@selector(JSONString)]) { + NSLog(@"Deprecation warning: window.Setting will be removed Aug 2013. Refer to https://issues.apache.org/jira/browse/CB-2433"); + NSString* js = [NSString stringWithFormat:@"window.Settings = %@;", [temp JSONString]]; + [self.commandDelegate evalJs:js]; + } + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (NSDictionary*)deviceProperties +{ + UIDevice* device = [UIDevice currentDevice]; + NSMutableDictionary* devProps = [NSMutableDictionary dictionaryWithCapacity:4]; + + [devProps setObject:[device modelVersion] forKey:@"model"]; + [devProps setObject:@"iOS" forKey:@"platform"]; + [devProps setObject:[device systemVersion] forKey:@"version"]; + [devProps setObject:[device uniqueAppInstanceIdentifier] forKey:@"uuid"]; + [devProps setObject:[device model] forKey:@"name"]; + [devProps setObject:[[self class] cordovaVersion] forKey:@"cordova"]; + + NSDictionary* devReturn = [NSDictionary dictionaryWithDictionary:devProps]; + return devReturn; +} + ++ (NSString*)cordovaVersion +{ + return CDV_VERSION; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVEcho.h b/cordova/ios/CordovaLib/Classes/CDVEcho.h new file mode 100755 index 000000000..76a4a96fc --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVEcho.h @@ -0,0 +1,23 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" + +@interface CDVEcho : CDVPlugin +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVEcho.m b/cordova/ios/CordovaLib/Classes/CDVEcho.m new file mode 100755 index 000000000..c74990dc2 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVEcho.m @@ -0,0 +1,61 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVEcho.h" +#import "CDV.h" + +@implementation CDVEcho + +- (void)echo:(CDVInvokedUrlCommand*)command +{ + id message = [command.arguments objectAtIndex:0]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)echoAsyncHelper:(NSArray*)args +{ + [self.commandDelegate sendPluginResult:[args objectAtIndex:0] callbackId:[args objectAtIndex:1]]; +} + +- (void)echoAsync:(CDVInvokedUrlCommand*)command +{ + id message = [command.arguments objectAtIndex:0]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + + [self performSelector:@selector(echoAsyncHelper:) withObject:[NSArray arrayWithObjects:pluginResult, command.callbackId, nil] afterDelay:0]; +} + +- (void)echoArrayBuffer:(CDVInvokedUrlCommand*)command +{ + id message = [command.arguments objectAtIndex:0]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArrayBuffer:message]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)echoMultiPart:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsMultipart:command.arguments]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVExif.h b/cordova/ios/CordovaLib/Classes/CDVExif.h new file mode 100755 index 000000000..3e8adbd02 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVExif.h @@ -0,0 +1,43 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#ifndef CordovaLib_ExifData_h +#define CordovaLib_ExifData_h + +// exif data types +typedef enum exifDataTypes { + EDT_UBYTE = 1, // 8 bit unsigned integer + EDT_ASCII_STRING, // 8 bits containing 7 bit ASCII code, null terminated + EDT_USHORT, // 16 bit unsigned integer + EDT_ULONG, // 32 bit unsigned integer + EDT_URATIONAL, // 2 longs, first is numerator and second is denominator + EDT_SBYTE, + EDT_UNDEFINED, // 8 bits + EDT_SSHORT, + EDT_SLONG, // 32bit signed integer (2's complement) + EDT_SRATIONAL, // 2 SLONGS, first long is numerator, second is denominator + EDT_SINGLEFLOAT, + EDT_DOUBLEFLOAT +} ExifDataTypes; + +// maps integer code for exif data types to width in bytes +static const int DataTypeToWidth[] = {1,1,2,4,8,1,1,2,4,8,4,8}; + +static const int RECURSE_HORIZON = 8; +#endif diff --git a/cordova/ios/CordovaLib/Classes/CDVFile.h b/cordova/ios/CordovaLib/Classes/CDVFile.h new file mode 100755 index 000000000..eaf8cbe84 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVFile.h @@ -0,0 +1,106 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +enum CDVFileError { + NO_ERROR = 0, + NOT_FOUND_ERR = 1, + SECURITY_ERR = 2, + ABORT_ERR = 3, + NOT_READABLE_ERR = 4, + ENCODING_ERR = 5, + NO_MODIFICATION_ALLOWED_ERR = 6, + INVALID_STATE_ERR = 7, + SYNTAX_ERR = 8, + INVALID_MODIFICATION_ERR = 9, + QUOTA_EXCEEDED_ERR = 10, + TYPE_MISMATCH_ERR = 11, + PATH_EXISTS_ERR = 12 +}; +typedef int CDVFileError; + +enum CDVFileSystemType { + TEMPORARY = 0, + PERSISTENT = 1 +}; +typedef int CDVFileSystemType; + +extern NSString* const kCDVAssetsLibraryPrefix; + +@interface CDVFile : CDVPlugin { + NSString* appDocsPath; + NSString* appLibraryPath; + NSString* appTempPath; + NSString* persistentPath; + NSString* temporaryPath; + + BOOL userHasAllowed; +} +- (NSNumber*)checkFreeDiskSpace:(NSString*)appPath; +- (NSString*)getAppPath:(NSString*)pathFragment; +// -(NSString*) getFullPath: (NSString*)pathFragment; +- (void)requestFileSystem:(CDVInvokedUrlCommand*)command; +- (NSDictionary*)getDirectoryEntry:(NSString*)fullPath isDirectory:(BOOL)isDir; +- (void)resolveLocalFileSystemURI:(CDVInvokedUrlCommand*)command; +- (void)getDirectory:(CDVInvokedUrlCommand*)command; +- (void)getFile:(CDVInvokedUrlCommand*)command; +- (void)getParent:(CDVInvokedUrlCommand*)command; +- (void)getMetadata:(CDVInvokedUrlCommand*)command; +- (void)removeRecursively:(CDVInvokedUrlCommand*)command; +- (void)remove:(CDVInvokedUrlCommand*)command; +- (CDVPluginResult*)doRemove:(NSString*)fullPath; +- (void)copyTo:(CDVInvokedUrlCommand*)command; +- (void)moveTo:(CDVInvokedUrlCommand*)command; +- (BOOL)canCopyMoveSrc:(NSString*)src ToDestination:(NSString*)dest; +- (void)doCopyMove:(CDVInvokedUrlCommand*)command isCopy:(BOOL)bCopy; +// - (void) toURI:(CDVInvokedUrlCommand*)command; +- (void)getFileMetadata:(CDVInvokedUrlCommand*)command; +- (void)readEntries:(CDVInvokedUrlCommand*)command; + +- (void)readAsText:(CDVInvokedUrlCommand*)command; +- (void)readAsDataURL:(CDVInvokedUrlCommand*)command; +- (void)readAsArrayBuffer:(CDVInvokedUrlCommand*)command; +- (NSString*)getMimeTypeFromPath:(NSString*)fullPath; +- (void)write:(CDVInvokedUrlCommand*)command; +- (void)testFileExists:(CDVInvokedUrlCommand*)command; +- (void)testDirectoryExists:(CDVInvokedUrlCommand*)command; +// - (void) createDirectory:(CDVInvokedUrlCommand*)command; +// - (void) deleteDirectory:(CDVInvokedUrlCommand*)command; +// - (void) deleteFile:(CDVInvokedUrlCommand*)command; +- (void)getFreeDiskSpace:(CDVInvokedUrlCommand*)command; +- (void)truncate:(CDVInvokedUrlCommand*)command; + +// - (BOOL) fileExists:(NSString*)fileName; +// - (BOOL) directoryExists:(NSString*)dirName; +- (void)writeToFile:(NSString*)fileName withData:(NSString*)data append:(BOOL)shouldAppend callback:(NSString*)callbackId; +- (unsigned long long)truncateFile:(NSString*)filePath atPosition:(unsigned long long)pos; + +@property (nonatomic, strong) NSString* appDocsPath; +@property (nonatomic, strong) NSString* appLibraryPath; +@property (nonatomic, strong) NSString* appTempPath; +@property (nonatomic, strong) NSString* persistentPath; +@property (nonatomic, strong) NSString* temporaryPath; +@property BOOL userHasAllowed; + +@end + +#define kW3FileTemporary @"temporary" +#define kW3FilePersistent @"persistent" diff --git a/cordova/ios/CordovaLib/Classes/CDVFile.m b/cordova/ios/CordovaLib/Classes/CDVFile.m new file mode 100755 index 000000000..10908ce91 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVFile.m @@ -0,0 +1,1414 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVFile.h" +#import "NSArray+Comparisons.h" +#import "NSDictionary+Extensions.h" +#import "CDVJSON.h" +#import "NSData+Base64.h" +#import +#import +#import +#import +#import "CDVAvailability.h" +#import "sys/xattr.h" + +extern NSString * const NSURLIsExcludedFromBackupKey __attribute__((weak_import)); + +#ifndef __IPHONE_5_1 + NSString* const NSURLIsExcludedFromBackupKey = @"NSURLIsExcludedFromBackupKey"; +#endif + +NSString* const kCDVAssetsLibraryPrefix = @"assets-library://"; + +@implementation CDVFile + +@synthesize appDocsPath, appLibraryPath, appTempPath, persistentPath, temporaryPath, userHasAllowed; + +- (id)initWithWebView:(UIWebView*)theWebView +{ + self = (CDVFile*)[super initWithWebView:theWebView]; + if (self) { + // get the documents directory path + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + self.appDocsPath = [paths objectAtIndex:0]; + + paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + self.appLibraryPath = [paths objectAtIndex:0]; + + self.appTempPath = [NSTemporaryDirectory()stringByStandardizingPath]; // remove trailing slash from NSTemporaryDirectory() + + self.persistentPath = [NSString stringWithFormat:@"/%@", [self.appDocsPath lastPathComponent]]; + self.temporaryPath = [NSString stringWithFormat:@"/%@", [self.appTempPath lastPathComponent]]; + // NSLog(@"docs: %@ - temp: %@", self.appDocsPath, self.appTempPath); + } + + return self; +} + +- (NSNumber*)checkFreeDiskSpace:(NSString*)appPath +{ + NSFileManager* fMgr = [[NSFileManager alloc] init]; + + NSError* __autoreleasing pError = nil; + + NSDictionary* pDict = [fMgr attributesOfFileSystemForPath:appPath error:&pError]; + NSNumber* pNumAvail = (NSNumber*)[pDict objectForKey:NSFileSystemFreeSize]; + + return pNumAvail; +} + +// figure out if the pathFragment represents a persistent of temporary directory and return the full application path. +// returns nil if path is not persistent or temporary +- (NSString*)getAppPath:(NSString*)pathFragment +{ + NSString* appPath = nil; + NSRange rangeP = [pathFragment rangeOfString:self.persistentPath]; + NSRange rangeT = [pathFragment rangeOfString:self.temporaryPath]; + + if ((rangeP.location != NSNotFound) && (rangeT.location != NSNotFound)) { + // we found both in the path, return whichever one is first + if (rangeP.length < rangeT.length) { + appPath = self.appDocsPath; + } else { + appPath = self.appTempPath; + } + } else if (rangeP.location != NSNotFound) { + appPath = self.appDocsPath; + } else if (rangeT.location != NSNotFound) { + appPath = self.appTempPath; + } + return appPath; +} + +/* get the full path to this resource + * IN + * NSString* pathFragment - full Path from File or Entry object (includes system path info) + * OUT + * NSString* fullPath - full iOS path to this resource, nil if not found + */ + +/* Was here in order to NOT have to return full path, but W3C synchronous DirectoryEntry.toURI() killed that idea since I can't call into iOS to + * resolve full URI. Leaving this code here in case W3C spec changes. +-(NSString*) getFullPath: (NSString*)pathFragment +{ + return pathFragment; + NSString* fullPath = nil; + NSString *appPath = [ self getAppPath: pathFragment]; + if (appPath){ + + // remove last component from appPath + NSRange range = [appPath rangeOfString:@"/" options: NSBackwardsSearch]; + NSString* newPath = [appPath substringToIndex:range.location]; + // add pathFragment to get test Path + fullPath = [newPath stringByAppendingPathComponent:pathFragment]; + } + return fullPath; +} */ + +/* Request the File System info + * + * IN: + * arguments[0] - type (number as string) + * TEMPORARY = 0, PERSISTENT = 1; + * arguments[1] - size + * + * OUT: + * Dictionary representing FileSystem object + * name - the human readable directory name + * root = DirectoryEntry object + * bool isDirectory + * bool isFile + * string name + * string fullPath + * fileSystem = FileSystem object - !! ignored because creates circular reference !! + */ + +- (void)requestFileSystem:(CDVInvokedUrlCommand*)command +{ + NSArray* arguments = command.arguments; + + // arguments + NSString* strType = [arguments objectAtIndex:0]; + unsigned long long size = [[arguments objectAtIndex:1] longLongValue]; + + int type = [strType intValue]; + CDVPluginResult* result = nil; + + if (type > 1) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:NOT_FOUND_ERR]; + NSLog(@"iOS only supports TEMPORARY and PERSISTENT file systems"); + } else { + // NSString* fullPath = [NSString stringWithFormat:@"/%@", (type == 0 ? [self.appTempPath lastPathComponent] : [self.appDocsPath lastPathComponent])]; + NSString* fullPath = (type == 0 ? self.appTempPath : self.appDocsPath); + // check for avail space for size request + NSNumber* pNumAvail = [self checkFreeDiskSpace:fullPath]; + // NSLog(@"Free space: %@", [NSString stringWithFormat:@"%qu", [ pNumAvail unsignedLongLongValue ]]); + if (pNumAvail && ([pNumAvail unsignedLongLongValue] < size)) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:QUOTA_EXCEEDED_ERR]; + } else { + NSMutableDictionary* fileSystem = [NSMutableDictionary dictionaryWithCapacity:2]; + [fileSystem setObject:(type == TEMPORARY ? kW3FileTemporary : kW3FilePersistent) forKey:@"name"]; + NSDictionary* dirEntry = [self getDirectoryEntry:fullPath isDirectory:YES]; + [fileSystem setObject:dirEntry forKey:@"root"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileSystem]; + } + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* Creates a dictionary representing an Entry Object + * + * IN: + * NSString* fullPath of the entry + * FileSystem type + * BOOL isDirectory - YES if this is a directory, NO if is a file + * OUT: + * NSDictionary* + Entry object + * bool as NSNumber isDirectory + * bool as NSNumber isFile + * NSString* name - last part of path + * NSString* fullPath + * fileSystem = FileSystem object - !! ignored because creates circular reference FileSystem contains DirectoryEntry which contains FileSystem.....!! + */ +- (NSDictionary*)getDirectoryEntry:(NSString*)fullPath isDirectory:(BOOL)isDir +{ + NSMutableDictionary* dirEntry = [NSMutableDictionary dictionaryWithCapacity:4]; + NSString* lastPart = [fullPath lastPathComponent]; + + [dirEntry setObject:[NSNumber numberWithBool:!isDir] forKey:@"isFile"]; + [dirEntry setObject:[NSNumber numberWithBool:isDir] forKey:@"isDirectory"]; + // NSURL* fileUrl = [NSURL fileURLWithPath:fullPath]; + // [dirEntry setObject: [fileUrl absoluteString] forKey: @"fullPath"]; + [dirEntry setObject:fullPath forKey:@"fullPath"]; + [dirEntry setObject:lastPart forKey:@"name"]; + + return dirEntry; +} + +/* + * Given a URI determine the File System information associated with it and return an appropriate W3C entry object + * IN + * NSString* fileURI - currently requires full file URI + * OUT + * Entry object + * bool isDirectory + * bool isFile + * string name + * string fullPath + * fileSystem = FileSystem object - !! ignored because creates circular reference FileSystem contains DirectoryEntry which contains FileSystem.....!! + */ +- (void)resolveLocalFileSystemURI:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* inputUri = [command.arguments objectAtIndex:0]; + + // don't know if string is encoded or not so unescape + NSString* cleanUri = [inputUri stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + // now escape in order to create URL + NSString* strUri = [cleanUri stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSURL* testUri = [NSURL URLWithString:strUri]; + CDVPluginResult* result = nil; + + if (!testUri) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:ENCODING_ERR]; + } else if ([testUri isFileURL]) { + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSString* path = [testUri path]; + // NSLog(@"url path: %@", path); + BOOL isDir = NO; + // see if exists and is file or dir + BOOL bExists = [fileMgr fileExistsAtPath:path isDirectory:&isDir]; + if (bExists) { + // see if it contains docs path + NSRange range = [path rangeOfString:self.appDocsPath]; + NSString* foundFullPath = nil; + // there's probably an api or easier way to figure out the path type but I can't find it! + if ((range.location != NSNotFound) && (range.length == [self.appDocsPath length])) { + foundFullPath = self.appDocsPath; + } else { + // see if it contains the temp path + range = [path rangeOfString:self.appTempPath]; + if ((range.location != NSNotFound) && (range.length == [self.appTempPath length])) { + foundFullPath = self.appTempPath; + } + } + if (foundFullPath == nil) { + // error SECURITY_ERR - not one of the two paths types supported + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:SECURITY_ERR]; + } else { + NSDictionary* fileSystem = [self getDirectoryEntry:path isDirectory:isDir]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileSystem]; + } + } else { + // return NOT_FOUND_ERR + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + } + } else if ([strUri hasPrefix:@"assets-library://"]) { + NSDictionary* fileSystem = [self getDirectoryEntry:strUri isDirectory:NO]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileSystem]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:ENCODING_ERR]; + } + + if (result != nil) { + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } +} + +/* Part of DirectoryEntry interface, creates or returns the specified directory + * IN: + * NSString* fullPath - full path for this directory + * NSString* path - directory to be created/returned; may be full path or relative path + * NSDictionary* - Flags object + * boolean as NSNumber create - + * if create is true and directory does not exist, create dir and return directory entry + * if create is true and exclusive is true and directory does exist, return error + * if create is false and directory does not exist, return error + * if create is false and the path represents a file, return error + * boolean as NSNumber exclusive - used in conjunction with create + * if exclusive is true and create is true - specifies failure if directory already exists + * + * + */ +- (void)getDirectory:(CDVInvokedUrlCommand*)command +{ + NSMutableArray* arguments = [NSMutableArray arrayWithArray:command.arguments]; + NSMutableDictionary* options = nil; + + if ([arguments count] >= 3) { + options = [arguments objectAtIndex:2 withDefault:nil]; + } + // add getDir to options and call getFile() + if (options != nil) { + options = [NSMutableDictionary dictionaryWithDictionary:options]; + } else { + options = [NSMutableDictionary dictionaryWithCapacity:1]; + } + [options setObject:[NSNumber numberWithInt:1] forKey:@"getDir"]; + if ([arguments count] >= 3) { + [arguments replaceObjectAtIndex:2 withObject:options]; + } else { + [arguments addObject:options]; + } + CDVInvokedUrlCommand* subCommand = + [[CDVInvokedUrlCommand alloc] initWithArguments:arguments + callbackId:command.callbackId + className:command.className + methodName:command.methodName]; + + [self getFile:subCommand]; +} + +/* Part of DirectoryEntry interface, creates or returns the specified file + * IN: + * NSString* fullPath - full path for this file + * NSString* path - file to be created/returned; may be full path or relative path + * NSDictionary* - Flags object + * boolean as NSNumber create - + * if create is true and file does not exist, create file and return File entry + * if create is true and exclusive is true and file does exist, return error + * if create is false and file does not exist, return error + * if create is false and the path represents a directory, return error + * boolean as NSNumber exclusive - used in conjunction with create + * if exclusive is true and create is true - specifies failure if file already exists + * + * + */ +- (void)getFile:(CDVInvokedUrlCommand*)command +{ + // arguments are URL encoded + NSString* fullPath = [command.arguments objectAtIndex:0]; + NSString* requestedPath = [command.arguments objectAtIndex:1]; + NSDictionary* options = [command.arguments objectAtIndex:2 withDefault:nil]; + + // return unsupported result for assets-library URLs + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"getFile not supported for assets-library URLs."]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + BOOL bDirRequest = NO; + BOOL create = NO; + BOOL exclusive = NO; + int errorCode = 0; // !!! risky - no error code currently defined for 0 + + if ([options valueForKeyIsNumber:@"create"]) { + create = [(NSNumber*)[options valueForKey:@"create"] boolValue]; + } + if ([options valueForKeyIsNumber:@"exclusive"]) { + exclusive = [(NSNumber*)[options valueForKey:@"exclusive"] boolValue]; + } + + if ([options valueForKeyIsNumber:@"getDir"]) { + // this will not exist for calls directly to getFile but will have been set by getDirectory before calling this method + bDirRequest = [(NSNumber*)[options valueForKey:@"getDir"] boolValue]; + } + // see if the requested path has invalid characters - should we be checking for more than just ":"? + if ([requestedPath rangeOfString:@":"].location != NSNotFound) { + errorCode = ENCODING_ERR; + } else { + // was full or relative path provided? + NSRange range = [requestedPath rangeOfString:fullPath]; + BOOL bIsFullPath = range.location != NSNotFound; + + NSString* reqFullPath = nil; + + if (!bIsFullPath) { + reqFullPath = [fullPath stringByAppendingPathComponent:requestedPath]; + } else { + reqFullPath = requestedPath; + } + + // NSLog(@"reqFullPath = %@", reqFullPath); + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + BOOL bIsDir; + BOOL bExists = [fileMgr fileExistsAtPath:reqFullPath isDirectory:&bIsDir]; + if (bExists && (create == NO) && (bIsDir == !bDirRequest)) { + // path exists and is of requested type - return TYPE_MISMATCH_ERR + errorCode = TYPE_MISMATCH_ERR; + } else if (!bExists && (create == NO)) { + // path does not exist and create is false - return NOT_FOUND_ERR + errorCode = NOT_FOUND_ERR; + } else if (bExists && (create == YES) && (exclusive == YES)) { + // file/dir already exists and exclusive and create are both true - return PATH_EXISTS_ERR + errorCode = PATH_EXISTS_ERR; + } else { + // if bExists and create == YES - just return data + // if bExists and create == NO - just return data + // if !bExists and create == YES - create and return data + BOOL bSuccess = YES; + NSError __autoreleasing* pError = nil; + if (!bExists && (create == YES)) { + if (bDirRequest) { + // create the dir + bSuccess = [fileMgr createDirectoryAtPath:reqFullPath withIntermediateDirectories:NO attributes:nil error:&pError]; + } else { + // create the empty file + bSuccess = [fileMgr createFileAtPath:reqFullPath contents:nil attributes:nil]; + } + } + if (!bSuccess) { + errorCode = ABORT_ERR; + if (pError) { + NSLog(@"error creating directory: %@", [pError localizedDescription]); + } + } else { + // NSLog(@"newly created file/dir (%@) exists: %d", reqFullPath, [fileMgr fileExistsAtPath:reqFullPath]); + // file existed or was created + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getDirectoryEntry:reqFullPath isDirectory:bDirRequest]]; + } + } // are all possible conditions met? + } + + if (errorCode > 0) { + // create error callback + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* + * Look up the parent Entry containing this Entry. + * If this Entry is the root of its filesystem, its parent is itself. + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * NSMutableDictionary* options + * empty + */ +- (void)getParent:(CDVInvokedUrlCommand*)command +{ + // arguments are URL encoded + NSString* fullPath = [command.arguments objectAtIndex:0]; + + // we don't (yet?) support getting the parent of an asset + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_READABLE_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + NSString* newPath = nil; + + if ([fullPath isEqualToString:self.appDocsPath] || [fullPath isEqualToString:self.appTempPath]) { + // return self + newPath = fullPath; + } else { + // since this call is made from an existing Entry object - the parent should already exist so no additional error checking + // remove last component and return Entry + NSRange range = [fullPath rangeOfString:@"/" options:NSBackwardsSearch]; + newPath = [fullPath substringToIndex:range.location]; + } + + if (newPath) { + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + BOOL bIsDir; + BOOL bExists = [fileMgr fileExistsAtPath:newPath isDirectory:&bIsDir]; + if (bExists) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getDirectoryEntry:newPath isDirectory:bIsDir]]; + } + } + if (!result) { + // invalid path or file does not exist + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* + * get MetaData of entry + * Currently MetaData only includes modificationTime. + */ +- (void)getMetadata:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + __block CDVPluginResult* result = nil; + + if ([argPath hasPrefix:kCDVAssetsLibraryPrefix]) { + // In this case, we need to use an asynchronous method to retrieve the file. + // Because of this, we can't just assign to `result` and send it at the end of the method. + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Retrieve the metadata and send it off. + NSDate* date = [asset valueForProperty:ALAssetPropertyDate]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:[date timeIntervalSince1970] * 1000]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } else { + // We couldn't find the asset. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + }; + // TODO(maxw): Consider making this a class variable since it's the same every time. + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:argPath] resultBlock:resultBlock failureBlock:failureBlock]; + return; + } + + NSString* testPath = argPath; // [self getFullPath: argPath]; + + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSError* __autoreleasing error = nil; + + NSDictionary* fileAttribs = [fileMgr attributesOfItemAtPath:testPath error:&error]; + + if (fileAttribs) { + NSDate* modDate = [fileAttribs fileModificationDate]; + if (modDate) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:[modDate timeIntervalSince1970] * 1000]; + } + } else { + // didn't get fileAttribs + CDVFileError errorCode = ABORT_ERR; + NSLog(@"error getting metadata: %@", [error localizedDescription]); + if ([error code] == NSFileNoSuchFileError) { + errorCode = NOT_FOUND_ERR; + } + // log [NSNumber numberWithDouble: theMessage] objCtype to see what it returns + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errorCode]; + } + if (!result) { + // invalid path or file does not exist + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* + * set MetaData of entry + * Currently we only support "com.apple.MobileBackup" (boolean) + */ +- (void)setMetadata:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* filePath = [command.arguments objectAtIndex:0]; + NSDictionary* options = [command.arguments objectAtIndex:1 withDefault:nil]; + CDVPluginResult* result = nil; + BOOL ok = NO; + + // setMetadata doesn't make sense for asset library files + if (![filePath hasPrefix:kCDVAssetsLibraryPrefix]) { + // we only care about this iCloud key for now. + // set to 1/true to skip backup, set to 0/false to back it up (effectively removing the attribute) + NSString* iCloudBackupExtendedAttributeKey = @"com.apple.MobileBackup"; + id iCloudBackupExtendedAttributeValue = [options objectForKey:iCloudBackupExtendedAttributeKey]; + + if ((iCloudBackupExtendedAttributeValue != nil) && [iCloudBackupExtendedAttributeValue isKindOfClass:[NSNumber class]]) { + if (IsAtLeastiOSVersion(@"5.1")) { + NSURL* url = [NSURL fileURLWithPath:filePath]; + NSError* __autoreleasing error = nil; + + ok = [url setResourceValue:[NSNumber numberWithBool:[iCloudBackupExtendedAttributeValue boolValue]] forKey:NSURLIsExcludedFromBackupKey error:&error]; + } else { // below 5.1 (deprecated - only really supported in 5.01) + u_int8_t value = [iCloudBackupExtendedAttributeValue intValue]; + if (value == 0) { // remove the attribute (allow backup, the default) + ok = (removexattr([filePath fileSystemRepresentation], [iCloudBackupExtendedAttributeKey cStringUsingEncoding:NSUTF8StringEncoding], 0) == 0); + } else { // set the attribute (skip backup) + ok = (setxattr([filePath fileSystemRepresentation], [iCloudBackupExtendedAttributeKey cStringUsingEncoding:NSUTF8StringEncoding], &value, sizeof(value), 0, 0) == 0); + } + } + } + } + + if (ok) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* removes the directory or file entry + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * + * returns NO_MODIFICATION_ALLOWED_ERR if is top level directory or no permission to delete dir + * returns INVALID_MODIFICATION_ERR if is non-empty dir or asset library file + * returns NOT_FOUND_ERR if file or dir is not found +*/ +- (void)remove:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* fullPath = [command.arguments objectAtIndex:0]; + CDVPluginResult* result = nil; + CDVFileError errorCode = 0; // !! 0 not currently defined + + // return error for assets-library URLs + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + errorCode = INVALID_MODIFICATION_ERR; + } else if ([fullPath isEqualToString:self.appDocsPath] || [fullPath isEqualToString:self.appTempPath]) { + // error if try to remove top level (documents or tmp) dir + errorCode = NO_MODIFICATION_ALLOWED_ERR; + } else { + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + BOOL bIsDir = NO; + BOOL bExists = [fileMgr fileExistsAtPath:fullPath isDirectory:&bIsDir]; + if (!bExists) { + errorCode = NOT_FOUND_ERR; + } + if (bIsDir && ([[fileMgr contentsOfDirectoryAtPath:fullPath error:nil] count] != 0)) { + // dir is not empty + errorCode = INVALID_MODIFICATION_ERR; + } + } + if (errorCode > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } else { + // perform actual remove + result = [self doRemove:fullPath]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* recursively removes the directory + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * + * returns NO_MODIFICATION_ALLOWED_ERR if is top level directory or no permission to delete dir + * returns NOT_FOUND_ERR if file or dir is not found + */ +- (void)removeRecursively:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* fullPath = [command.arguments objectAtIndex:0]; + + // return unsupported result for assets-library URLs + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"removeRecursively not supported for assets-library URLs."]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + + // error if try to remove top level (documents or tmp) dir + if ([fullPath isEqualToString:self.appDocsPath] || [fullPath isEqualToString:self.appTempPath]) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR]; + } else { + result = [self doRemove:fullPath]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* remove the file or directory (recursively) + * IN: + * NSString* fullPath - the full path to the file or directory to be removed + * NSString* callbackId + * called from remove and removeRecursively - check all pubic api specific error conditions (dir not empty, etc) before calling + */ + +- (CDVPluginResult*)doRemove:(NSString*)fullPath +{ + CDVPluginResult* result = nil; + BOOL bSuccess = NO; + NSError* __autoreleasing pError = nil; + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + + @try { + bSuccess = [fileMgr removeItemAtPath:fullPath error:&pError]; + if (bSuccess) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + // see if we can give a useful error + CDVFileError errorCode = ABORT_ERR; + NSLog(@"error getting metadata: %@", [pError localizedDescription]); + if ([pError code] == NSFileNoSuchFileError) { + errorCode = NOT_FOUND_ERR; + } else if ([pError code] == NSFileWriteNoPermissionError) { + errorCode = NO_MODIFICATION_ALLOWED_ERR; + } + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + } @catch(NSException* e) { // NSInvalidArgumentException if path is . or .. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:SYNTAX_ERR]; + } + + return result; +} + +- (void)copyTo:(CDVInvokedUrlCommand*)command +{ + [self doCopyMove:command isCopy:YES]; +} + +- (void)moveTo:(CDVInvokedUrlCommand*)command +{ + [self doCopyMove:command isCopy:NO]; +} + +/** + * Helper function to check to see if the user attempted to copy an entry into its parent without changing its name, + * or attempted to copy a directory into a directory that it contains directly or indirectly. + * + * IN: + * NSString* srcDir + * NSString* destinationDir + * OUT: + * YES copy/ move is allows + * NO move is onto itself + */ +- (BOOL)canCopyMoveSrc:(NSString*)src ToDestination:(NSString*)dest +{ + // This weird test is to determine if we are copying or moving a directory into itself. + // Copy /Documents/myDir to /Documents/myDir-backup is okay but + // Copy /Documents/myDir to /Documents/myDir/backup not okay + BOOL copyOK = YES; + NSRange range = [dest rangeOfString:src]; + + if (range.location != NSNotFound) { + NSRange testRange = {range.length - 1, ([dest length] - range.length)}; + NSRange resultRange = [dest rangeOfString:@"/" options:0 range:testRange]; + if (resultRange.location != NSNotFound) { + copyOK = NO; + } + } + return copyOK; +} + +/* Copy/move a file or directory to a new location + * IN: + * NSArray* arguments + * 0 - NSString* fullPath of entry + * 1 - NSString* newName the new name of the entry, defaults to the current name + * NSMutableDictionary* options - DirectoryEntry to which to copy the entry + * BOOL - bCopy YES if copy, NO if move + * + */ +- (void)doCopyMove:(CDVInvokedUrlCommand*)command isCopy:(BOOL)bCopy +{ + NSArray* arguments = command.arguments; + + // arguments + NSString* srcFullPath = [arguments objectAtIndex:0]; + NSString* destRootPath = [arguments objectAtIndex:1]; + // optional argument + NSString* newName = ([arguments count] > 2) ? [arguments objectAtIndex:2] : [srcFullPath lastPathComponent]; // use last component from appPath if new name not provided + + __block CDVPluginResult* result = nil; + CDVFileError errCode = 0; // !! Currently 0 is not defined, use this to signal error !! + + /*NSString* destRootPath = nil; + NSString* key = @"fullPath"; + if([options valueForKeyIsString:key]){ + destRootPath = [options objectForKey:@"fullPath"]; + }*/ + + if (!destRootPath) { + // no destination provided + errCode = NOT_FOUND_ERR; + } else if ([newName rangeOfString:@":"].location != NSNotFound) { + // invalid chars in new name + errCode = ENCODING_ERR; + } else { + NSString* newFullPath = [destRootPath stringByAppendingPathComponent:newName]; + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + if ([newFullPath isEqualToString:srcFullPath]) { + // source and destination can not be the same + errCode = INVALID_MODIFICATION_ERR; + } else if ([srcFullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + if (bCopy) { + // Copying (as opposed to moving) an assets library file is okay. + // In this case, we need to use an asynchronous method to retrieve the file. + // Because of this, we can't just assign to `result` and send it at the end of the method. + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Get the data and try to copy it over. + if (![fileMgr fileExistsAtPath:destRootPath]) { + // The destination directory doesn't exist. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } else if ([fileMgr fileExistsAtPath:newFullPath]) { + // A file already exists at the destination path. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:PATH_EXISTS_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + // We're good to go! Write the file to the new destination. + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + Byte* buffer = (Byte*)malloc([assetRepresentation size]); + NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil]; + NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; + [data writeToFile:newFullPath atomically:YES]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getDirectoryEntry:newFullPath isDirectory:NO]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } else { + // We couldn't find the asset. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:srcFullPath] resultBlock:resultBlock failureBlock:failureBlock]; + return; + } else { + // Moving an assets library file is not doable, since we can't remove it. + errCode = INVALID_MODIFICATION_ERR; + } + } else { + BOOL bSrcIsDir = NO; + BOOL bDestIsDir = NO; + BOOL bNewIsDir = NO; + BOOL bSrcExists = [fileMgr fileExistsAtPath:srcFullPath isDirectory:&bSrcIsDir]; + BOOL bDestExists = [fileMgr fileExistsAtPath:destRootPath isDirectory:&bDestIsDir]; + BOOL bNewExists = [fileMgr fileExistsAtPath:newFullPath isDirectory:&bNewIsDir]; + if (!bSrcExists || !bDestExists) { + // the source or the destination root does not exist + errCode = NOT_FOUND_ERR; + } else if (bSrcIsDir && (bNewExists && !bNewIsDir)) { + // can't copy/move dir to file + errCode = INVALID_MODIFICATION_ERR; + } else { // no errors yet + NSError* __autoreleasing error = nil; + BOOL bSuccess = NO; + if (bCopy) { + if (bSrcIsDir && ![self canCopyMoveSrc:srcFullPath ToDestination:newFullPath] /*[newFullPath hasPrefix:srcFullPath]*/) { + // can't copy dir into self + errCode = INVALID_MODIFICATION_ERR; + } else if (bNewExists) { + // the full destination should NOT already exist if a copy + errCode = PATH_EXISTS_ERR; + } else { + bSuccess = [fileMgr copyItemAtPath:srcFullPath toPath:newFullPath error:&error]; + } + } else { // move + // iOS requires that destination must not exist before calling moveTo + // is W3C INVALID_MODIFICATION_ERR error if destination dir exists and has contents + // + if (!bSrcIsDir && (bNewExists && bNewIsDir)) { + // can't move a file to directory + errCode = INVALID_MODIFICATION_ERR; + } else if (bSrcIsDir && ![self canCopyMoveSrc:srcFullPath ToDestination:newFullPath]) { // [newFullPath hasPrefix:srcFullPath]){ + // can't move a dir into itself + errCode = INVALID_MODIFICATION_ERR; + } else if (bNewExists) { + if (bNewIsDir && ([[fileMgr contentsOfDirectoryAtPath:newFullPath error:NULL] count] != 0)) { + // can't move dir to a dir that is not empty + errCode = INVALID_MODIFICATION_ERR; + newFullPath = nil; // so we won't try to move + } else { + // remove destination so can perform the moveItemAtPath + bSuccess = [fileMgr removeItemAtPath:newFullPath error:NULL]; + if (!bSuccess) { + errCode = INVALID_MODIFICATION_ERR; // is this the correct error? + newFullPath = nil; + } + } + } else if (bNewIsDir && [newFullPath hasPrefix:srcFullPath]) { + // can't move a directory inside itself or to any child at any depth; + errCode = INVALID_MODIFICATION_ERR; + newFullPath = nil; + } + + if (newFullPath != nil) { + bSuccess = [fileMgr moveItemAtPath:srcFullPath toPath:newFullPath error:&error]; + } + } + if (bSuccess) { + // should verify it is there and of the correct type??? + NSDictionary* newEntry = [self getDirectoryEntry:newFullPath isDirectory:bSrcIsDir]; // should be the same type as source + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:newEntry]; + } else { + errCode = INVALID_MODIFICATION_ERR; // catch all + if (error) { + if (([error code] == NSFileReadUnknownError) || ([error code] == NSFileReadTooLargeError)) { + errCode = NOT_READABLE_ERR; + } else if ([error code] == NSFileWriteOutOfSpaceError) { + errCode = QUOTA_EXCEEDED_ERR; + } else if ([error code] == NSFileWriteNoPermissionError) { + errCode = NO_MODIFICATION_ALLOWED_ERR; + } + } + } + } + } + } + if (errCode > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errCode]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* return the URI to the entry + * IN: + * NSArray* arguments + * 0 - NSString* fullPath of entry + * 1 - desired mime type of entry - ignored - always returns file:// + */ + +/* Not needed since W3C toURI is synchronous. Leaving code here for now in case W3C spec changes..... +- (void) toURI:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* argPath = [command.arguments objectAtIndex:0]; + PluginResult* result = nil; + NSString* jsString = nil; + + NSString* fullPath = [self getFullPath: argPath]; + if (fullPath) { + // do we need to make sure the file actually exists? + // create file uri + NSString* strUri = [fullPath stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; + NSURL* fileUrl = [NSURL fileURLWithPath:strUri]; + if (fileUrl) { + result = [PluginResult resultWithStatus:CDVCommandStatus_OK messageAsString: [fileUrl absoluteString]]; + jsString = [result toSuccessCallbackString:callbackId]; + } // else NOT_FOUND_ERR + } + if(!jsString) { + // was error + result = [PluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt: NOT_FOUND_ERR cast: @"window.localFileSystem._castError"]; + jsString = [result toErrorCallbackString:callbackId]; + } + + [self writeJavascript:jsString]; +}*/ +- (void)getFileMetadata:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + + __block CDVPluginResult* result = nil; + + NSString* fullPath = argPath; // [self getFullPath: argPath]; + + if (fullPath) { + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + // In this case, we need to use an asynchronous method to retrieve the file. + // Because of this, we can't just assign to `result` and send it at the end of the method. + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Populate the dictionary and send it off. + NSMutableDictionary* fileInfo = [NSMutableDictionary dictionaryWithCapacity:5]; + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + [fileInfo setObject:[NSNumber numberWithUnsignedLongLong:[assetRepresentation size]] forKey:@"size"]; + [fileInfo setObject:argPath forKey:@"fullPath"]; + NSString* filename = [assetRepresentation filename]; + [fileInfo setObject:filename forKey:@"name"]; + [fileInfo setObject:[self getMimeTypeFromPath:filename] forKey:@"type"]; + NSDate* creationDate = [asset valueForProperty:ALAssetPropertyDate]; + NSNumber* msDate = [NSNumber numberWithDouble:[creationDate timeIntervalSince1970] * 1000]; + [fileInfo setObject:msDate forKey:@"lastModifiedDate"]; + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileInfo]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } else { + // We couldn't find the asset. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:argPath] resultBlock:resultBlock failureBlock:failureBlock]; + return; + } else { + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + BOOL bIsDir = NO; + // make sure it exists and is not a directory + BOOL bExists = [fileMgr fileExistsAtPath:fullPath isDirectory:&bIsDir]; + if (!bExists || bIsDir) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + } else { + // create dictionary of file info + NSError* __autoreleasing error = nil; + NSDictionary* fileAttrs = [fileMgr attributesOfItemAtPath:fullPath error:&error]; + NSMutableDictionary* fileInfo = [NSMutableDictionary dictionaryWithCapacity:5]; + [fileInfo setObject:[NSNumber numberWithUnsignedLongLong:[fileAttrs fileSize]] forKey:@"size"]; + [fileInfo setObject:argPath forKey:@"fullPath"]; + [fileInfo setObject:@"" forKey:@"type"]; // can't easily get the mimetype unless create URL, send request and read response so skipping + [fileInfo setObject:[argPath lastPathComponent] forKey:@"name"]; + NSDate* modDate = [fileAttrs fileModificationDate]; + NSNumber* msDate = [NSNumber numberWithDouble:[modDate timeIntervalSince1970] * 1000]; + [fileInfo setObject:msDate forKey:@"lastModifiedDate"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileInfo]; + } + } + } + if (!result) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_INSTANTIATION_EXCEPTION]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (void)readEntries:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* fullPath = [command.arguments objectAtIndex:0]; + + // return unsupported result for assets-library URLs + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"readEntries not supported for assets-library URLs."]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSError* __autoreleasing error = nil; + NSArray* contents = [fileMgr contentsOfDirectoryAtPath:fullPath error:&error]; + + if (contents) { + NSMutableArray* entries = [NSMutableArray arrayWithCapacity:1]; + if ([contents count] > 0) { + // create an Entry (as JSON) for each file/dir + for (NSString* name in contents) { + // see if is dir or file + NSString* entryPath = [fullPath stringByAppendingPathComponent:name]; + BOOL bIsDir = NO; + [fileMgr fileExistsAtPath:entryPath isDirectory:&bIsDir]; + NSDictionary* entryDict = [self getDirectoryEntry:entryPath isDirectory:bIsDir]; + [entries addObject:entryDict]; + } + } + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:entries]; + } else { + // assume not found but could check error for more specific error conditions + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (void)readFileWithPath:(NSString*)path start:(NSInteger)start end:(NSInteger)end callback:(void (^)(NSData*, NSString* mimeType, CDVFileError))callback +{ + if (path == nil) { + callback(nil, nil, SYNTAX_ERR); + } else { + [self.commandDelegate runInBackground:^ { + if ([path hasPrefix:kCDVAssetsLibraryPrefix]) { + // In this case, we need to use an asynchronous method to retrieve the file. + // Because of this, we can't just assign to `result` and send it at the end of the method. + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Get the data and send it off. + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + Byte* buffer = (Byte*)malloc([assetRepresentation size]); + NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil]; + NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; + NSString* MIMEType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)[assetRepresentation UTI], kUTTagClassMIMEType); + + callback(data, MIMEType, NO_ERROR); + } else { + callback(nil, nil, NOT_FOUND_ERR); + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + NSLog(@"Error: %@", error); + callback(nil, nil, SECURITY_ERR); + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:path] resultBlock:resultBlock failureBlock:failureBlock]; + } else { + NSString* mimeType = [self getMimeTypeFromPath:path]; + if (mimeType == nil) { + mimeType = @"*/*"; + } + NSFileHandle* file = [NSFileHandle fileHandleForReadingAtPath:path]; + if (start > 0) { + [file seekToFileOffset:start]; + } + + NSData* readData; + if (end < 0) { + readData = [file readDataToEndOfFile]; + } else { + readData = [file readDataOfLength:(end - start)]; + } + + [file closeFile]; + + callback(readData, mimeType, readData != nil ? NO_ERROR : NOT_FOUND_ERR); + } + }]; + } +} + +/* read and return file data + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * 1 - NSString* encoding + * 2 - NSString* start + * 3 - NSString* end + */ +- (void)readAsText:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* path = [command argumentAtIndex:0]; + NSString* encoding = [command argumentAtIndex:1]; + NSInteger start = [[command argumentAtIndex:2] integerValue]; + NSInteger end = [[command argumentAtIndex:3] integerValue]; + + // TODO: implement + if (![@"UTF-8" isEqualToString : encoding]) { + NSLog(@"Only UTF-8 encodings are currently supported by readAsText"); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:ENCODING_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + [self readFileWithPath:path start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) { + CDVPluginResult* result = nil; + if (data != nil) { + NSString* str = [[NSString alloc] initWithBytesNoCopy:(void*)[data bytes] length:[data length] encoding:NSUTF8StringEncoding freeWhenDone:NO]; + // Check that UTF8 conversion did not fail. + if (str != nil) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:str]; + result.associatedObject = data; + } else { + errorCode = ENCODING_ERR; + } + } + if (result == nil) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +/* Read content of text file and return as base64 encoded data url. + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * 1 - NSString* start + * 2 - NSString* end + * + * Determines the mime type from the file extension, returns ENCODING_ERR if mimetype can not be determined. + */ + +- (void)readAsDataURL:(CDVInvokedUrlCommand*)command +{ + NSString* path = [command argumentAtIndex:0]; + NSInteger start = [[command argumentAtIndex:1] integerValue]; + NSInteger end = [[command argumentAtIndex:2] integerValue]; + + [self readFileWithPath:path start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) { + CDVPluginResult* result = nil; + if (data != nil) { + // TODO: Would be faster to base64 encode directly to the final string. + NSString* output = [NSString stringWithFormat:@"data:%@;base64,%@", mimeType, [data base64EncodedString]]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:output]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +/* Read content of text file and return as an arraybuffer + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * 1 - NSString* start + * 2 - NSString* end + */ + +- (void)readAsArrayBuffer:(CDVInvokedUrlCommand*)command +{ + NSString* path = [command argumentAtIndex:0]; + NSInteger start = [[command argumentAtIndex:1] integerValue]; + NSInteger end = [[command argumentAtIndex:2] integerValue]; + + [self readFileWithPath:path start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) { + CDVPluginResult* result = nil; + if (data != nil) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArrayBuffer:data]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +- (void)readAsBinaryString:(CDVInvokedUrlCommand*)command +{ + NSString* path = [command argumentAtIndex:0]; + NSInteger start = [[command argumentAtIndex:1] integerValue]; + NSInteger end = [[command argumentAtIndex:2] integerValue]; + + [self readFileWithPath:path start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) { + CDVPluginResult* result = nil; + if (data != nil) { + NSString* payload = [[NSString alloc] initWithBytesNoCopy:(void*)[data bytes] length:[data length] encoding:NSASCIIStringEncoding freeWhenDone:NO]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:payload]; + result.associatedObject = data; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +/* helper function to get the mimeType from the file extension + * IN: + * NSString* fullPath - filename (may include path) + * OUT: + * NSString* the mime type as type/subtype. nil if not able to determine + */ +- (NSString*)getMimeTypeFromPath:(NSString*)fullPath +{ + NSString* mimeType = nil; + + if (fullPath) { + CFStringRef typeId = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fullPath pathExtension], NULL); + if (typeId) { + mimeType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass(typeId, kUTTagClassMIMEType); + if (!mimeType) { + // special case for m4a + if ([(__bridge NSString*)typeId rangeOfString : @"m4a-audio"].location != NSNotFound) { + mimeType = @"audio/mp4"; + } else if ([[fullPath pathExtension] rangeOfString:@"wav"].location != NSNotFound) { + mimeType = @"audio/wav"; + } + } + CFRelease(typeId); + } + } + return mimeType; +} + +- (void)truncate:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + unsigned long long pos = (unsigned long long)[[command.arguments objectAtIndex:1] longLongValue]; + + // assets-library files can't be truncated + if ([argPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + NSString* appFile = argPath; // [self getFullPath:argPath]; + + unsigned long long newPos = [self truncateFile:appFile atPosition:pos]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:newPos]; + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (unsigned long long)truncateFile:(NSString*)filePath atPosition:(unsigned long long)pos +{ + unsigned long long newPos = 0UL; + + NSFileHandle* file = [NSFileHandle fileHandleForWritingAtPath:filePath]; + + if (file) { + [file truncateFileAtOffset:(unsigned long long)pos]; + newPos = [file offsetInFile]; + [file synchronizeFile]; + [file closeFile]; + } + return newPos; +} + +/* write + * IN: + * NSArray* arguments + * 0 - NSString* file path to write to + * 1 - NSString* data to write + * 2 - NSNumber* position to begin writing + */ +- (void)write:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSArray* arguments = command.arguments; + + // arguments + NSString* argPath = [arguments objectAtIndex:0]; + NSString* argData = [arguments objectAtIndex:1]; + unsigned long long pos = (unsigned long long)[[arguments objectAtIndex:2] longLongValue]; + + // text can't be written into assets-library files + if ([argPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + NSString* fullPath = argPath; // [self getFullPath:argPath]; + + [self truncateFile:fullPath atPosition:pos]; + + [self writeToFile:fullPath withData:argData append:YES callback:callbackId]; +} + +- (void)writeToFile:(NSString*)filePath withData:(NSString*)data append:(BOOL)shouldAppend callback:(NSString*)callbackId +{ + CDVPluginResult* result = nil; + CDVFileError errCode = INVALID_MODIFICATION_ERR; + int bytesWritten = 0; + NSData* encData = [data dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; + + if (filePath) { + NSOutputStream* fileStream = [NSOutputStream outputStreamToFileAtPath:filePath append:shouldAppend]; + if (fileStream) { + NSUInteger len = [encData length]; + [fileStream open]; + + bytesWritten = [fileStream write:[encData bytes] maxLength:len]; + + [fileStream close]; + if (bytesWritten > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:bytesWritten]; + // } else { + // can probably get more detailed error info via [fileStream streamError] + // errCode already set to INVALID_MODIFICATION_ERR; + // bytesWritten = 0; // may be set to -1 on error + } + } // else fileStream not created return INVALID_MODIFICATION_ERR + } else { + // invalid filePath + errCode = NOT_FOUND_ERR; + } + if (!result) { + // was an error + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errCode]; + } + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)testFileExists:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + + // Get the file manager + NSFileManager* fMgr = [NSFileManager defaultManager]; + NSString* appFile = argPath; // [ self getFullPath: argPath]; + + BOOL bExists = [fMgr fileExistsAtPath:appFile]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:(bExists ? 1 : 0)]; + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (void)testDirectoryExists:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + + // Get the file manager + NSFileManager* fMgr = [[NSFileManager alloc] init]; + NSString* appFile = argPath; // [self getFullPath: argPath]; + BOOL bIsDir = NO; + BOOL bExists = [fMgr fileExistsAtPath:appFile isDirectory:&bIsDir]; + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:((bExists && bIsDir) ? 1 : 0)]; + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +// Returns number of bytes available via callback +- (void)getFreeDiskSpace:(CDVInvokedUrlCommand*)command +{ + // no arguments + + NSNumber* pNumAvail = [self checkFreeDiskSpace:self.appDocsPath]; + + NSString* strFreeSpace = [NSString stringWithFormat:@"%qu", [pNumAvail unsignedLongLongValue]]; + // NSLog(@"Free space is %@", strFreeSpace ); + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:strFreeSpace]; + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVFileTransfer.h b/cordova/ios/CordovaLib/Classes/CDVFileTransfer.h new file mode 100755 index 000000000..233a1147c --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVFileTransfer.h @@ -0,0 +1,81 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +enum CDVFileTransferError { + FILE_NOT_FOUND_ERR = 1, + INVALID_URL_ERR = 2, + CONNECTION_ERR = 3, + CONNECTION_ABORTED = 4 +}; +typedef int CDVFileTransferError; + +enum CDVFileTransferDirection { + CDV_TRANSFER_UPLOAD = 1, + CDV_TRANSFER_DOWNLOAD = 2, +}; +typedef int CDVFileTransferDirection; + +// Magic value within the options dict used to set a cookie. +extern NSString* const kOptionsKeyCookie; + +@interface CDVFileTransfer : CDVPlugin {} + +- (void)upload:(CDVInvokedUrlCommand*)command; +- (void)download:(CDVInvokedUrlCommand*)command; +- (NSString*)escapePathComponentForUrlString:(NSString*)urlString; + +// Visible for testing. +- (NSURLRequest*)requestForUploadCommand:(CDVInvokedUrlCommand*)command fileData:(NSData*)fileData; +- (NSMutableDictionary*)createFileTransferError:(int)code AndSource:(NSString*)source AndTarget:(NSString*)target; + +- (NSMutableDictionary*)createFileTransferError:(int)code + AndSource:(NSString*)source + AndTarget:(NSString*)target + AndHttpStatus:(int)httpStatus + AndBody:(NSString*)body; +@property (readonly) NSMutableDictionary* activeTransfers; +@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskID; +@end + +@class CDVFileTransferEntityLengthRequest; + +@interface CDVFileTransferDelegate : NSObject {} + +- (void)updateBytesExpected:(NSInteger)newBytesExpected; + +@property (strong) NSMutableData* responseData; // atomic +@property (nonatomic, strong) CDVFileTransfer* command; +@property (nonatomic, assign) CDVFileTransferDirection direction; +@property (nonatomic, strong) NSURLConnection* connection; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSString* objectId; +@property (nonatomic, copy) NSString* source; +@property (nonatomic, copy) NSString* target; +@property (nonatomic, copy) NSString* mimeType; +@property (assign) int responseCode; // atomic +@property (nonatomic, assign) NSInteger bytesTransfered; +@property (nonatomic, assign) NSInteger bytesExpected; +@property (nonatomic, assign) BOOL trustAllHosts; +@property (strong) NSFileHandle* targetFileHandle; +@property (nonatomic, strong) CDVFileTransferEntityLengthRequest* entityLengthRequest; + +@end; diff --git a/cordova/ios/CordovaLib/Classes/CDVFileTransfer.m b/cordova/ios/CordovaLib/Classes/CDVFileTransfer.m new file mode 100755 index 000000000..553671557 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVFileTransfer.m @@ -0,0 +1,714 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDV.h" + +#import +#import +#import +#import + +@interface CDVFileTransfer () +// Sets the requests headers for the request. +- (void)applyRequestHeaders:(NSDictionary*)headers toRequest:(NSMutableURLRequest*)req; +// Creates a delegate to handle an upload. +- (CDVFileTransferDelegate*)delegateForUploadCommand:(CDVInvokedUrlCommand*)command; +// Creates an NSData* for the file for the given upload arguments. +- (void)fileDataForUploadCommand:(CDVInvokedUrlCommand*)command; +@end + +// Buffer size to use for streaming uploads. +static const NSUInteger kStreamBufferSize = 32768; +// Magic value within the options dict used to set a cookie. +NSString* const kOptionsKeyCookie = @"__cookie"; +// Form boundary for multi-part requests. +NSString* const kFormBoundary = @"+++++org.apache.cordova.formBoundary"; + +// Writes the given data to the stream in a blocking way. +// If successful, returns bytesToWrite. +// If the stream was closed on the other end, returns 0. +// If there was an error, returns -1. +static CFIndex WriteDataToStream(NSData* data, CFWriteStreamRef stream) +{ + UInt8* bytes = (UInt8*)[data bytes]; + NSUInteger bytesToWrite = [data length]; + NSUInteger totalBytesWritten = 0; + + while (totalBytesWritten < bytesToWrite) { + CFIndex result = CFWriteStreamWrite(stream, + bytes + totalBytesWritten, + bytesToWrite - totalBytesWritten); + if (result < 0) { + CFStreamError error = CFWriteStreamGetError(stream); + NSLog(@"WriteStreamError domain: %ld error: %ld", error.domain, error.error); + return result; + } else if (result == 0) { + return result; + } + totalBytesWritten += result; + } + + return totalBytesWritten; +} + +@implementation CDVFileTransfer +@synthesize activeTransfers; + +- (NSString*)escapePathComponentForUrlString:(NSString*)urlString +{ + NSRange schemeAndHostRange = [urlString rangeOfString:@"://.*?/" options:NSRegularExpressionSearch]; + + if (schemeAndHostRange.length == 0) { + return urlString; + } + + NSInteger schemeAndHostEndIndex = NSMaxRange(schemeAndHostRange); + NSString* schemeAndHost = [urlString substringToIndex:schemeAndHostEndIndex]; + NSString* pathComponent = [urlString substringFromIndex:schemeAndHostEndIndex]; + pathComponent = [pathComponent stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + return [schemeAndHost stringByAppendingString:pathComponent]; +} + +- (void)applyRequestHeaders:(NSDictionary*)headers toRequest:(NSMutableURLRequest*)req +{ + [req setValue:@"XMLHttpRequest" forHTTPHeaderField:@"X-Requested-With"]; + + NSString* userAgent = [self.commandDelegate userAgent]; + if (userAgent) { + [req setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + + for (NSString* headerName in headers) { + id value = [headers objectForKey:headerName]; + if (!value || (value == [NSNull null])) { + value = @"null"; + } + + // First, remove an existing header if one exists. + [req setValue:nil forHTTPHeaderField:headerName]; + + if (![value isKindOfClass:[NSArray class]]) { + value = [NSArray arrayWithObject:value]; + } + + // Then, append all header values. + for (id __strong subValue in value) { + // Convert from an NSNumber -> NSString. + if ([subValue respondsToSelector:@selector(stringValue)]) { + subValue = [subValue stringValue]; + } + if ([subValue isKindOfClass:[NSString class]]) { + [req addValue:subValue forHTTPHeaderField:headerName]; + } + } + } +} + +- (NSURLRequest*)requestForUploadCommand:(CDVInvokedUrlCommand*)command fileData:(NSData*)fileData +{ + // arguments order from js: [filePath, server, fileKey, fileName, mimeType, params, debug, chunkedMode] + // however, params is a JavaScript object and during marshalling is put into the options dict, + // thus debug and chunkedMode are the 6th and 7th arguments + NSString* target = [command argumentAtIndex:0]; + NSString* server = [command argumentAtIndex:1]; + NSString* fileKey = [command argumentAtIndex:2 withDefault:@"file"]; + NSString* fileName = [command argumentAtIndex:3 withDefault:@"no-filename"]; + NSString* mimeType = [command argumentAtIndex:4 withDefault:nil]; + NSDictionary* options = [command argumentAtIndex:5 withDefault:nil]; + // BOOL trustAllHosts = [[arguments objectAtIndex:6 withDefault:[NSNumber numberWithBool:YES]] boolValue]; // allow self-signed certs + BOOL chunkedMode = [[command argumentAtIndex:7 withDefault:[NSNumber numberWithBool:YES]] boolValue]; + NSDictionary* headers = [command argumentAtIndex:8 withDefault:nil]; + // Allow alternative http method, default to POST. JS side checks + // for allowed methods, currently PUT or POST (forces POST for + // unrecognised values) + NSString* httpMethod = [command argumentAtIndex:10 withDefault:@"POST"]; + CDVPluginResult* result = nil; + CDVFileTransferError errorCode = 0; + + // NSURL does not accepts URLs with spaces in the path. We escape the path in order + // to be more lenient. + NSURL* url = [NSURL URLWithString:server]; + + if (!url) { + errorCode = INVALID_URL_ERR; + NSLog(@"File Transfer Error: Invalid server URL %@", server); + } else if (!fileData) { + errorCode = FILE_NOT_FOUND_ERR; + } + + if (errorCode > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:errorCode AndSource:target AndTarget:server]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return nil; + } + + NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url]; + + [req setHTTPMethod:httpMethod]; + + // Magic value to set a cookie + if ([options objectForKey:kOptionsKeyCookie]) { + [req setValue:[options objectForKey:kOptionsKeyCookie] forHTTPHeaderField:@"Cookie"]; + [req setHTTPShouldHandleCookies:NO]; + } + + NSString* contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kFormBoundary]; + [req setValue:contentType forHTTPHeaderField:@"Content-Type"]; + [self applyRequestHeaders:headers toRequest:req]; + + NSData* formBoundaryData = [[NSString stringWithFormat:@"--%@\r\n", kFormBoundary] dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableData* postBodyBeforeFile = [NSMutableData data]; + + for (NSString* key in options) { + id val = [options objectForKey:key]; + if (!val || (val == [NSNull null]) || [key isEqualToString:kOptionsKeyCookie]) { + continue; + } + // if it responds to stringValue selector (eg NSNumber) get the NSString + if ([val respondsToSelector:@selector(stringValue)]) { + val = [val stringValue]; + } + // finally, check whether it is a NSString (for dataUsingEncoding selector below) + if (![val isKindOfClass:[NSString class]]) { + continue; + } + + [postBodyBeforeFile appendData:formBoundaryData]; + [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBodyBeforeFile appendData:[val dataUsingEncoding:NSUTF8StringEncoding]]; + [postBodyBeforeFile appendData:[@"\r\n" dataUsingEncoding : NSUTF8StringEncoding]]; + } + + [postBodyBeforeFile appendData:formBoundaryData]; + [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fileKey, fileName] dataUsingEncoding:NSUTF8StringEncoding]]; + if (mimeType != nil) { + [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n", mimeType] dataUsingEncoding:NSUTF8StringEncoding]]; + } + [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Length: %d\r\n\r\n", [fileData length]] dataUsingEncoding:NSUTF8StringEncoding]]; + + DLog(@"fileData length: %d", [fileData length]); + NSData* postBodyAfterFile = [[NSString stringWithFormat:@"\r\n--%@--\r\n", kFormBoundary] dataUsingEncoding:NSUTF8StringEncoding]; + + NSUInteger totalPayloadLength = [postBodyBeforeFile length] + [fileData length] + [postBodyAfterFile length]; + [req setValue:[[NSNumber numberWithInteger:totalPayloadLength] stringValue] forHTTPHeaderField:@"Content-Length"]; + + if (chunkedMode) { + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + CFStreamCreateBoundPair(NULL, &readStream, &writeStream, kStreamBufferSize); + [req setHTTPBodyStream:CFBridgingRelease(readStream)]; + + self.backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskID]; + self.backgroundTaskID = UIBackgroundTaskInvalid; + NSLog(@"Background task to upload media finished."); + }]; + + [self.commandDelegate runInBackground:^{ + if (CFWriteStreamOpen(writeStream)) { + NSData* chunks[] = {postBodyBeforeFile, fileData, postBodyAfterFile}; + int numChunks = sizeof(chunks) / sizeof(chunks[0]); + + for (int i = 0; i < numChunks; ++i) { + CFIndex result = WriteDataToStream(chunks[i], writeStream); + if (result <= 0) { + break; + } + } + } else { + NSLog(@"FileTransfer: Failed to open writeStream"); + } + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + }]; + } else { + [postBodyBeforeFile appendData:fileData]; + [postBodyBeforeFile appendData:postBodyAfterFile]; + [req setHTTPBody:postBodyBeforeFile]; + } + return req; +} + +- (CDVFileTransferDelegate*)delegateForUploadCommand:(CDVInvokedUrlCommand*)command +{ + NSString* source = [command.arguments objectAtIndex:0]; + NSString* server = [command.arguments objectAtIndex:1]; + BOOL trustAllHosts = [[command.arguments objectAtIndex:6 withDefault:[NSNumber numberWithBool:YES]] boolValue]; // allow self-signed certs + NSString* objectId = [command.arguments objectAtIndex:9]; + + CDVFileTransferDelegate* delegate = [[CDVFileTransferDelegate alloc] init]; + + delegate.command = self; + delegate.callbackId = command.callbackId; + delegate.direction = CDV_TRANSFER_UPLOAD; + delegate.objectId = objectId; + delegate.source = source; + delegate.target = server; + delegate.trustAllHosts = trustAllHosts; + + return delegate; +} + +- (void)fileDataForUploadCommand:(CDVInvokedUrlCommand*)command +{ + NSString* target = (NSString*)[command.arguments objectAtIndex:0]; + NSError* __autoreleasing err = nil; + + // return unsupported result for assets-library URLs + if ([target hasPrefix:kCDVAssetsLibraryPrefix]) { + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Get the data and send it off. + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + Byte* buffer = (Byte*)malloc([assetRepresentation size]); + NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil]; + NSData* fileData = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; + [self uploadData:fileData command:command]; + } else { + // We couldn't find the asset. Send the appropriate error. + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:target] resultBlock:resultBlock failureBlock:failureBlock]; + return; + } else { + // Extract the path part out of a file: URL. + NSString* filePath = [target hasPrefix:@"/"] ? [target copy] : [[NSURL URLWithString:target] path]; + if (filePath == nil) { + // We couldn't find the asset. Send the appropriate error. + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + // Memory map the file so that it can be read efficiently even if it is large. + NSData* fileData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&err]; + + if (err != nil) { + NSLog(@"Error opening file %@: %@", target, err); + } + [self uploadData:fileData command:command]; + } +} + +- (void)upload:(CDVInvokedUrlCommand*)command +{ + // fileData and req are split into helper functions to ease the unit testing of delegateForUpload. + // First, get the file data. This method will call `uploadData:command`. + [self fileDataForUploadCommand:command]; +} + +- (void)uploadData:(NSData*)fileData command:(CDVInvokedUrlCommand*)command +{ + NSURLRequest* req = [self requestForUploadCommand:command fileData:fileData]; + + if (req == nil) { + return; + } + CDVFileTransferDelegate* delegate = [self delegateForUploadCommand:command]; + [NSURLConnection connectionWithRequest:req delegate:delegate]; + + if (activeTransfers == nil) { + activeTransfers = [[NSMutableDictionary alloc] init]; + } + + [activeTransfers setObject:delegate forKey:delegate.objectId]; +} + +- (void)abort:(CDVInvokedUrlCommand*)command +{ + NSString* objectId = [command.arguments objectAtIndex:0]; + + CDVFileTransferDelegate* delegate = [activeTransfers objectForKey:objectId]; + + if (delegate != nil) { + [delegate.connection cancel]; + [activeTransfers removeObjectForKey:objectId]; + + // delete uncomplete file + NSFileManager* fileMgr = [NSFileManager defaultManager]; + [fileMgr removeItemAtPath:delegate.target error:nil]; + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:CONNECTION_ABORTED AndSource:delegate.source AndTarget:delegate.target]]; + [self.commandDelegate sendPluginResult:result callbackId:delegate.callbackId]; + } +} + +- (void)download:(CDVInvokedUrlCommand*)command +{ + DLog(@"File Transfer downloading file..."); + NSString* sourceUrl = [command.arguments objectAtIndex:0]; + NSString* filePath = [command.arguments objectAtIndex:1]; + BOOL trustAllHosts = [[command.arguments objectAtIndex:2 withDefault:[NSNumber numberWithBool:YES]] boolValue]; // allow self-signed certs + NSString* objectId = [command.arguments objectAtIndex:3]; + NSDictionary* headers = [command.arguments objectAtIndex:4 withDefault:nil]; + + // return unsupported result for assets-library URLs + if ([filePath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"download not supported for assets-library URLs."]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + CDVFileTransferError errorCode = 0; + + NSURL* file; + + if ([filePath hasPrefix:@"/"]) { + file = [NSURL fileURLWithPath:filePath]; + } else { + file = [NSURL URLWithString:filePath]; + } + + NSURL* url = [NSURL URLWithString:sourceUrl]; + + if (!url) { + errorCode = INVALID_URL_ERR; + NSLog(@"File Transfer Error: Invalid server URL %@", sourceUrl); + } else if (![file isFileURL]) { + errorCode = FILE_NOT_FOUND_ERR; + NSLog(@"File Transfer Error: Invalid file path or URL %@", filePath); + } + + if (errorCode > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:errorCode AndSource:sourceUrl AndTarget:filePath]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url]; + [self applyRequestHeaders:headers toRequest:req]; + + CDVFileTransferDelegate* delegate = [[CDVFileTransferDelegate alloc] init]; + delegate.command = self; + delegate.direction = CDV_TRANSFER_DOWNLOAD; + delegate.callbackId = command.callbackId; + delegate.objectId = objectId; + delegate.source = sourceUrl; + delegate.target = filePath; + delegate.trustAllHosts = trustAllHosts; + + delegate.connection = [NSURLConnection connectionWithRequest:req delegate:delegate]; + + if (activeTransfers == nil) { + activeTransfers = [[NSMutableDictionary alloc] init]; + } + + [activeTransfers setObject:delegate forKey:delegate.objectId]; +} + +- (NSMutableDictionary*)createFileTransferError:(int)code AndSource:(NSString*)source AndTarget:(NSString*)target +{ + NSMutableDictionary* result = [NSMutableDictionary dictionaryWithCapacity:3]; + + [result setObject:[NSNumber numberWithInt:code] forKey:@"code"]; + [result setObject:source forKey:@"source"]; + [result setObject:target forKey:@"target"]; + NSLog(@"FileTransferError %@", result); + + return result; +} + +- (NSMutableDictionary*)createFileTransferError:(int)code + AndSource:(NSString*)source + AndTarget:(NSString*)target + AndHttpStatus:(int)httpStatus + AndBody:(NSString*)body +{ + NSMutableDictionary* result = [NSMutableDictionary dictionaryWithCapacity:5]; + + [result setObject:[NSNumber numberWithInt:code] forKey:@"code"]; + [result setObject:source forKey:@"source"]; + [result setObject:target forKey:@"target"]; + [result setObject:[NSNumber numberWithInt:httpStatus] forKey:@"http_status"]; + [result setObject:body forKey:@"body"]; + NSLog(@"FileTransferError %@", result); + + return result; +} + +- (void)onReset +{ + for (CDVFileTransferDelegate* delegate in [activeTransfers allValues]) { + [delegate.connection cancel]; + } + + [activeTransfers removeAllObjects]; +} + +@end + +@interface CDVFileTransferEntityLengthRequest : NSObject { + NSURLConnection* _connection; + CDVFileTransferDelegate* __weak _originalDelegate; +} + +- (CDVFileTransferEntityLengthRequest*)initWithOriginalRequest:(NSURLRequest*)originalRequest andDelegate:(CDVFileTransferDelegate*)originalDelegate; + +@end; + +@implementation CDVFileTransferEntityLengthRequest; + +- (CDVFileTransferEntityLengthRequest*)initWithOriginalRequest:(NSURLRequest*)originalRequest andDelegate:(CDVFileTransferDelegate*)originalDelegate +{ + if (self) { + DLog(@"Requesting entity length for GZIPped content..."); + + NSMutableURLRequest* req = [originalRequest mutableCopy]; + [req setHTTPMethod:@"HEAD"]; + [req setValue:@"identity" forHTTPHeaderField:@"Accept-Encoding"]; + + _originalDelegate = originalDelegate; + _connection = [NSURLConnection connectionWithRequest:req delegate:self]; + } + return self; +} + +- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response +{ + DLog(@"HEAD request returned; content-length is %lld", [response expectedContentLength]); + [_originalDelegate updateBytesExpected:[response expectedContentLength]]; +} + +- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data +{} + +- (void)connectionDidFinishLoading:(NSURLConnection*)connection +{} + +@end + +@implementation CDVFileTransferDelegate + +@synthesize callbackId, connection = _connection, source, target, responseData, command, bytesTransfered, bytesExpected, direction, responseCode, objectId, targetFileHandle; + +- (void)connectionDidFinishLoading:(NSURLConnection*)connection +{ + NSString* uploadResponse = nil; + NSString* downloadResponse = nil; + NSMutableDictionary* uploadResult; + CDVPluginResult* result = nil; + BOOL bDirRequest = NO; + CDVFile* file; + + NSLog(@"File Transfer Finished with response code %d", self.responseCode); + + if (self.direction == CDV_TRANSFER_UPLOAD) { + uploadResponse = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding]; + + if ((self.responseCode >= 200) && (self.responseCode < 300)) { + // create dictionary to return FileUploadResult object + uploadResult = [NSMutableDictionary dictionaryWithCapacity:3]; + if (uploadResponse != nil) { + [uploadResult setObject:uploadResponse forKey:@"response"]; + } + [uploadResult setObject:[NSNumber numberWithInt:self.bytesTransfered] forKey:@"bytesSent"]; + [uploadResult setObject:[NSNumber numberWithInt:self.responseCode] forKey:@"responseCode"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:uploadResult]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:CONNECTION_ERR AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:uploadResponse]]; + } + } + if (self.direction == CDV_TRANSFER_DOWNLOAD) { + if (self.targetFileHandle) { + [self.targetFileHandle closeFile]; + self.targetFileHandle = nil; + DLog(@"File Transfer Download success"); + + file = [[CDVFile alloc] init]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[file getDirectoryEntry:target isDirectory:bDirRequest]]; + } else { + downloadResponse = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:CONNECTION_ERR AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:downloadResponse]]; + } + } + + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; + + // remove connection for activeTransfers + [command.activeTransfers removeObjectForKey:objectId]; + + // remove background id task in case our upload was done in the background + [[UIApplication sharedApplication] endBackgroundTask:self.command.backgroundTaskID]; + self.command.backgroundTaskID = UIBackgroundTaskInvalid; +} + +- (void)cancelTransferWithError:(NSURLConnection*)connection errorMessage:(NSString*)errorMessage +{ + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsDictionary:[self.command createFileTransferError:FILE_NOT_FOUND_ERR AndSource:self.source AndTarget:self.target AndHttpStatus:self.responseCode AndBody:errorMessage]]; + + NSLog(@"File Transfer Error: %@", errorMessage); + [connection cancel]; + [self.command.activeTransfers removeObjectForKey:self.objectId]; + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response +{ + NSError* __autoreleasing error = nil; + + self.mimeType = [response MIMEType]; + self.targetFileHandle = nil; + + // required for iOS 4.3, for some reason; response is + // a plain NSURLResponse, not the HTTP subclass + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; + + self.responseCode = [httpResponse statusCode]; + self.bytesExpected = [response expectedContentLength]; + if ((self.direction == CDV_TRANSFER_DOWNLOAD) && (self.responseCode == 200) && (self.bytesExpected == NSURLResponseUnknownLength)) { + // Kick off HEAD request to server to get real length + // bytesExpected will be updated when that response is returned + self.entityLengthRequest = [[CDVFileTransferEntityLengthRequest alloc] initWithOriginalRequest:connection.currentRequest andDelegate:self]; + } + } else if ([response.URL isFileURL]) { + NSDictionary* attr = [[NSFileManager defaultManager] attributesOfItemAtPath:[response.URL path] error:nil]; + self.responseCode = 200; + self.bytesExpected = [attr[NSFileSize] longLongValue]; + } else { + self.responseCode = 200; + self.bytesExpected = NSURLResponseUnknownLength; + } + if ((self.direction == CDV_TRANSFER_DOWNLOAD) && (self.responseCode >= 200) && (self.responseCode < 300)) { + // Download response is okay; begin streaming output to file + NSString* parentPath = [self.target stringByDeletingLastPathComponent]; + + // create parent directories if needed + if ([[NSFileManager defaultManager] createDirectoryAtPath:parentPath withIntermediateDirectories:YES attributes:nil error:&error] == NO) { + if (error) { + [self cancelTransferWithError:connection errorMessage:[NSString stringWithFormat:@"Could not create path to save downloaded file: %@", [error localizedDescription]]]; + } else { + [self cancelTransferWithError:connection errorMessage:@"Could not create path to save downloaded file"]; + } + return; + } + // create target file + if ([[NSFileManager defaultManager] createFileAtPath:self.target contents:nil attributes:nil] == NO) { + [self cancelTransferWithError:connection errorMessage:@"Could not create target file"]; + return; + } + // open target file for writing + self.targetFileHandle = [NSFileHandle fileHandleForWritingAtPath:self.target]; + if (self.targetFileHandle == nil) { + [self cancelTransferWithError:connection errorMessage:@"Could not open target file for writing"]; + } + DLog(@"Streaming to file %@", target); + } +} + +- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error +{ + NSString* body = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:CONNECTION_ERR AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:body]]; + + NSLog(@"File Transfer Error: %@", [error localizedDescription]); + + // remove connection for activeTransfers + [command.activeTransfers removeObjectForKey:objectId]; + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data +{ + self.bytesTransfered += data.length; + if (self.targetFileHandle) { + [self.targetFileHandle writeData:data]; + } else { + [self.responseData appendData:data]; + } + [self updateProgress]; +} + +- (void)updateBytesExpected:(NSInteger)newBytesExpected +{ + DLog(@"Updating bytesExpected to %d", newBytesExpected); + self.bytesExpected = newBytesExpected; + [self updateProgress]; +} + +- (void)updateProgress +{ + if (self.direction == CDV_TRANSFER_DOWNLOAD) { + BOOL lengthComputable = (self.bytesExpected != NSURLResponseUnknownLength); + // If the response is GZipped, and we have an outstanding HEAD request to get + // the length, then hold off on sending progress events. + if (!lengthComputable && (self.entityLengthRequest != nil)) { + return; + } + NSMutableDictionary* downloadProgress = [NSMutableDictionary dictionaryWithCapacity:3]; + [downloadProgress setObject:[NSNumber numberWithBool:lengthComputable] forKey:@"lengthComputable"]; + [downloadProgress setObject:[NSNumber numberWithInt:self.bytesTransfered] forKey:@"loaded"]; + [downloadProgress setObject:[NSNumber numberWithInt:self.bytesExpected] forKey:@"total"]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:downloadProgress]; + [result setKeepCallbackAsBool:true]; + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (void)connection:(NSURLConnection*)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite +{ + if (self.direction == CDV_TRANSFER_UPLOAD) { + NSMutableDictionary* uploadProgress = [NSMutableDictionary dictionaryWithCapacity:3]; + + [uploadProgress setObject:[NSNumber numberWithBool:true] forKey:@"lengthComputable"]; + [uploadProgress setObject:[NSNumber numberWithInt:totalBytesWritten] forKey:@"loaded"]; + [uploadProgress setObject:[NSNumber numberWithInt:totalBytesExpectedToWrite] forKey:@"total"]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:uploadProgress]; + [result setKeepCallbackAsBool:true]; + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + self.bytesTransfered = totalBytesWritten; +} + +// for self signed certificates +- (void)connection:(NSURLConnection*)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge +{ + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + if (self.trustAllHosts) { + NSURLCredential* credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; + [challenge.sender useCredential:credential forAuthenticationChallenge:challenge]; + } + [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge]; + } else { + [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge]; + } +} + +- (id)init +{ + if ((self = [super init])) { + self.responseData = [NSMutableData data]; + self.targetFileHandle = nil; + } + return self; +} + +@end; diff --git a/cordova/ios/CordovaLib/Classes/CDVGlobalization.h b/cordova/ios/CordovaLib/Classes/CDVGlobalization.h new file mode 100755 index 000000000..038465630 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVGlobalization.h @@ -0,0 +1,150 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +#define CDV_FORMAT_SHORT 0 +#define CDV_FORMAT_MEDIUM 1 +#define CDV_FORMAT_LONG 2 +#define CDV_FORMAT_FULL 3 +#define CDV_SELECTOR_MONTHS 0 +#define CDV_SELECTOR_DAYS 1 + +enum CDVGlobalizationError { + CDV_UNKNOWN_ERROR = 0, + CDV_FORMATTING_ERROR = 1, + CDV_PARSING_ERROR = 2, + CDV_PATTERN_ERROR = 3, +}; +typedef NSUInteger CDVGlobalizationError; + +@interface CDVGlobalization : CDVPlugin { + CFLocaleRef currentLocale; +} + +- (void)getPreferredLanguage:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns the string identifier for the clients current locale setting. + * It returns the locale identifier string to the successCB callback with a + * properties object as a parameter. If there is an error getting the locale, + * then the errorCB callback is invoked. + */ +- (void)getLocaleName:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns a date formatted as a string according to the clients user preferences and + * calendar using the time zone of the client. It returns the formatted date string to the + * successCB callback with a properties object as a parameter. If there is an error + * formatting the date, then the errorCB callback is invoked. + * + * options: "date" contains the number of milliseconds that represents the JavaScript date + */ +- (void)dateToString:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Parses a date formatted as a string according to the clients user + * preferences and calendar using the time zone of the client and returns + * the corresponding date object. It returns the date to the successCB + * callback with a properties object as a parameter. If there is an error + * parsing the date string, then the errorCB callback is invoked. + * + * options: "dateString" contains the JavaScript string to parse for a date + */ +- (void)stringToDate:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns a pattern string for formatting and parsing dates according to the clients + * user preferences. It returns the pattern to the successCB callback with a + * properties object as a parameter. If there is an error obtaining the pattern, + * then the errorCB callback is invoked. + * + */ +- (void)getDatePattern:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns an array of either the names of the months or days of the week + * according to the clients user preferences and calendar. It returns the array of names to the + * successCB callback with a properties object as a parameter. If there is an error obtaining the + * names, then the errorCB callback is invoked. + * + */ +- (void)getDateNames:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns whether daylight savings time is in effect for a given date using the clients + * time zone and calendar. It returns whether or not daylight savings time is in effect + * to the successCB callback with a properties object as a parameter. If there is an error + * reading the date, then the errorCB callback is invoked. + * + * options: "date" contains the number of milliseconds that represents the JavaScript date + * + */ +- (void)isDayLightSavingsTime:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns the first day of the week according to the clients user preferences and calendar. + * The days of the week are numbered starting from 1 where 1 is considered to be Sunday. + * It returns the day to the successCB callback with a properties object as a parameter. + * If there is an error obtaining the pattern, then the errorCB callback is invoked. + * + */ +- (void)getFirstDayOfWeek:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns a number formatted as a string according to the clients user preferences. + * It returns the formatted number string to the successCB callback with a properties object as a + * parameter. If there is an error formatting the number, then the errorCB callback is invoked. + * + * options: "number" contains the JavaScript number to format + * + */ +- (void)numberToString:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Parses a number formatted as a string according to the clients user preferences and + * returns the corresponding number. It returns the number to the successCB callback with a + * properties object as a parameter. If there is an error parsing the number string, then + * the errorCB callback is invoked. + * + * options: "numberString" contains the JavaScript string to parse for a number + * + */ +- (void)stringToNumber:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns a pattern string for formatting and parsing numbers according to the clients user + * preferences. It returns the pattern to the successCB callback with a properties object as a + * parameter. If there is an error obtaining the pattern, then the errorCB callback is invoked. + * + */ +- (void)getNumberPattern:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +/** + * Returns a pattern string for formatting and parsing currency values according to the clients + * user preferences and ISO 4217 currency code. It returns the pattern to the successCB callback with a + * properties object as a parameter. If there is an error obtaining the pattern, then the errorCB + * callback is invoked. + * + * options: "currencyCode" contains the ISO currency code from JavaScript + */ +- (void)getCurrencyPattern:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVGlobalization.m b/cordova/ios/CordovaLib/Classes/CDVGlobalization.m new file mode 100755 index 000000000..9eb972105 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVGlobalization.m @@ -0,0 +1,790 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVGlobalization.h" + +@implementation CDVGlobalization + +- (id)initWithWebView:(UIWebView*)theWebView +{ + self = (CDVGlobalization*)[super initWithWebView:theWebView]; + if (self) { + currentLocale = CFLocaleCopyCurrent(); + } + return self; +} + +- (void)getPreferredLanguage:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + NSString* callbackId = [arguments objectAtIndex:0]; + CDVPluginResult* result = nil; + + NSLog(@"log1"); + // Source: http://stackoverflow.com/questions/3910244/getting-current-device-language-in-ios + // (should be OK) + NSString* language = [[NSLocale preferredLanguages] objectAtIndex:0]; + + if (language) { + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:language forKey:@"value"]; + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:dictionary]; + } else { + // TBD is this ever expected to happen? + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_UNKNOWN_ERROR] forKey:@"code"]; + [dictionary setValue:@"Unknown error" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)getLocaleName:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CDVPluginResult* result = nil; + NSString* callbackId = [arguments objectAtIndex:0]; + NSDictionary* dictionary = nil; + + NSLocale* locale = [NSLocale currentLocale]; + + if (locale) { + dictionary = [NSDictionary dictionaryWithObject:[locale localeIdentifier] forKey:@"value"]; + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } else { + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_UNKNOWN_ERROR] forKey:@"code"]; + [dictionary setValue:@"Unknown error" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)dateToString:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CFDateFormatterStyle style = kCFDateFormatterShortStyle; + CFDateFormatterStyle dateStyle = kCFDateFormatterShortStyle; + CFDateFormatterStyle timeStyle = kCFDateFormatterShortStyle; + NSDate* date = nil; + NSString* dateString = nil; + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + + id milliseconds = [options valueForKey:@"date"]; + + if (milliseconds && [milliseconds isKindOfClass:[NSNumber class]]) { + // get the number of seconds since 1970 and create the date object + date = [NSDate dateWithTimeIntervalSince1970:[milliseconds doubleValue] / 1000]; + } + + // see if any options have been specified + id items = [options valueForKey:@"options"]; + if (items && [items isKindOfClass:[NSMutableDictionary class]]) { + NSEnumerator* enumerator = [items keyEnumerator]; + id key; + + // iterate through all the options + while ((key = [enumerator nextObject])) { + id item = [items valueForKey:key]; + + // make sure that only string values are present + if ([item isKindOfClass:[NSString class]]) { + // get the desired format length + if ([key isEqualToString:@"formatLength"]) { + if ([item isEqualToString:@"short"]) { + style = kCFDateFormatterShortStyle; + } else if ([item isEqualToString:@"medium"]) { + style = kCFDateFormatterMediumStyle; + } else if ([item isEqualToString:@"long"]) { + style = kCFDateFormatterLongStyle; + } else if ([item isEqualToString:@"full"]) { + style = kCFDateFormatterFullStyle; + } + } + // get the type of date and time to generate + else if ([key isEqualToString:@"selector"]) { + if ([item isEqualToString:@"date"]) { + dateStyle = style; + timeStyle = kCFDateFormatterNoStyle; + } else if ([item isEqualToString:@"time"]) { + dateStyle = kCFDateFormatterNoStyle; + timeStyle = style; + } else if ([item isEqualToString:@"date and time"]) { + dateStyle = style; + timeStyle = style; + } + } + } + } + } + + // create the formatter using the user's current default locale and formats for dates and times + CFDateFormatterRef formatter = CFDateFormatterCreate(kCFAllocatorDefault, + currentLocale, + dateStyle, + timeStyle); + // if we have a valid date object then call the formatter + if (date) { + dateString = (__bridge_transfer NSString*)CFDateFormatterCreateStringWithDate(kCFAllocatorDefault, + formatter, + (__bridge CFDateRef)date); + } + + // if the date was converted to a string successfully then return the result + if (dateString) { + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:dateString forKey:@"value"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + // error + else { + // DLog(@"GlobalizationCommand dateToString unable to format %@", [date description]); + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_FORMATTING_ERROR] forKey:@"code"]; + [dictionary setValue:@"Formatting error" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; + + CFRelease(formatter); +} + +- (void)stringToDate:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CFDateFormatterStyle style = kCFDateFormatterShortStyle; + CFDateFormatterStyle dateStyle = kCFDateFormatterShortStyle; + CFDateFormatterStyle timeStyle = kCFDateFormatterShortStyle; + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + NSString* dateString = nil; + NSDateComponents* comps = nil; + + // get the string that is to be parsed for a date + id ms = [options valueForKey:@"dateString"]; + + if (ms && [ms isKindOfClass:[NSString class]]) { + dateString = ms; + } + + // see if any options have been specified + id items = [options valueForKey:@"options"]; + if (items && [items isKindOfClass:[NSMutableDictionary class]]) { + NSEnumerator* enumerator = [items keyEnumerator]; + id key; + + // iterate through all the options + while ((key = [enumerator nextObject])) { + id item = [items valueForKey:key]; + + // make sure that only string values are present + if ([item isKindOfClass:[NSString class]]) { + // get the desired format length + if ([key isEqualToString:@"formatLength"]) { + if ([item isEqualToString:@"short"]) { + style = kCFDateFormatterShortStyle; + } else if ([item isEqualToString:@"medium"]) { + style = kCFDateFormatterMediumStyle; + } else if ([item isEqualToString:@"long"]) { + style = kCFDateFormatterLongStyle; + } else if ([item isEqualToString:@"full"]) { + style = kCFDateFormatterFullStyle; + } + } + // get the type of date and time to generate + else if ([key isEqualToString:@"selector"]) { + if ([item isEqualToString:@"date"]) { + dateStyle = style; + timeStyle = kCFDateFormatterNoStyle; + } else if ([item isEqualToString:@"time"]) { + dateStyle = kCFDateFormatterNoStyle; + timeStyle = style; + } else if ([item isEqualToString:@"date and time"]) { + dateStyle = style; + timeStyle = style; + } + } + } + } + } + + // get the user's default settings for date and time formats + CFDateFormatterRef formatter = CFDateFormatterCreate(kCFAllocatorDefault, + currentLocale, + dateStyle, + timeStyle); + + // set the parsing to be more lenient + CFDateFormatterSetProperty(formatter, kCFDateFormatterIsLenient, kCFBooleanTrue); + + // parse tha date and time string + CFDateRef date = CFDateFormatterCreateDateFromString(kCFAllocatorDefault, + formatter, + (__bridge CFStringRef)dateString, + NULL); + + // if we were able to parse the date then get the date and time components + if (date != NULL) { + NSCalendar* calendar = [NSCalendar currentCalendar]; + + unsigned unitFlags = NSYearCalendarUnit | + NSMonthCalendarUnit | + NSDayCalendarUnit | + NSHourCalendarUnit | + NSMinuteCalendarUnit | + NSSecondCalendarUnit; + + comps = [calendar components:unitFlags fromDate:(__bridge NSDate*)date]; + CFRelease(date); + } + + // put the various elements of the date and time into a dictionary + if (comps != nil) { + NSArray* keys = [NSArray arrayWithObjects:@"year", @"month", @"day", @"hour", @"minute", @"second", @"millisecond", nil]; + NSArray* values = [NSArray arrayWithObjects:[NSNumber numberWithInt:[comps year]], + [NSNumber numberWithInt:[comps month] - 1], + [NSNumber numberWithInt:[comps day]], + [NSNumber numberWithInt:[comps hour]], + [NSNumber numberWithInt:[comps minute]], + [NSNumber numberWithInt:[comps second]], + [NSNumber numberWithInt:0], /* iOS does not provide milliseconds */ + nil]; + + NSDictionary* dictionary = [NSDictionary dictionaryWithObjects:values forKeys:keys]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + // error + else { + // Dlog(@"GlobalizationCommand stringToDate unable to parse %@", dateString); + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_PARSING_ERROR] forKey:@"code"]; + [dictionary setValue:@"unable to parse" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; + + CFRelease(formatter); +} + +- (void)getDatePattern:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CFDateFormatterStyle style = kCFDateFormatterShortStyle; + CFDateFormatterStyle dateStyle = kCFDateFormatterShortStyle; + CFDateFormatterStyle timeStyle = kCFDateFormatterShortStyle; + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + + // see if any options have been specified + id items = [options valueForKey:@"options"]; + + if (items && [items isKindOfClass:[NSMutableDictionary class]]) { + NSEnumerator* enumerator = [items keyEnumerator]; + id key; + + // iterate through all the options + while ((key = [enumerator nextObject])) { + id item = [items valueForKey:key]; + + // make sure that only string values are present + if ([item isKindOfClass:[NSString class]]) { + // get the desired format length + if ([key isEqualToString:@"formatLength"]) { + if ([item isEqualToString:@"short"]) { + style = kCFDateFormatterShortStyle; + } else if ([item isEqualToString:@"medium"]) { + style = kCFDateFormatterMediumStyle; + } else if ([item isEqualToString:@"long"]) { + style = kCFDateFormatterLongStyle; + } else if ([item isEqualToString:@"full"]) { + style = kCFDateFormatterFullStyle; + } + } + // get the type of date and time to generate + else if ([key isEqualToString:@"selector"]) { + if ([item isEqualToString:@"date"]) { + dateStyle = style; + timeStyle = kCFDateFormatterNoStyle; + } else if ([item isEqualToString:@"time"]) { + dateStyle = kCFDateFormatterNoStyle; + timeStyle = style; + } else if ([item isEqualToString:@"date and time"]) { + dateStyle = style; + timeStyle = style; + } + } + } + } + } + + // get the user's default settings for date and time formats + CFDateFormatterRef formatter = CFDateFormatterCreate(kCFAllocatorDefault, + currentLocale, + dateStyle, + timeStyle); + + // get the date pattern to apply when formatting and parsing + CFStringRef datePattern = CFDateFormatterGetFormat(formatter); + // get the user's current time zone information + CFTimeZoneRef timezone = (CFTimeZoneRef)CFDateFormatterCopyProperty(formatter, kCFDateFormatterTimeZone); + + // put the pattern and time zone information into the dictionary + if ((datePattern != nil) && (timezone != nil)) { + NSArray* keys = [NSArray arrayWithObjects:@"pattern", @"timezone", @"utc_offset", @"dst_offset", nil]; + NSArray* values = [NSArray arrayWithObjects:((__bridge NSString*)datePattern), + [((__bridge NSTimeZone*)timezone)abbreviation], + [NSNumber numberWithLong:[((__bridge NSTimeZone*)timezone)secondsFromGMT]], + [NSNumber numberWithDouble:[((__bridge NSTimeZone*)timezone)daylightSavingTimeOffset]], + nil]; + + NSDictionary* dictionary = [NSDictionary dictionaryWithObjects:values forKeys:keys]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + // error + else { + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_PATTERN_ERROR] forKey:@"code"]; + [dictionary setValue:@"Pattern error" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; + + if (timezone) { + CFRelease(timezone); + } + CFRelease(formatter); +} + +- (void)getDateNames:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + int style = CDV_FORMAT_LONG; + int selector = CDV_SELECTOR_MONTHS; + CFStringRef dataStyle = kCFDateFormatterMonthSymbols; + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + + // see if any options have been specified + id items = [options valueForKey:@"options"]; + + if (items && [items isKindOfClass:[NSMutableDictionary class]]) { + NSEnumerator* enumerator = [items keyEnumerator]; + id key; + + // iterate through all the options + while ((key = [enumerator nextObject])) { + id item = [items valueForKey:key]; + + // make sure that only string values are present + if ([item isKindOfClass:[NSString class]]) { + // get the desired type of name + if ([key isEqualToString:@"type"]) { + if ([item isEqualToString:@"narrow"]) { + style = CDV_FORMAT_SHORT; + } else if ([item isEqualToString:@"wide"]) { + style = CDV_FORMAT_LONG; + } + } + // determine if months or days are needed + else if ([key isEqualToString:@"item"]) { + if ([item isEqualToString:@"months"]) { + selector = CDV_SELECTOR_MONTHS; + } else if ([item isEqualToString:@"days"]) { + selector = CDV_SELECTOR_DAYS; + } + } + } + } + } + + CFDateFormatterRef formatter = CFDateFormatterCreate(kCFAllocatorDefault, + currentLocale, + kCFDateFormatterFullStyle, + kCFDateFormatterFullStyle); + + if ((selector == CDV_SELECTOR_MONTHS) && (style == CDV_FORMAT_LONG)) { + dataStyle = kCFDateFormatterMonthSymbols; + } else if ((selector == CDV_SELECTOR_MONTHS) && (style == CDV_FORMAT_SHORT)) { + dataStyle = kCFDateFormatterShortMonthSymbols; + } else if ((selector == CDV_SELECTOR_DAYS) && (style == CDV_FORMAT_LONG)) { + dataStyle = kCFDateFormatterWeekdaySymbols; + } else if ((selector == CDV_SELECTOR_DAYS) && (style == CDV_FORMAT_SHORT)) { + dataStyle = kCFDateFormatterShortWeekdaySymbols; + } + + CFArrayRef names = (CFArrayRef)CFDateFormatterCopyProperty(formatter, dataStyle); + + if (names) { + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:((__bridge NSArray*)names) forKey:@"value"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + CFRelease(names); + } + // error + else { + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_UNKNOWN_ERROR] forKey:@"code"]; + [dictionary setValue:@"Unknown error" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; + + CFRelease(formatter); +} + +- (void)isDayLightSavingsTime:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + NSDate* date = nil; + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + + id milliseconds = [options valueForKey:@"date"]; + + if (milliseconds && [milliseconds isKindOfClass:[NSNumber class]]) { + // get the number of seconds since 1970 and create the date object + date = [NSDate dateWithTimeIntervalSince1970:[milliseconds doubleValue] / 1000]; + } + + if (date) { + // get the current calendar for the user and check if the date is using DST + NSCalendar* calendar = [NSCalendar currentCalendar]; + NSTimeZone* timezone = [calendar timeZone]; + NSNumber* dst = [NSNumber numberWithBool:[timezone isDaylightSavingTimeForDate:date]]; + + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:dst forKey:@"dst"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + // error + else { + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_UNKNOWN_ERROR] forKey:@"code"]; + [dictionary setValue:@"Unknown error" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; +} + +- (void)getFirstDayOfWeek:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + + NSCalendar* calendar = [NSCalendar autoupdatingCurrentCalendar]; + + NSNumber* day = [NSNumber numberWithInt:[calendar firstWeekday]]; + + if (day) { + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:day forKey:@"value"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + // error + else { + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_UNKNOWN_ERROR] forKey:@"code"]; + [dictionary setValue:@"Unknown error" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; +} + +- (void)numberToString:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + CFNumberFormatterStyle style = kCFNumberFormatterDecimalStyle; + NSNumber* number = nil; + + id value = [options valueForKey:@"number"]; + + if (value && [value isKindOfClass:[NSNumber class]]) { + number = (NSNumber*)value; + } + + // see if any options have been specified + id items = [options valueForKey:@"options"]; + if (items && [items isKindOfClass:[NSMutableDictionary class]]) { + NSEnumerator* enumerator = [items keyEnumerator]; + id key; + + // iterate through all the options + while ((key = [enumerator nextObject])) { + id item = [items valueForKey:key]; + + // make sure that only string values are present + if ([item isKindOfClass:[NSString class]]) { + // get the desired style of formatting + if ([key isEqualToString:@"type"]) { + if ([item isEqualToString:@"percent"]) { + style = kCFNumberFormatterPercentStyle; + } else if ([item isEqualToString:@"currency"]) { + style = kCFNumberFormatterCurrencyStyle; + } else if ([item isEqualToString:@"decimal"]) { + style = kCFNumberFormatterDecimalStyle; + } + } + } + } + } + + CFNumberFormatterRef formatter = CFNumberFormatterCreate(kCFAllocatorDefault, + currentLocale, + style); + + // get the localized string based upon the locale and user preferences + NSString* numberString = (__bridge_transfer NSString*)CFNumberFormatterCreateStringWithNumber(kCFAllocatorDefault, + formatter, + (__bridge CFNumberRef)number); + + if (numberString) { + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:numberString forKey:@"value"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + // error + else { + // DLog(@"GlobalizationCommand numberToString unable to format %@", [number stringValue]); + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_FORMATTING_ERROR] forKey:@"code"]; + [dictionary setValue:@"Unable to format" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; + + CFRelease(formatter); +} + +- (void)stringToNumber:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + CFNumberFormatterStyle style = kCFNumberFormatterDecimalStyle; + NSString* numberString = nil; + double doubleValue; + + id value = [options valueForKey:@"numberString"]; + + if (value && [value isKindOfClass:[NSString class]]) { + numberString = (NSString*)value; + } + + // see if any options have been specified + id items = [options valueForKey:@"options"]; + if (items && [items isKindOfClass:[NSMutableDictionary class]]) { + NSEnumerator* enumerator = [items keyEnumerator]; + id key; + + // iterate through all the options + while ((key = [enumerator nextObject])) { + id item = [items valueForKey:key]; + + // make sure that only string values are present + if ([item isKindOfClass:[NSString class]]) { + // get the desired style of formatting + if ([key isEqualToString:@"type"]) { + if ([item isEqualToString:@"percent"]) { + style = kCFNumberFormatterPercentStyle; + } else if ([item isEqualToString:@"currency"]) { + style = kCFNumberFormatterCurrencyStyle; + } else if ([item isEqualToString:@"decimal"]) { + style = kCFNumberFormatterDecimalStyle; + } + } + } + } + } + + CFNumberFormatterRef formatter = CFNumberFormatterCreate(kCFAllocatorDefault, + currentLocale, + style); + + // we need to make this lenient so as to avoid problems with parsing currencies that have non-breaking space characters + if (style == kCFNumberFormatterCurrencyStyle) { + CFNumberFormatterSetProperty(formatter, kCFNumberFormatterIsLenient, kCFBooleanTrue); + } + + // parse againist the largest type to avoid data loss + Boolean rc = CFNumberFormatterGetValueFromString(formatter, + (__bridge CFStringRef)numberString, + NULL, + kCFNumberDoubleType, + &doubleValue); + + if (rc) { + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:[NSNumber numberWithDouble:doubleValue] forKey:@"value"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + // error + else { + // DLog(@"GlobalizationCommand stringToNumber unable to parse %@", numberString); + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_PARSING_ERROR] forKey:@"code"]; + [dictionary setValue:@"Unable to parse" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; + + CFRelease(formatter); +} + +- (void)getNumberPattern:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + CFNumberFormatterStyle style = kCFNumberFormatterDecimalStyle; + CFStringRef symbolType = NULL; + NSString* symbol = @""; + + // see if any options have been specified + id items = [options valueForKey:@"options"]; + + if (items && [items isKindOfClass:[NSMutableDictionary class]]) { + NSEnumerator* enumerator = [items keyEnumerator]; + id key; + + // iterate through all the options + while ((key = [enumerator nextObject])) { + id item = [items valueForKey:key]; + + // make sure that only string values are present + if ([item isKindOfClass:[NSString class]]) { + // get the desired style of formatting + if ([key isEqualToString:@"type"]) { + if ([item isEqualToString:@"percent"]) { + style = kCFNumberFormatterPercentStyle; + } else if ([item isEqualToString:@"currency"]) { + style = kCFNumberFormatterCurrencyStyle; + } else if ([item isEqualToString:@"decimal"]) { + style = kCFNumberFormatterDecimalStyle; + } + } + } + } + } + + CFNumberFormatterRef formatter = CFNumberFormatterCreate(kCFAllocatorDefault, + currentLocale, + style); + + NSString* numberPattern = (__bridge NSString*)CFNumberFormatterGetFormat(formatter); + + if (style == kCFNumberFormatterCurrencyStyle) { + symbolType = kCFNumberFormatterCurrencySymbol; + } else if (style == kCFNumberFormatterPercentStyle) { + symbolType = kCFNumberFormatterPercentSymbol; + } + + if (symbolType) { + symbol = (__bridge_transfer NSString*)CFNumberFormatterCopyProperty(formatter, symbolType); + } + + NSString* decimal = (__bridge_transfer NSString*)CFNumberFormatterCopyProperty(formatter, kCFNumberFormatterDecimalSeparator); + NSString* grouping = (__bridge_transfer NSString*)CFNumberFormatterCopyProperty(formatter, kCFNumberFormatterGroupingSeparator); + NSString* posSign = (__bridge_transfer NSString*)CFNumberFormatterCopyProperty(formatter, kCFNumberFormatterPlusSign); + NSString* negSign = (__bridge_transfer NSString*)CFNumberFormatterCopyProperty(formatter, kCFNumberFormatterMinusSign); + NSNumber* fracDigits = (__bridge_transfer NSNumber*)CFNumberFormatterCopyProperty(formatter, kCFNumberFormatterMinFractionDigits); + NSNumber* roundingDigits = (__bridge_transfer NSNumber*)CFNumberFormatterCopyProperty(formatter, kCFNumberFormatterRoundingIncrement); + + // put the pattern information into the dictionary + if (numberPattern != nil) { + NSArray* keys = [NSArray arrayWithObjects:@"pattern", @"symbol", @"fraction", @"rounding", + @"positive", @"negative", @"decimal", @"grouping", nil]; + NSArray* values = [NSArray arrayWithObjects:numberPattern, symbol, fracDigits, roundingDigits, + posSign, negSign, decimal, grouping, nil]; + NSDictionary* dictionary = [NSDictionary dictionaryWithObjects:values forKeys:keys]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + } + // error + else { + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_PATTERN_ERROR] forKey:@"code"]; + [dictionary setValue:@"Pattern error" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; + + CFRelease(formatter); +} + +- (void)getCurrencyPattern:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options +{ + CDVPluginResult* result = nil; + NSString* callBackId = [arguments objectAtIndex:0]; + NSString* currencyCode = nil; + NSString* numberPattern = nil; + NSString* decimal = nil; + NSString* grouping = nil; + int32_t defaultFractionDigits; + double roundingIncrement; + Boolean rc; + + id value = [options valueForKey:@"currencyCode"]; + + if (value && [value isKindOfClass:[NSString class]]) { + currencyCode = (NSString*)value; + } + + // first see if there is base currency info available and fill in the currency_info structure + rc = CFNumberFormatterGetDecimalInfoForCurrencyCode((__bridge CFStringRef)currencyCode, &defaultFractionDigits, &roundingIncrement); + + // now set the currency code in the formatter + if (rc) { + CFNumberFormatterRef formatter = CFNumberFormatterCreate(kCFAllocatorDefault, + currentLocale, + kCFNumberFormatterCurrencyStyle); + + CFNumberFormatterSetProperty(formatter, kCFNumberFormatterCurrencyCode, (__bridge CFStringRef)currencyCode); + CFNumberFormatterSetProperty(formatter, kCFNumberFormatterInternationalCurrencySymbol, (__bridge CFStringRef)currencyCode); + + numberPattern = (__bridge NSString*)CFNumberFormatterGetFormat(formatter); + decimal = (__bridge_transfer NSString*)CFNumberFormatterCopyProperty(formatter, kCFNumberFormatterCurrencyDecimalSeparator); + grouping = (__bridge_transfer NSString*)CFNumberFormatterCopyProperty(formatter, kCFNumberFormatterCurrencyGroupingSeparator); + + NSArray* keys = [NSArray arrayWithObjects:@"pattern", @"code", @"fraction", @"rounding", + @"decimal", @"grouping", nil]; + NSArray* values = [NSArray arrayWithObjects:numberPattern, currencyCode, [NSNumber numberWithInt:defaultFractionDigits], + [NSNumber numberWithDouble:roundingIncrement], decimal, grouping, nil]; + NSDictionary* dictionary = [NSDictionary dictionaryWithObjects:values forKeys:keys]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + CFRelease(formatter); + } + // error + else { + // DLog(@"GlobalizationCommand getCurrencyPattern unable to get pattern for %@", currencyCode); + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + [dictionary setValue:[NSNumber numberWithInt:CDV_PATTERN_ERROR] forKey:@"code"]; + [dictionary setValue:@"Unable to get pattern" forKey:@"message"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + } + + [self.commandDelegate sendPluginResult:result callbackId:callBackId]; +} + +- (void)dealloc +{ + if (currentLocale) { + CFRelease(currentLocale); + currentLocale = nil; + } +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVInAppBrowser.h b/cordova/ios/CordovaLib/Classes/CDVInAppBrowser.h new file mode 100755 index 000000000..765326a49 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVInAppBrowser.h @@ -0,0 +1,82 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" +#import "CDVInvokedUrlCommand.h" +#import "CDVScreenOrientationDelegate.h" +#import "CDVWebViewDelegate.h" + +@class CDVInAppBrowserViewController; + +@interface CDVInAppBrowser : CDVPlugin { + BOOL _injectedIframeBridge; +} + +@property (nonatomic, retain) CDVInAppBrowserViewController* inAppBrowserViewController; +@property (nonatomic, copy) NSString* callbackId; + +- (void)open:(CDVInvokedUrlCommand*)command; +- (void)close:(CDVInvokedUrlCommand*)command; +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command; + +@end + +@interface CDVInAppBrowserViewController : UIViewController { + @private + NSString* _userAgent; + NSString* _prevUserAgent; + NSInteger _userAgentLockToken; + CDVWebViewDelegate* _webViewDelegate; +} + +@property (nonatomic, strong) IBOutlet UIWebView* webView; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; +@property (nonatomic, strong) IBOutlet UILabel* addressLabel; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; +@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; +@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; + +@property (nonatomic, weak) id orientationDelegate; +@property (nonatomic, weak) CDVInAppBrowser* navigationDelegate; +@property (nonatomic) NSURL* currentURL; + +- (void)close; +- (void)navigateTo:(NSURL*)url; +- (void)showLocationBar:(BOOL)show; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent; + +@end + +@interface CDVInAppBrowserOptions : NSObject {} + +@property (nonatomic, assign) BOOL location; +@property (nonatomic, copy) NSString* presentationstyle; +@property (nonatomic, copy) NSString* transitionstyle; + +@property (nonatomic, assign) BOOL enableviewportscale; +@property (nonatomic, assign) BOOL mediaplaybackrequiresuseraction; +@property (nonatomic, assign) BOOL allowinlinemediaplayback; +@property (nonatomic, assign) BOOL keyboarddisplayrequiresuseraction; +@property (nonatomic, assign) BOOL suppressesincrementalrendering; + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVInAppBrowser.m b/cordova/ios/CordovaLib/Classes/CDVInAppBrowser.m new file mode 100755 index 000000000..b03d1feea --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVInAppBrowser.m @@ -0,0 +1,705 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInAppBrowser.h" +#import "CDVPluginResult.h" +#import "CDVUserAgentUtil.h" +#import "CDVJSON.h" + +#define kInAppBrowserTargetSelf @"_self" +#define kInAppBrowserTargetSystem @"_system" +#define kInAppBrowserTargetBlank @"_blank" + +#define TOOLBAR_HEIGHT 44.0 +#define LOCATIONBAR_HEIGHT 21.0 +#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) + +#pragma mark CDVInAppBrowser + +@implementation CDVInAppBrowser + +- (CDVInAppBrowser*)initWithWebView:(UIWebView*)theWebView +{ + self = [super initWithWebView:theWebView]; + if (self != nil) { + // your initialization here + } + + return self; +} + +- (void)onReset +{ + [self close:nil]; +} + +- (void)close:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController != nil) { + [self.inAppBrowserViewController close]; + self.inAppBrowserViewController = nil; + } + + self.callbackId = nil; +} + +- (void)open:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult* pluginResult; + + NSString* url = [command argumentAtIndex:0]; + NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; + NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; + + self.callbackId = command.callbackId; + + if (url != nil) { + NSURL* baseUrl = [self.webView.request URL]; + NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; + if ([target isEqualToString:kInAppBrowserTargetSelf]) { + [self openInCordovaWebView:absoluteUrl withOptions:options]; + } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { + [self openInSystem:absoluteUrl]; + } else { // _blank or anything else + [self openInInAppBrowser:absoluteUrl withOptions:options]; + } + + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; + } + + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options +{ + if (self.inAppBrowserViewController == nil) { + NSString* originalUA = [CDVUserAgentUtil originalUserAgent]; + self.inAppBrowserViewController = [[CDVInAppBrowserViewController alloc] initWithUserAgent:originalUA prevUserAgent:[self.commandDelegate userAgent]]; + self.inAppBrowserViewController.navigationDelegate = self; + + if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { + self.inAppBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; + } + } + + CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; + [self.inAppBrowserViewController showLocationBar:browserOptions.location]; + + // Set Presentation Style + UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default + if (browserOptions.presentationstyle != nil) { + if ([browserOptions.presentationstyle isEqualToString:@"pagesheet"]) { + presentationStyle = UIModalPresentationPageSheet; + } else if ([browserOptions.presentationstyle isEqualToString:@"formsheet"]) { + presentationStyle = UIModalPresentationFormSheet; + } + } + self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; + + // Set Transition Style + UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default + if (browserOptions.transitionstyle != nil) { + if ([browserOptions.transitionstyle isEqualToString:@"fliphorizontal"]) { + transitionStyle = UIModalTransitionStyleFlipHorizontal; + } else if ([browserOptions.transitionstyle isEqualToString:@"crossdissolve"]) { + transitionStyle = UIModalTransitionStyleCrossDissolve; + } + } + self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; + + // UIWebView options + self.inAppBrowserViewController.webView.scalesPageToFit = browserOptions.enableviewportscale; + self.inAppBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction; + self.inAppBrowserViewController.webView.allowsInlineMediaPlayback = browserOptions.allowinlinemediaplayback; + if (IsAtLeastiOSVersion(@"6.0")) { + self.inAppBrowserViewController.webView.keyboardDisplayRequiresUserAction = browserOptions.keyboarddisplayrequiresuseraction; + self.inAppBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering; + } + + if (self.viewController.modalViewController != self.inAppBrowserViewController) { + [self.viewController presentModalViewController:self.inAppBrowserViewController animated:YES]; + } + [self.inAppBrowserViewController navigateTo:url]; +} + +- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options +{ + if ([self.commandDelegate URLIsWhitelisted:url]) { + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + [self.webView loadRequest:request]; + } else { // this assumes the InAppBrowser can be excepted from the white-list + [self openInInAppBrowser:url withOptions:options]; + } +} + +- (void)openInSystem:(NSURL*)url +{ + if ([[UIApplication sharedApplication] canOpenURL:url]) { + [[UIApplication sharedApplication] openURL:url]; + } else { // handle any custom schemes to plugins + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + } +} + +// This is a helper method for the inject{Script|Style}{Code|File} API calls, which +// provides a consistent method for injecting JavaScript code into the document. +// +// If a wrapper string is supplied, then the source string will be JSON-encoded (adding +// quotes) and wrapped using string formatting. (The wrapper string should have a single +// '%@' marker). +// +// If no wrapper is supplied, then the source string is executed directly. + +- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper +{ + if (!_injectedIframeBridge) { + _injectedIframeBridge = YES; + // Create an iframe bridge in the new document to communicate with the CDVInAppBrowserViewController + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){var e = _cdvIframeBridge = d.createElement('iframe');e.style.display='none';d.body.appendChild(e);})(document)"]; + } + + if (jsWrapper != nil) { + NSString* sourceArrayString = [@[source] JSONString]; + if (sourceArrayString) { + NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; + NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:jsToInject]; + } + } else { + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:source]; + } +} + +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper = nil; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"_cdvIframeBridge.src='gap-iab://%@/'+window.escape(JSON.stringify([eval(%%@)]));", command.callbackId]; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectScriptFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +/** + * The iframe bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging + * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no + * other code execution is possible. + * + * To trigger the bridge, the iframe (or any other resource) should attempt to load a url of the form: + * + * gap-iab:/// + * + * where is the string id of the callback to trigger (something like "InAppBrowser0123456789") + * + * If present, the path component of the special gap-iab:// url is expected to be a URL-escaped JSON-encoded + * value to pass to the callback. [NSURL path] should take care of the URL-unescaping, and a JSON_EXCEPTION + * is returned if the JSON is invalid. + */ +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + NSURL* url = request.URL; + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, + // and the path, if present, should be a JSON-encoded value to pass to the callback. + if ([[url scheme] isEqualToString:@"gap-iab"]) { + NSString* scriptCallbackId = [url host]; + CDVPluginResult* pluginResult = nil; + + if ([scriptCallbackId hasPrefix:@"InAppBrowser"]) { + NSString* scriptResult = [url path]; + NSError* __autoreleasing error = nil; + + // The message should be a JSON-encoded array of the result of the script which executed. + if ((scriptResult != nil) && ([scriptResult length] > 1)) { + scriptResult = [scriptResult substringFromIndex:1]; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + } + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; + } + [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; + return NO; + } + } else if ((self.callbackId != nil) && isTopLevelNavigation) { + // Send a loadstart event for each top-level navigation (includes redirects). + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + + return YES; +} + +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ + _injectedIframeBridge = NO; +} + +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + if (self.callbackId != nil) { + // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected). + NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + if (self.callbackId != nil) { + NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInt:error.code], @"message": error.localizedDescription}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)browserExit +{ + if (self.callbackId != nil) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"exit"}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + // Don't recycle the ViewController since it may be consuming a lot of memory. + // Also - this is required for the PDF/User-Agent bug work-around. + self.inAppBrowserViewController = nil; +} + +@end + +#pragma mark CDVInAppBrowserViewController + +@implementation CDVInAppBrowserViewController + +@synthesize currentURL; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent +{ + self = [super init]; + if (self != nil) { + _userAgent = userAgent; + _prevUserAgent = prevUserAgent; + _webViewDelegate = [[CDVWebViewDelegate alloc] initWithDelegate:self]; + [self createViews]; + } + + return self; +} + +- (void)createViews +{ + // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included + + CGRect webViewBounds = self.view.bounds; + + webViewBounds.size.height -= FOOTER_HEIGHT; + + self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; + self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); + + [self.view addSubview:self.webView]; + [self.view sendSubviewToBack:self.webView]; + + self.webView.delegate = _webViewDelegate; + self.webView.backgroundColor = [UIColor whiteColor]; + + self.webView.clearsContextBeforeDrawing = YES; + self.webView.clipsToBounds = YES; + self.webView.contentMode = UIViewContentModeScaleToFill; + self.webView.contentStretch = CGRectFromString(@"{{0, 0}, {1, 1}}"); + self.webView.multipleTouchEnabled = YES; + self.webView.opaque = YES; + self.webView.scalesPageToFit = NO; + self.webView.userInteractionEnabled = YES; + + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; + self.spinner.alpha = 1.000; + self.spinner.autoresizesSubviews = YES; + self.spinner.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin; + self.spinner.clearsContextBeforeDrawing = NO; + self.spinner.clipsToBounds = NO; + self.spinner.contentMode = UIViewContentModeScaleToFill; + self.spinner.contentStretch = CGRectFromString(@"{{0, 0}, {1, 1}}"); + self.spinner.frame = CGRectMake(454.0, 231.0, 20.0, 20.0); + self.spinner.hidden = YES; + self.spinner.hidesWhenStopped = YES; + self.spinner.multipleTouchEnabled = NO; + self.spinner.opaque = NO; + self.spinner.userInteractionEnabled = NO; + [self.spinner stopAnimating]; + + self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + self.closeButton.imageInsets = UIEdgeInsetsZero; + self.closeButton.style = UIBarButtonItemStylePlain; + self.closeButton.width = 32.000; + + UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + + UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + fixedSpaceButton.width = 20; + + self.toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0.0, (self.view.bounds.size.height - TOOLBAR_HEIGHT), self.view.bounds.size.width, TOOLBAR_HEIGHT)]; + self.toolbar.alpha = 1.000; + self.toolbar.autoresizesSubviews = YES; + self.toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + self.toolbar.barStyle = UIBarStyleBlackOpaque; + self.toolbar.clearsContextBeforeDrawing = NO; + self.toolbar.clipsToBounds = NO; + self.toolbar.contentMode = UIViewContentModeScaleToFill; + self.toolbar.contentStretch = CGRectFromString(@"{{0, 0}, {1, 1}}"); + self.toolbar.hidden = NO; + self.toolbar.multipleTouchEnabled = NO; + self.toolbar.opaque = NO; + self.toolbar.userInteractionEnabled = YES; + + CGFloat labelInset = 5.0; + self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, (self.view.bounds.size.height - FOOTER_HEIGHT), self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; + self.addressLabel.adjustsFontSizeToFitWidth = NO; + self.addressLabel.alpha = 1.000; + self.addressLabel.autoresizesSubviews = YES; + self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; + self.addressLabel.backgroundColor = [UIColor clearColor]; + self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + self.addressLabel.clearsContextBeforeDrawing = YES; + self.addressLabel.clipsToBounds = YES; + self.addressLabel.contentMode = UIViewContentModeScaleToFill; + self.addressLabel.contentStretch = CGRectFromString(@"{{0, 0}, {1, 1}}"); + self.addressLabel.enabled = YES; + self.addressLabel.hidden = NO; + self.addressLabel.lineBreakMode = UILineBreakModeTailTruncation; + self.addressLabel.minimumFontSize = 10.000; + self.addressLabel.multipleTouchEnabled = NO; + self.addressLabel.numberOfLines = 1; + self.addressLabel.opaque = NO; + self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); + self.addressLabel.text = @"Loading..."; + self.addressLabel.textAlignment = UITextAlignmentLeft; + self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; + self.addressLabel.userInteractionEnabled = NO; + + NSString* frontArrowString = @"►"; // create arrow from Unicode char + self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; + self.forwardButton.enabled = YES; + self.forwardButton.imageInsets = UIEdgeInsetsZero; + + NSString* backArrowString = @"◄"; // create arrow from Unicode char + self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; + self.backButton.enabled = YES; + self.backButton.imageInsets = UIEdgeInsetsZero; + + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; + + self.view.backgroundColor = [UIColor grayColor]; + [self.view addSubview:self.toolbar]; + [self.view addSubview:self.addressLabel]; + [self.view addSubview:self.spinner]; +} + +- (void)showLocationBar:(BOOL)show +{ + CGRect addressLabelFrame = self.addressLabel.frame; + BOOL locationBarVisible = (addressLabelFrame.size.height > 0); + + // prevent double show/hide + if (locationBarVisible == show) { + return; + } + + if (show) { + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= FOOTER_HEIGHT; + self.webView.frame = webViewBounds; + + CGRect addressLabelFrame = self.addressLabel.frame; + addressLabelFrame.size.height = LOCATIONBAR_HEIGHT; + self.addressLabel.frame = addressLabelFrame; + } else { + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + self.webView.frame = webViewBounds; + + CGRect addressLabelFrame = self.addressLabel.frame; + addressLabelFrame.size.height = 0; + self.addressLabel.frame = addressLabelFrame; + } +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; +} + +- (void)viewDidUnload +{ + [self.webView loadHTMLString:nil baseURL:nil]; + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + [super viewDidUnload]; +} + +- (void)close +{ + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + + if ([self respondsToSelector:@selector(presentingViewController)]) { + [[self presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[self parentViewController] dismissModalViewControllerAnimated:YES]; + } + + self.currentURL = nil; + + if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + } +} + +- (void)navigateTo:(NSURL*)url +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + + if (_userAgentLockToken != 0) { + [self.webView loadRequest:request]; + } else { + [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { + _userAgentLockToken = lockToken; + [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; + [self.webView loadRequest:request]; + }]; + } +} + +- (void)goBack:(id)sender +{ + [self.webView goBack]; +} + +- (void)goForward:(id)sender +{ + [self.webView goForward]; +} + +#pragma mark UIWebViewDelegate + +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ + // loading url, start spinner, update back/forward + + self.addressLabel.text = @"Loading..."; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + [self.spinner startAnimating]; + + return [self.navigationDelegate webViewDidStartLoad:theWebView]; +} + +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + if (isTopLevelNavigation) { + self.currentURL = request.URL; + } + return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType]; +} + +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + // update url, stop spinner, update back/forward + + self.addressLabel.text = [self.currentURL absoluteString]; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + [self.spinner stopAnimating]; + + // Work around a bug where the first time a PDF is opened, all UIWebViews + // reload their User-Agent from NSUserDefaults. + // This work-around makes the following assumptions: + // 1. The app has only a single Cordova Webview. If not, then the app should + // take it upon themselves to load a PDF in the background as a part of + // their start-up flow. + // 2. That the PDF does not require any additional network requests. We change + // the user-agent here back to that of the CDVViewController, so requests + // from it must pass through its white-list. This *does* break PDFs that + // contain links to other remote PDF/websites. + // More info at https://issues.apache.org/jira/browse/CB-2225 + BOOL isPDF = [@"true" isEqualToString :[theWebView stringByEvaluatingJavaScriptFromString:@"document.body==null"]]; + if (isPDF) { + [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; + } + + [self.navigationDelegate webViewDidFinishLoad:theWebView]; +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + // log fail message, stop spinner, update back/forward + NSLog(@"webView:didFailLoadWithError - %@", [error localizedDescription]); + + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner stopAnimating]; + + self.addressLabel.text = @"Load Error"; + + [self.navigationDelegate webView:theWebView didFailLoadWithError:error]; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + +@end + +@implementation CDVInAppBrowserOptions + +- (id)init +{ + if (self = [super init]) { + // default values + self.location = YES; + + self.enableviewportscale = NO; + self.mediaplaybackrequiresuseraction = NO; + self.allowinlinemediaplayback = NO; + self.keyboarddisplayrequiresuseraction = YES; + self.suppressesincrementalrendering = NO; + } + + return self; +} + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options +{ + CDVInAppBrowserOptions* obj = [[CDVInAppBrowserOptions alloc] init]; + + // NOTE: this parsing does not handle quotes within values + NSArray* pairs = [options componentsSeparatedByString:@","]; + + // parse keys and values, set the properties + for (NSString* pair in pairs) { + NSArray* keyvalue = [pair componentsSeparatedByString:@"="]; + + if ([keyvalue count] == 2) { + NSString* key = [[keyvalue objectAtIndex:0] lowercaseString]; + NSString* value = [[keyvalue objectAtIndex:1] lowercaseString]; + + BOOL isBoolean = [value isEqualToString:@"yes"] || [value isEqualToString:@"no"]; + NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setAllowsFloats:YES]; + BOOL isNumber = [numberFormatter numberFromString:value] != nil; + + // set the property according to the key name + if ([obj respondsToSelector:NSSelectorFromString(key)]) { + if (isNumber) { + [obj setValue:[numberFormatter numberFromString:value] forKey:key]; + } else if (isBoolean) { + [obj setValue:[NSNumber numberWithBool:[value isEqualToString:@"yes"]] forKey:key]; + } else { + [obj setValue:value forKey:key]; + } + } + } + } + + return obj; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVInvokedUrlCommand.h b/cordova/ios/CordovaLib/Classes/CDVInvokedUrlCommand.h new file mode 100755 index 000000000..7be88841d --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVInvokedUrlCommand.h @@ -0,0 +1,57 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVInvokedUrlCommand : NSObject { + NSString* _callbackId; + NSString* _className; + NSString* _methodName; + NSArray* _arguments; +} + +@property (nonatomic, readonly) NSArray* arguments; +@property (nonatomic, readonly) NSString* callbackId; +@property (nonatomic, readonly) NSString* className; +@property (nonatomic, readonly) NSString* methodName; + ++ (CDVInvokedUrlCommand*)commandFromJson:(NSArray*)jsonEntry; + +- (id)initWithArguments:(NSArray*)arguments + callbackId:(NSString*)callbackId + className:(NSString*)className + methodName:(NSString*)methodName; + +- (id)initFromJson:(NSArray*)jsonEntry; + +// The first NSDictionary found in the arguments will be returned in legacyDict. +// The arguments array with be prepended with the callbackId and have the first +// dict removed from it. +- (void)legacyArguments:(NSMutableArray**)legacyArguments andDict:(NSMutableDictionary**)legacyDict; + +// Returns the argument at the given index. +// If index >= the number of arguments, returns nil. +// If the argument at the given index is NSNull, returns nil. +- (id)argumentAtIndex:(NSUInteger)index; +// Same as above, but returns defaultValue instead of nil. +- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue; +// Same as above, but returns defaultValue instead of nil, and if the argument is not of the expected class, returns defaultValue +- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue andClass:(Class)aClass; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVInvokedUrlCommand.m b/cordova/ios/CordovaLib/Classes/CDVInvokedUrlCommand.m new file mode 100755 index 000000000..6c7a85676 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVInvokedUrlCommand.m @@ -0,0 +1,140 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInvokedUrlCommand.h" +#import "CDVJSON.h" +#import "NSData+Base64.h" + +@implementation CDVInvokedUrlCommand + +@synthesize arguments = _arguments; +@synthesize callbackId = _callbackId; +@synthesize className = _className; +@synthesize methodName = _methodName; + ++ (CDVInvokedUrlCommand*)commandFromJson:(NSArray*)jsonEntry +{ + return [[CDVInvokedUrlCommand alloc] initFromJson:jsonEntry]; +} + +- (id)initFromJson:(NSArray*)jsonEntry +{ + id tmp = [jsonEntry objectAtIndex:0]; + NSString* callbackId = tmp == [NSNull null] ? nil : tmp; + NSString* className = [jsonEntry objectAtIndex:1]; + NSString* methodName = [jsonEntry objectAtIndex:2]; + NSMutableArray* arguments = [jsonEntry objectAtIndex:3]; + + return [self initWithArguments:arguments + callbackId:callbackId + className:className + methodName:methodName]; +} + +- (id)initWithArguments:(NSArray*)arguments + callbackId:(NSString*)callbackId + className:(NSString*)className + methodName:(NSString*)methodName +{ + self = [super init]; + if (self != nil) { + _arguments = arguments; + _callbackId = callbackId; + _className = className; + _methodName = methodName; + } + [self massageArguments]; + return self; +} + +- (void)massageArguments +{ + NSMutableArray* newArgs = nil; + + for (NSUInteger i = 0, count = [_arguments count]; i < count; ++i) { + id arg = [_arguments objectAtIndex:i]; + if (![arg isKindOfClass:[NSDictionary class]]) { + continue; + } + NSDictionary* dict = arg; + NSString* type = [dict objectForKey:@"CDVType"]; + if (!type || ![type isEqualToString:@"ArrayBuffer"]) { + continue; + } + NSString* data = [dict objectForKey:@"data"]; + if (!data) { + continue; + } + if (newArgs == nil) { + newArgs = [NSMutableArray arrayWithArray:_arguments]; + _arguments = newArgs; + } + [newArgs replaceObjectAtIndex:i withObject:[NSData dataFromBase64String:data]]; + } +} + +- (void)legacyArguments:(NSMutableArray**)legacyArguments andDict:(NSMutableDictionary**)legacyDict +{ + NSMutableArray* newArguments = [NSMutableArray arrayWithArray:_arguments]; + + for (NSUInteger i = 0; i < [newArguments count]; ++i) { + if ([[newArguments objectAtIndex:i] isKindOfClass:[NSDictionary class]]) { + if (legacyDict != NULL) { + *legacyDict = [newArguments objectAtIndex:i]; + } + [newArguments removeObjectAtIndex:i]; + break; + } + } + + // Legacy (two versions back) has no callbackId. + if (_callbackId != nil) { + [newArguments insertObject:_callbackId atIndex:0]; + } + if (legacyArguments != NULL) { + *legacyArguments = newArguments; + } +} + +- (id)argumentAtIndex:(NSUInteger)index +{ + return [self argumentAtIndex:index withDefault:nil]; +} + +- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue +{ + return [self argumentAtIndex:index withDefault:defaultValue andClass:nil]; +} + +- (id)argumentAtIndex:(NSUInteger)index withDefault:(id)defaultValue andClass:(Class)aClass +{ + if (index >= [_arguments count]) { + return defaultValue; + } + id ret = [_arguments objectAtIndex:index]; + if (ret == [NSNull null]) { + ret = defaultValue; + } + if ((aClass != nil) && ![ret isKindOfClass:aClass]) { + ret = defaultValue; + } + return ret; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVJSON.h b/cordova/ios/CordovaLib/Classes/CDVJSON.h new file mode 100755 index 000000000..eaa895e76 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVJSON.h @@ -0,0 +1,30 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +@interface NSArray (CDVJSONSerializing) +- (NSString*)JSONString; +@end + +@interface NSDictionary (CDVJSONSerializing) +- (NSString*)JSONString; +@end + +@interface NSString (CDVJSONSerializing) +- (id)JSONObject; +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVJSON.m b/cordova/ios/CordovaLib/Classes/CDVJSON.m new file mode 100755 index 000000000..78267e50f --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVJSON.m @@ -0,0 +1,77 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVJSON.h" +#import + +@implementation NSArray (CDVJSONSerializing) + +- (NSString*)JSONString +{ + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:self + options:NSJSONWritingPrettyPrinted + error:&error]; + + if (error != nil) { + NSLog(@"NSArray JSONString error: %@", [error localizedDescription]); + return nil; + } else { + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } +} + +@end + +@implementation NSDictionary (CDVJSONSerializing) + +- (NSString*)JSONString +{ + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:self + options:NSJSONWritingPrettyPrinted + error:&error]; + + if (error != nil) { + NSLog(@"NSDictionary JSONString error: %@", [error localizedDescription]); + return nil; + } else { + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } +} + +@end + +@implementation NSString (CDVJSONSerializing) + +- (id)JSONObject +{ + NSError* error = nil; + id object = [NSJSONSerialization JSONObjectWithData:[self dataUsingEncoding:NSUTF8StringEncoding] + options:kNilOptions + error:&error]; + + if (error != nil) { + NSLog(@"NSString JSONObject error: %@", [error localizedDescription]); + } + + return object; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVJpegHeaderWriter.h b/cordova/ios/CordovaLib/Classes/CDVJpegHeaderWriter.h new file mode 100755 index 000000000..3b43ef0b7 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVJpegHeaderWriter.h @@ -0,0 +1,62 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVJpegHeaderWriter : NSObject { + NSDictionary * SubIFDTagFormatDict; + NSDictionary * IFD0TagFormatDict; +} + +- (NSData*) spliceExifBlockIntoJpeg: (NSData*) jpegdata + withExifBlock: (NSString*) exifstr; +- (NSString*) createExifAPP1 : (NSDictionary*) datadict; +- (NSString*) formattedHexStringFromDecimalNumber: (NSNumber*) numb + withPlaces: (NSNumber*) width; +- (NSString*) formatNumberWithLeadingZeroes: (NSNumber*) numb + withPlaces: (NSNumber*) places; +- (NSString*) decimalToUnsignedRational: (NSNumber*) numb + withResultNumerator: (NSNumber**) numerator + withResultDenominator: (NSNumber**) denominator; +- (void) continuedFraction: (double) val + withFractionList: (NSMutableArray*) fractionlist + withHorizon: (int) horizon; +//- (void) expandContinuedFraction: (NSArray*) fractionlist; +- (void) splitDouble: (double) val + withIntComponent: (int*) rightside + withFloatRemainder: (double*) leftside; +- (NSString*) formatRationalWithNumerator: (NSNumber*) numerator + withDenominator: (NSNumber*) denominator + asSigned: (Boolean) signedFlag; +- (NSString*) hexStringFromData : (NSData*) data; +- (NSNumber*) numericFromHexString : (NSString *) hexstring; + +/* +- (void) readExifMetaData : (NSData*) imgdata; +- (void) spliceImageData : (NSData*) imgdata withExifData: (NSDictionary*) exifdata; +- (void) locateExifMetaData : (NSData*) imgdata; +- (NSString*) createExifAPP1 : (NSDictionary*) datadict; +- (void) createExifDataString : (NSDictionary*) datadict; +- (NSString*) createDataElement : (NSString*) element + withElementData: (NSString*) data + withExternalDataBlock: (NSDictionary*) memblock; +- (NSString*) hexStringFromData : (NSData*) data; +- (NSNumber*) numericFromHexString : (NSString *) hexstring; +*/ +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVJpegHeaderWriter.m b/cordova/ios/CordovaLib/Classes/CDVJpegHeaderWriter.m new file mode 100755 index 000000000..93cafb8d5 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVJpegHeaderWriter.m @@ -0,0 +1,547 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVJpegHeaderWriter.h" +#include "CDVExif.h" + +/* macros for tag info shorthand: + tagno : tag number + typecode : data type + components : number of components + appendString (TAGINF_W_APPEND only) : string to append to data + Exif date data format include an extra 0x00 to the end of the data + */ +#define TAGINF(tagno, typecode, components) [NSArray arrayWithObjects: tagno, typecode, components, nil] +#define TAGINF_W_APPEND(tagno, typecode, components, appendString) [NSArray arrayWithObjects: tagno, typecode, components, appendString, nil] + +const uint mJpegId = 0xffd8; // JPEG format marker +const uint mExifMarker = 0xffe1; // APP1 jpeg header marker +const uint mExif = 0x45786966; // ASCII 'Exif', first characters of valid exif header after size +const uint mMotorallaByteAlign = 0x4d4d; // 'MM', motorola byte align, msb first or 'sane' +const uint mIntelByteAlgin = 0x4949; // 'II', Intel byte align, lsb first or 'batshit crazy reverso world' +const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a(MM) or 0x2a00(II), tiff version number + + +@implementation CDVJpegHeaderWriter + +- (id) init { + self = [super init]; + // supported tags for exif IFD + IFD0TagFormatDict = [[NSDictionary alloc] initWithObjectsAndKeys: + // TAGINF(@"010e", [NSNumber numberWithInt:EDT_ASCII_STRING], @0), @"ImageDescription", + TAGINF_W_APPEND(@"0132", [NSNumber numberWithInt:EDT_ASCII_STRING], @20, @"00"), @"DateTime", + TAGINF(@"010f", [NSNumber numberWithInt:EDT_ASCII_STRING], @0), @"Make", + TAGINF(@"0110", [NSNumber numberWithInt:EDT_ASCII_STRING], @0), @"Model", + TAGINF(@"0131", [NSNumber numberWithInt:EDT_ASCII_STRING], @0), @"Software", + TAGINF(@"011a", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"XResolution", + TAGINF(@"011b", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"YResolution", + // currently supplied outside of Exif data block by UIImagePickerControllerMediaMetadata, this is set manually in CDVCamera.m + /* TAGINF(@"0112", [NSNumber numberWithInt:EDT_USHORT], @1), @"Orientation", + + // rest of the tags are supported by exif spec, but are not specified by UIImagePickerControllerMediaMedadata + // should camera hardware supply these values in future versions, or if they can be derived, ImageHeaderWriter will include them gracefully + TAGINF(@"0128", [NSNumber numberWithInt:EDT_USHORT], @1), @"ResolutionUnit", + TAGINF(@"013e", [NSNumber numberWithInt:EDT_URATIONAL], @2), @"WhitePoint", + TAGINF(@"013f", [NSNumber numberWithInt:EDT_URATIONAL], @6), @"PrimaryChromaticities", + TAGINF(@"0211", [NSNumber numberWithInt:EDT_URATIONAL], @3), @"YCbCrCoefficients", + TAGINF(@"0213", [NSNumber numberWithInt:EDT_USHORT], @1), @"YCbCrPositioning", + TAGINF(@"0214", [NSNumber numberWithInt:EDT_URATIONAL], @6), @"ReferenceBlackWhite", + TAGINF(@"8298", [NSNumber numberWithInt:EDT_URATIONAL], @0), @"Copyright", + + // offset to exif subifd, we determine this dynamically based on the size of the main exif IFD + TAGINF(@"8769", [NSNumber numberWithInt:EDT_ULONG], @1), @"ExifOffset",*/ + nil]; + + + // supported tages for exif subIFD + SubIFDTagFormatDict = [[NSDictionary alloc] initWithObjectsAndKeys: + //TAGINF(@"9000", [NSNumber numberWithInt:], @), @"ExifVersion", + //TAGINF(@"9202",[NSNumber numberWithInt:EDT_URATIONAL],@1), @"ApertureValue", + //TAGINF(@"9203",[NSNumber numberWithInt:EDT_SRATIONAL],@1), @"BrightnessValue", + TAGINF(@"a001",[NSNumber numberWithInt:EDT_USHORT],@1), @"ColorSpace", + TAGINF_W_APPEND(@"9004",[NSNumber numberWithInt:EDT_ASCII_STRING],@20,@"00"), @"DateTimeDigitized", + TAGINF_W_APPEND(@"9003",[NSNumber numberWithInt:EDT_ASCII_STRING],@20,@"00"), @"DateTimeOriginal", + TAGINF(@"a402", [NSNumber numberWithInt:EDT_USHORT], @1), @"ExposureMode", + TAGINF(@"8822", [NSNumber numberWithInt:EDT_USHORT], @1), @"ExposureProgram", + //TAGINF(@"829a", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"ExposureTime", + //TAGINF(@"829d", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"FNumber", + TAGINF(@"9209", [NSNumber numberWithInt:EDT_USHORT], @1), @"Flash", + // FocalLengthIn35mmFilm + TAGINF(@"a405", [NSNumber numberWithInt:EDT_USHORT], @1), @"FocalLenIn35mmFilm", + //TAGINF(@"920a", [NSNumber numberWithInt:EDT_URATIONAL], @1), @"FocalLength", + //TAGINF(@"8827", [NSNumber numberWithInt:EDT_USHORT], @2), @"ISOSpeedRatings", + TAGINF(@"9207", [NSNumber numberWithInt:EDT_USHORT],@1), @"MeteringMode", + // specific to compressed data + TAGINF(@"a002", [NSNumber numberWithInt:EDT_ULONG],@1), @"PixelXDimension", + TAGINF(@"a003", [NSNumber numberWithInt:EDT_ULONG],@1), @"PixelYDimension", + // data type undefined, but this is a DSC camera, so value is always 1, treat as ushort + TAGINF(@"a301", [NSNumber numberWithInt:EDT_USHORT],@1), @"SceneType", + TAGINF(@"a217",[NSNumber numberWithInt:EDT_USHORT],@1), @"SensingMethod", + //TAGINF(@"9201", [NSNumber numberWithInt:EDT_SRATIONAL], @1), @"ShutterSpeedValue", + // specifies location of main subject in scene (x,y,wdith,height) expressed before rotation processing + //TAGINF(@"9214", [NSNumber numberWithInt:EDT_USHORT], @4), @"SubjectArea", + TAGINF(@"a403", [NSNumber numberWithInt:EDT_USHORT], @1), @"WhiteBalance", + nil]; + return self; +} + +- (NSData*) spliceExifBlockIntoJpeg: (NSData*) jpegdata withExifBlock: (NSString*) exifstr { + + CDVJpegHeaderWriter * exifWriter = [[CDVJpegHeaderWriter alloc] init]; + + NSMutableData * exifdata = [NSMutableData dataWithCapacity: [exifstr length]/2]; + int idx; + for (idx = 0; idx+1 < [exifstr length]; idx+=2) { + NSRange range = NSMakeRange(idx, 2); + NSString* hexStr = [exifstr substringWithRange:range]; + NSScanner* scanner = [NSScanner scannerWithString:hexStr]; + unsigned int intValue; + [scanner scanHexInt:&intValue]; + [exifdata appendBytes:&intValue length:1]; + } + + NSMutableData * ddata = [NSMutableData dataWithCapacity: [jpegdata length]]; + NSMakeRange(0,4); + int loc = 0; + bool done = false; + // read the jpeg data until we encounter the app1==0xFFE1 marker + while (loc+1 < [jpegdata length]) { + NSData * blag = [jpegdata subdataWithRange: NSMakeRange(loc,2)]; + if( [[blag description] isEqualToString : @""]) { + // read the APP1 block size bits + NSString * the = [exifWriter hexStringFromData:[jpegdata subdataWithRange: NSMakeRange(loc+2,2)]]; + NSNumber * app1width = [exifWriter numericFromHexString:the]; + //consume the original app1 block + [ddata appendData:exifdata]; + // advance our loc marker past app1 + loc += [app1width intValue] + 2; + done = true; + } else { + if(!done) { + [ddata appendData:blag]; + loc += 2; + } else { + break; + } + } + } + // copy the remaining data + [ddata appendData:[jpegdata subdataWithRange: NSMakeRange(loc,[jpegdata length]-loc)]]; + return ddata; +} + + + +/** + * Create the Exif data block as a hex string + * jpeg uses Application Markers (APP's) as markers for application data + * APP1 is the application marker reserved for exif data + * + * (NSDictionary*) datadict - with subdictionaries marked '{TIFF}' and '{EXIF}' as returned by imagePickerController with a valid + * didFinishPickingMediaWithInfo data dict, under key @"UIImagePickerControllerMediaMetadata" + * + * the following constructs a hex string to Exif specifications, and is therefore brittle + * altering the order of arguments to the string constructors, modifying field sizes or formats, + * and any other minor change will likely prevent the exif data from being read + */ +- (NSString*) createExifAPP1 : (NSDictionary*) datadict { + NSMutableString * app1; // holds finalized product + NSString * exifIFD; // exif information file directory + NSString * subExifIFD; // subexif information file directory + + // FFE1 is the hex APP1 marker code, and will allow client apps to read the data + NSString * app1marker = @"ffe1"; + // SSSS size, to be determined + // EXIF ascii characters followed by 2bytes of zeros + NSString * exifmarker = @"457869660000"; + // Tiff header: 4d4d is motorolla byte align (big endian), 002a is hex for 42 + NSString * tiffheader = @"4d4d002a"; + //first IFD offset from the Tiff header to IFD0. Since we are writing it, we know it's address 0x08 + NSString * ifd0offset = @"00000008"; + // current offset to next data area + int currentDataOffset = 0; + + //data labeled as TIFF in UIImagePickerControllerMediaMetaData is part of the EXIF IFD0 portion of APP1 + exifIFD = [self createExifIFDFromDict: [datadict objectForKey:@"{TIFF}"] withFormatDict: IFD0TagFormatDict isIFD0:YES currentDataOffset:¤tDataOffset]; + + //data labeled as EXIF in UIImagePickerControllerMediaMetaData is part of the EXIF Sub IFD portion of APP1 + subExifIFD = [self createExifIFDFromDict: [datadict objectForKey:@"{Exif}"] withFormatDict: SubIFDTagFormatDict isIFD0:NO currentDataOffset:¤tDataOffset]; + /* + NSLog(@"SUB EXIF IFD %@ WITH SIZE: %d",exifIFD,[exifIFD length]); + + NSLog(@"SUB EXIF IFD %@ WITH SIZE: %d",subExifIFD,[subExifIFD length]); + */ + // construct the complete app1 data block + app1 = [[NSMutableString alloc] initWithFormat: @"%@%04x%@%@%@%@%@", + app1marker, + 16 + ([exifIFD length]/2) + ([subExifIFD length]/2) /*16+[exifIFD length]/2*/, + exifmarker, + tiffheader, + ifd0offset, + exifIFD, + subExifIFD]; + + return app1; +} + +// returns hex string representing a valid exif information file directory constructed from the datadict and formatdict +- (NSString*) createExifIFDFromDict : (NSDictionary*) datadict + withFormatDict : (NSDictionary*) formatdict + isIFD0 : (BOOL) ifd0flag + currentDataOffset : (int*) dataoffset { + NSArray * datakeys = [datadict allKeys]; // all known data keys + NSArray * knownkeys = [formatdict allKeys]; // only keys in knowkeys are considered for entry in this IFD + NSMutableArray * ifdblock = [[NSMutableArray alloc] initWithCapacity: [datadict count]]; // all ifd entries + NSMutableArray * ifddatablock = [[NSMutableArray alloc] initWithCapacity: [datadict count]]; // data block entries + // ifd0flag = NO; // ifd0 requires a special flag and has offset to next ifd appended to end + + // iterate through known provided data keys + for (int i = 0; i < [datakeys count]; i++) { + NSString * key = [datakeys objectAtIndex:i]; + // don't muck about with unknown keys + if ([knownkeys indexOfObject: key] != NSNotFound) { + // create new IFD entry + NSString * entry = [self createIFDElement: key + withFormat: [formatdict objectForKey:key] + withElementData: [datadict objectForKey:key]]; + // create the IFD entry's data block + NSString * data = [self createIFDElementDataWithFormat: [formatdict objectForKey:key] + withData: [datadict objectForKey:key]]; + if (entry) { + [ifdblock addObject:entry]; + if(!data) { + [ifdblock addObject:@""]; + } else { + [ifddatablock addObject:data]; + } + } + } + } + + NSMutableString * exifstr = [[NSMutableString alloc] initWithCapacity: [ifdblock count] * 24]; + NSMutableString * dbstr = [[NSMutableString alloc] initWithCapacity: 100]; + + int addr=*dataoffset; // current offset/address in datablock + if (ifd0flag) { + // calculate offset to datablock based on ifd file entry count + addr += 14+(12*([ifddatablock count]+1)); // +1 for tag 0x8769, exifsubifd offset + } else { + // current offset + numSubIFDs (2-bytes) + 12*numSubIFDs + endMarker (4-bytes) + addr += 2+(12*[ifddatablock count])+4; + } + + for (int i = 0; i < [ifdblock count]; i++) { + NSString * entry = [ifdblock objectAtIndex:i]; + NSString * data = [ifddatablock objectAtIndex:i]; + + // check if the data fits into 4 bytes + if( [data length] <= 8) { + // concatenate the entry and the (4byte) data entry into the final IFD entry and append to exif ifd string + [exifstr appendFormat : @"%@%@", entry, data]; + } else { + [exifstr appendFormat : @"%@%08x", entry, addr]; + [dbstr appendFormat: @"%@", data]; + addr+= [data length] / 2; + /* + NSLog(@"=====data-length[%i]=======",[data length]); + NSLog(@"addr-offset[%i]",addr); + NSLog(@"entry[%@]",entry); + NSLog(@"data[%@]",data); + */ + } + } + + // calculate IFD0 terminal offset tags, currently ExifSubIFD + int entrycount = [ifdblock count]; + if (ifd0flag) { + // 18 accounts for 8769's width + offset to next ifd, 8 accounts for start of header + NSNumber * offset = [NSNumber numberWithInt:[exifstr length] / 2 + [dbstr length] / 2 + 18+8]; + + [self appendExifOffsetTagTo: exifstr + withOffset : offset]; + entrycount++; + } + *dataoffset = addr; + return [[NSString alloc] initWithFormat: @"%04x%@%@%@", + entrycount, + exifstr, + @"00000000", // offset to next IFD, 0 since there is none + dbstr]; // lastly, the datablock +} + +// Creates an exif formatted exif information file directory entry +- (NSString*) createIFDElement: (NSString*) elementName withFormat: (NSArray*) formtemplate withElementData: (NSString*) data { + //NSArray * fielddata = [formatdict objectForKey: elementName];// format data of desired field + if (formtemplate) { + // format string @"%@%@%@%@", tag number, data format, components, value + NSNumber * dataformat = [formtemplate objectAtIndex:1]; + NSNumber * components = [formtemplate objectAtIndex:2]; + if([components intValue] == 0) { + components = [NSNumber numberWithInt: [data length] * DataTypeToWidth[[dataformat intValue]-1]]; + } + + return [[NSString alloc] initWithFormat: @"%@%@%08x", + [formtemplate objectAtIndex:0], // the field code + [self formatNumberWithLeadingZeroes: dataformat withPlaces: @4], // the data type code + [components intValue]]; // number of components + } + return NULL; +} + +/** + * appends exif IFD0 tag 8769 "ExifOffset" to the string provided + * (NSMutableString*) str - string you wish to append the 8769 tag to: APP1 or IFD0 hex data string + * // TAGINF(@"8769", [NSNumber numberWithInt:EDT_ULONG], @1), @"ExifOffset", + */ +- (void) appendExifOffsetTagTo: (NSMutableString*) str withOffset : (NSNumber*) offset { + NSArray * format = TAGINF(@"8769", [NSNumber numberWithInt:EDT_ULONG], @1); + + NSString * entry = [self createIFDElement: @"ExifOffset" + withFormat: format + withElementData: [offset stringValue]]; + + NSString * data = [self createIFDElementDataWithFormat: format + withData: [offset stringValue]]; + [str appendFormat:@"%@%@", entry, data]; +} + +// formats the Information File Directory Data to exif format +- (NSString*) createIFDElementDataWithFormat: (NSArray*) dataformat withData: (NSString*) data { + NSMutableString * datastr = nil; + NSNumber * tmp = nil; + NSNumber * formatcode = [dataformat objectAtIndex:1]; + NSUInteger formatItemsCount = [dataformat count]; + NSNumber * num = @0; + NSNumber * denom = @0; + + switch ([formatcode intValue]) { + case EDT_UBYTE: + break; + case EDT_ASCII_STRING: + datastr = [[NSMutableString alloc] init]; + for (int i = 0; i < [data length]; i++) { + [datastr appendFormat:@"%02x",[data characterAtIndex:i]]; + } + if (formatItemsCount > 3) { + // We have additional data to append. + // currently used by Date format to append final 0x00 but can be used by other data types as well in the future + [datastr appendString:[dataformat objectAtIndex:3]]; + } + if ([datastr length] < 8) { + NSString * format = [NSString stringWithFormat:@"%%0%dd", 8 - [datastr length]]; + [datastr appendFormat:format,0]; + } + return datastr; + case EDT_USHORT: + return [[NSString alloc] initWithFormat : @"%@%@", + [self formattedHexStringFromDecimalNumber: [NSNumber numberWithInt: [data intValue]] withPlaces: @4], + @"0000"]; + case EDT_ULONG: + tmp = [NSNumber numberWithUnsignedLong:[data intValue]]; + return [NSString stringWithFormat : @"%@", + [self formattedHexStringFromDecimalNumber: tmp withPlaces: @8]]; + case EDT_URATIONAL: + return [self decimalToUnsignedRational: [NSNumber numberWithDouble:[data doubleValue]] + withResultNumerator: &num + withResultDenominator: &denom]; + case EDT_SBYTE: + + break; + case EDT_UNDEFINED: + break; // 8 bits + case EDT_SSHORT: + break; + case EDT_SLONG: + break; // 32bit signed integer (2's complement) + case EDT_SRATIONAL: + break; // 2 SLONGS, first long is numerator, second is denominator + case EDT_SINGLEFLOAT: + break; + case EDT_DOUBLEFLOAT: + break; + } + return datastr; +} + +//====================================================================================================================== +// Utility Methods +//====================================================================================================================== + +// creates a formatted little endian hex string from a number and width specifier +- (NSString*) formattedHexStringFromDecimalNumber: (NSNumber*) numb withPlaces: (NSNumber*) width { + NSMutableString * str = [[NSMutableString alloc] initWithCapacity:[width intValue]]; + NSString * formatstr = [[NSString alloc] initWithFormat: @"%%%@%dx", @"0", [width intValue]]; + [str appendFormat:formatstr, [numb intValue]]; + return str; +} + +// format number as string with leading 0's +- (NSString*) formatNumberWithLeadingZeroes: (NSNumber *) numb withPlaces: (NSNumber *) places { + NSNumberFormatter * formatter = [[NSNumberFormatter alloc] init]; + NSString *formatstr = [@"" stringByPaddingToLength:[places unsignedIntegerValue] withString:@"0" startingAtIndex:0]; + [formatter setPositiveFormat:formatstr]; + return [formatter stringFromNumber:numb]; +} + +// approximate a decimal with a rational by method of continued fraction +// can be collasped into decimalToUnsignedRational after testing +- (void) decimalToRational: (NSNumber *) numb + withResultNumerator: (NSNumber**) numerator + withResultDenominator: (NSNumber**) denominator { + NSMutableArray * fractionlist = [[NSMutableArray alloc] initWithCapacity:8]; + + [self continuedFraction: [numb doubleValue] + withFractionList: fractionlist + withHorizon: 8]; + + // simplify complex fraction represented by partial fraction list + [self expandContinuedFraction: fractionlist + withResultNumerator: numerator + withResultDenominator: denominator]; + +} + +// approximate a decimal with an unsigned rational by method of continued fraction +- (NSString*) decimalToUnsignedRational: (NSNumber *) numb + withResultNumerator: (NSNumber**) numerator + withResultDenominator: (NSNumber**) denominator { + NSMutableArray * fractionlist = [[NSMutableArray alloc] initWithCapacity:8]; + + // generate partial fraction list + [self continuedFraction: [numb doubleValue] + withFractionList: fractionlist + withHorizon: 8]; + + // simplify complex fraction represented by partial fraction list + [self expandContinuedFraction: fractionlist + withResultNumerator: numerator + withResultDenominator: denominator]; + + return [self formatFractionList: fractionlist]; +} + +// recursive implementation of decimal approximation by continued fraction +- (void) continuedFraction: (double) val + withFractionList: (NSMutableArray*) fractionlist + withHorizon: (int) horizon { + int whole; + double remainder; + // 1. split term + [self splitDouble: val withIntComponent: &whole withFloatRemainder: &remainder]; + [fractionlist addObject: [NSNumber numberWithInt:whole]]; + + // 2. calculate reciprocal of remainder + if (!remainder) return; // early exit, exact fraction found, avoids recip/0 + double recip = 1 / remainder; + + // 3. exit condition + if ([fractionlist count] > horizon) { + return; + } + + // 4. recurse + [self continuedFraction:recip withFractionList: fractionlist withHorizon: horizon]; + +} + +// expand continued fraction list, creating a single level rational approximation +-(void) expandContinuedFraction: (NSArray*) fractionlist + withResultNumerator: (NSNumber**) numerator + withResultDenominator: (NSNumber**) denominator { + int i = 0; + int den = 0; + int num = 0; + if ([fractionlist count] == 1) { + *numerator = [NSNumber numberWithInt:[[fractionlist objectAtIndex:0] intValue]]; + *denominator = @1; + return; + } + + //begin at the end of the list + i = [fractionlist count] - 1; + num = 1; + den = [[fractionlist objectAtIndex:i] intValue]; + + while (i > 0) { + int t = [[fractionlist objectAtIndex: i-1] intValue]; + num = t * den + num; + if (i==1) { + break; + } else { + t = num; + num = den; + den = t; + } + i--; + } + // set result parameters values + *numerator = [NSNumber numberWithInt: num]; + *denominator = [NSNumber numberWithInt: den]; +} + +// formats expanded fraction list to string matching exif specification +- (NSString*) formatFractionList: (NSArray *) fractionlist { + NSMutableString * str = [[NSMutableString alloc] initWithCapacity:16]; + + if ([fractionlist count] == 1){ + [str appendFormat: @"%08x00000001", [[fractionlist objectAtIndex:0] intValue]]; + } + return str; +} + +// format rational as +- (NSString*) formatRationalWithNumerator: (NSNumber*) numerator withDenominator: (NSNumber*) denominator asSigned: (Boolean) signedFlag { + NSMutableString * str = [[NSMutableString alloc] initWithCapacity:16]; + if (signedFlag) { + long num = [numerator longValue]; + long den = [denominator longValue]; + [str appendFormat: @"%08lx%08lx", num >= 0 ? num : ~ABS(num) + 1, num >= 0 ? den : ~ABS(den) + 1]; + } else { + [str appendFormat: @"%08lx%08lx", [numerator unsignedLongValue], [denominator unsignedLongValue]]; + } + return str; +} + +// split a floating point number into two integer values representing the left and right side of the decimal +- (void) splitDouble: (double) val withIntComponent: (int*) rightside withFloatRemainder: (double*) leftside { + *rightside = val; // convert numb to int representation, which truncates the decimal portion + *leftside = val - *rightside; +} + + +// +- (NSString*) hexStringFromData : (NSData*) data { + //overflow detection + const unsigned char *dataBuffer = [data bytes]; + return [[NSString alloc] initWithFormat: @"%02x%02x", + (unsigned char)dataBuffer[0], + (unsigned char)dataBuffer[1]]; +} + +// convert a hex string to a number +- (NSNumber*) numericFromHexString : (NSString *) hexstring { + NSScanner * scan = NULL; + unsigned int numbuf= 0; + + scan = [NSScanner scannerWithString:hexstring]; + [scan scanHexInt:&numbuf]; + return [NSNumber numberWithInt:numbuf]; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVLocalStorage.h b/cordova/ios/CordovaLib/Classes/CDVLocalStorage.h new file mode 100755 index 000000000..dec6ab3b9 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVLocalStorage.h @@ -0,0 +1,50 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" + +#define kCDVLocalStorageErrorDomain @"kCDVLocalStorageErrorDomain" +#define kCDVLocalStorageFileOperationError 1 + +@interface CDVLocalStorage : CDVPlugin + +@property (nonatomic, readonly, strong) NSMutableArray* backupInfo; + +- (BOOL)shouldBackup; +- (BOOL)shouldRestore; +- (void)backup:(CDVInvokedUrlCommand*)command; +- (void)restore:(CDVInvokedUrlCommand*)command; + ++ (void)__fixupDatabaseLocationsWithBackupType:(NSString*)backupType; +// Visible for testing. ++ (BOOL)__verifyAndFixDatabaseLocationsWithAppPlistDict:(NSMutableDictionary*)appPlistDict + bundlePath:(NSString*)bundlePath + fileManager:(NSFileManager*)fileManager; +@end + +@interface CDVBackupInfo : NSObject + +@property (nonatomic, copy) NSString* original; +@property (nonatomic, copy) NSString* backup; +@property (nonatomic, copy) NSString* label; + +- (BOOL)shouldBackup; +- (BOOL)shouldRestore; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVLocalStorage.m b/cordova/ios/CordovaLib/Classes/CDVLocalStorage.m new file mode 100755 index 000000000..238d6807a --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVLocalStorage.m @@ -0,0 +1,485 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVLocalStorage.h" +#import "CDV.h" + +@interface CDVLocalStorage () + +@property (nonatomic, readwrite, strong) NSMutableArray* backupInfo; // array of CDVBackupInfo objects +@property (nonatomic, readwrite, weak) id webviewDelegate; + +@end + +@implementation CDVLocalStorage + +@synthesize backupInfo, webviewDelegate; + +- (void)pluginInitialize +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResignActive) + name:UIApplicationWillResignActiveNotification object:nil]; + BOOL cloudBackup = [@"cloud" isEqualToString : self.commandDelegate.settings[@"BackupWebStorage"]]; + + self.backupInfo = [[self class] createBackupInfoWithCloudBackup:cloudBackup]; +} + +#pragma mark - +#pragma mark Plugin interface methods + ++ (NSMutableArray*)createBackupInfoWithTargetDir:(NSString*)targetDir backupDir:(NSString*)backupDir targetDirNests:(BOOL)targetDirNests backupDirNests:(BOOL)backupDirNests rename:(BOOL)rename +{ + /* + This "helper" does so much work and has so many options it would probably be clearer to refactor the whole thing. + Basically, there are three database locations: + + 1. "Normal" dir -- LIB// + 2. "Caches" dir -- LIB/Caches/ + 3. "Backup" dir -- DOC/Backups/ + + And between these three, there are various migration paths, most of which only consider 2 of the 3, which is why this helper is based on 2 locations and has a notion of "direction". + */ + NSMutableArray* backupInfo = [NSMutableArray arrayWithCapacity:3]; + + NSString* original; + NSString* backup; + CDVBackupInfo* backupItem; + + // ////////// LOCALSTORAGE + + original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/file__0.localstorage":@"file__0.localstorage"]; + backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")]; + backup = [backup stringByAppendingPathComponent:(rename ? @"localstorage.appdata.db" : @"file__0.localstorage")]; + + backupItem = [[CDVBackupInfo alloc] init]; + backupItem.backup = backup; + backupItem.original = original; + backupItem.label = @"localStorage database"; + + [backupInfo addObject:backupItem]; + + // ////////// WEBSQL MAIN DB + + original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/Databases.db":@"Databases.db"]; + backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")]; + backup = [backup stringByAppendingPathComponent:(rename ? @"websqlmain.appdata.db" : @"Databases.db")]; + + backupItem = [[CDVBackupInfo alloc] init]; + backupItem.backup = backup; + backupItem.original = original; + backupItem.label = @"websql main database"; + + [backupInfo addObject:backupItem]; + + // ////////// WEBSQL DATABASES + + original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/file__0":@"file__0"]; + backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")]; + backup = [backup stringByAppendingPathComponent:(rename ? @"websqldbs.appdata.db" : @"file__0")]; + + backupItem = [[CDVBackupInfo alloc] init]; + backupItem.backup = backup; + backupItem.original = original; + backupItem.label = @"websql databases"; + + [backupInfo addObject:backupItem]; + + return backupInfo; +} + ++ (NSMutableArray*)createBackupInfoWithCloudBackup:(BOOL)cloudBackup +{ + // create backup info from backup folder to caches folder + NSString* appLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + NSString* appDocumentsFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + NSString* cacheFolder = [appLibraryFolder stringByAppendingPathComponent:@"Caches"]; + NSString* backupsFolder = [appDocumentsFolder stringByAppendingPathComponent:@"Backups"]; + + // create the backups folder, if needed + [[NSFileManager defaultManager] createDirectoryAtPath:backupsFolder withIntermediateDirectories:YES attributes:nil error:nil]; + + [self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:backupsFolder] skip:!cloudBackup]; + + return [self createBackupInfoWithTargetDir:cacheFolder backupDir:backupsFolder targetDirNests:NO backupDirNests:NO rename:YES]; +} + ++ (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL*)URL skip:(BOOL)skip +{ + NSAssert(IsAtLeastiOSVersion(@"5.1"), @"Cannot mark files for NSURLIsExcludedFromBackupKey on iOS less than 5.1"); + + NSError* error = nil; + BOOL success = [URL setResourceValue:[NSNumber numberWithBool:skip] forKey:NSURLIsExcludedFromBackupKey error:&error]; + if (!success) { + NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error); + } + return success; +} + ++ (BOOL)copyFrom:(NSString*)src to:(NSString*)dest error:(NSError* __autoreleasing*)error +{ + NSFileManager* fileManager = [NSFileManager defaultManager]; + + if (![fileManager fileExistsAtPath:src]) { + NSString* errorString = [NSString stringWithFormat:@"%@ file does not exist.", src]; + if (error != NULL) { + (*error) = [NSError errorWithDomain:kCDVLocalStorageErrorDomain + code:kCDVLocalStorageFileOperationError + userInfo:[NSDictionary dictionaryWithObject:errorString + forKey:NSLocalizedDescriptionKey]]; + } + return NO; + } + + // generate unique filepath in temp directory + CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); + CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); + NSString* tempBackup = [[NSTemporaryDirectory() stringByAppendingPathComponent:(__bridge NSString*)uuidString] stringByAppendingPathExtension:@"bak"]; + CFRelease(uuidString); + CFRelease(uuidRef); + + BOOL destExists = [fileManager fileExistsAtPath:dest]; + + // backup the dest + if (destExists && ![fileManager copyItemAtPath:dest toPath:tempBackup error:error]) { + return NO; + } + + // remove the dest + if (destExists && ![fileManager removeItemAtPath:dest error:error]) { + return NO; + } + + // create path to dest + if (!destExists && ![fileManager createDirectoryAtPath:[dest stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:error]) { + return NO; + } + + // copy src to dest + if ([fileManager copyItemAtPath:src toPath:dest error:error]) { + // success - cleanup - delete the backup to the dest + if ([fileManager fileExistsAtPath:tempBackup]) { + [fileManager removeItemAtPath:tempBackup error:error]; + } + return YES; + } else { + // failure - we restore the temp backup file to dest + [fileManager copyItemAtPath:tempBackup toPath:dest error:error]; + // cleanup - delete the backup to the dest + if ([fileManager fileExistsAtPath:tempBackup]) { + [fileManager removeItemAtPath:tempBackup error:error]; + } + return NO; + } +} + +- (BOOL)shouldBackup +{ + for (CDVBackupInfo* info in self.backupInfo) { + if ([info shouldBackup]) { + return YES; + } + } + + return NO; +} + +- (BOOL)shouldRestore +{ + for (CDVBackupInfo* info in self.backupInfo) { + if ([info shouldRestore]) { + return YES; + } + } + + return NO; +} + +/* copy from webkitDbLocation to persistentDbLocation */ +- (void)backup:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + + NSError* __autoreleasing error = nil; + CDVPluginResult* result = nil; + NSString* message = nil; + + for (CDVBackupInfo* info in self.backupInfo) { + if ([info shouldBackup]) { + [[self class] copyFrom:info.original to:info.backup error:&error]; + + if (callbackId) { + if (error == nil) { + message = [NSString stringWithFormat:@"Backed up: %@", info.label]; + NSLog(@"%@", message); + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + message = [NSString stringWithFormat:@"Error in CDVLocalStorage (%@) backup: %@", info.label, [error localizedDescription]]; + NSLog(@"%@", message); + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:message]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + } + } + } +} + +/* copy from persistentDbLocation to webkitDbLocation */ +- (void)restore:(CDVInvokedUrlCommand*)command +{ + NSError* __autoreleasing error = nil; + CDVPluginResult* result = nil; + NSString* message = nil; + + for (CDVBackupInfo* info in self.backupInfo) { + if ([info shouldRestore]) { + [[self class] copyFrom:info.backup to:info.original error:&error]; + + if (error == nil) { + message = [NSString stringWithFormat:@"Restored: %@", info.label]; + NSLog(@"%@", message); + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } else { + message = [NSString stringWithFormat:@"Error in CDVLocalStorage (%@) restore: %@", info.label, [error localizedDescription]]; + NSLog(@"%@", message); + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:message]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + } + } +} + ++ (void)__fixupDatabaseLocationsWithBackupType:(NSString*)backupType +{ + [self __verifyAndFixDatabaseLocations]; + [self __restoreLegacyDatabaseLocationsWithBackupType:backupType]; +} + ++ (void)__verifyAndFixDatabaseLocations +{ + NSBundle* mainBundle = [NSBundle mainBundle]; + NSString* bundlePath = [[mainBundle bundlePath] stringByDeletingLastPathComponent]; + NSString* bundleIdentifier = [[mainBundle infoDictionary] objectForKey:@"CFBundleIdentifier"]; + NSString* appPlistPath = [bundlePath stringByAppendingPathComponent:[NSString stringWithFormat:@"Library/Preferences/%@.plist", bundleIdentifier]]; + + NSMutableDictionary* appPlistDict = [NSMutableDictionary dictionaryWithContentsOfFile:appPlistPath]; + BOOL modified = [[self class] __verifyAndFixDatabaseLocationsWithAppPlistDict:appPlistDict + bundlePath:bundlePath + fileManager:[NSFileManager defaultManager]]; + + if (modified) { + BOOL ok = [appPlistDict writeToFile:appPlistPath atomically:YES]; + [[NSUserDefaults standardUserDefaults] synchronize]; + NSLog(@"Fix applied for database locations?: %@", ok ? @"YES" : @"NO"); + } +} + ++ (BOOL)__verifyAndFixDatabaseLocationsWithAppPlistDict:(NSMutableDictionary*)appPlistDict + bundlePath:(NSString*)bundlePath + fileManager:(NSFileManager*)fileManager +{ + NSString* libraryCaches = @"Library/Caches"; + NSString* libraryWebKit = @"Library/WebKit"; + + NSArray* keysToCheck = [NSArray arrayWithObjects: + @"WebKitLocalStorageDatabasePathPreferenceKey", + @"WebDatabaseDirectory", + nil]; + + BOOL dirty = NO; + + for (NSString* key in keysToCheck) { + NSString* value = [appPlistDict objectForKey:key]; + // verify key exists, and path is in app bundle, if not - fix + if ((value != nil) && ![value hasPrefix:bundlePath]) { + // the pathSuffix to use may be wrong - OTA upgrades from < 5.1 to 5.1 do keep the old path Library/WebKit, + // while Xcode synced ones do change the storage location to Library/Caches + NSString* newBundlePath = [bundlePath stringByAppendingPathComponent:libraryCaches]; + if (![fileManager fileExistsAtPath:newBundlePath]) { + newBundlePath = [bundlePath stringByAppendingPathComponent:libraryWebKit]; + } + [appPlistDict setValue:newBundlePath forKey:key]; + dirty = YES; + } + } + + return dirty; +} + ++ (void)__restoreLegacyDatabaseLocationsWithBackupType:(NSString*)backupType +{ + // on iOS 6, if you toggle between cloud/local backup, you must move database locations. Default upgrade from iOS5.1 to iOS6 is like a toggle from local to cloud. + if (!IsAtLeastiOSVersion(@"6.0")) { + return; + } + + NSString* appLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + NSString* appDocumentsFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + + NSMutableArray* backupInfo = [NSMutableArray arrayWithCapacity:0]; + + if ([backupType isEqualToString:@"cloud"]) { + // We would like to restore old backups/caches databases to the new destination (nested in lib folder) + [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:appLibraryFolder backupDir:[appDocumentsFolder stringByAppendingPathComponent:@"Backups"] targetDirNests:YES backupDirNests:NO rename:YES]]; + [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:appLibraryFolder backupDir:[appLibraryFolder stringByAppendingPathComponent:@"Caches"] targetDirNests:YES backupDirNests:NO rename:NO]]; + } else { + // For ios6 local backups we also want to restore from Backups dir -- but we don't need to do that here, since the plugin will do that itself. + [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:[appLibraryFolder stringByAppendingPathComponent:@"Caches"] backupDir:appLibraryFolder targetDirNests:NO backupDirNests:YES rename:NO]]; + } + + NSFileManager* manager = [NSFileManager defaultManager]; + + for (CDVBackupInfo* info in backupInfo) { + if ([manager fileExistsAtPath:info.backup]) { + if ([info shouldRestore]) { + NSLog(@"Restoring old webstorage backup. From: '%@' To: '%@'.", info.backup, info.original); + [self copyFrom:info.backup to:info.original error:nil]; + } + NSLog(@"Removing old webstorage backup: '%@'.", info.backup); + [manager removeItemAtPath:info.backup error:nil]; + } + } + + [[NSUserDefaults standardUserDefaults] setBool:[backupType isEqualToString:@"cloud"] forKey:@"WebKitStoreWebDataForBackup"]; +} + +#pragma mark - +#pragma mark Notification handlers + +- (void)onResignActive +{ + UIDevice* device = [UIDevice currentDevice]; + NSNumber* exitsOnSuspend = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationExitsOnSuspend"]; + + BOOL isMultitaskingSupported = [device respondsToSelector:@selector(isMultitaskingSupported)] && [device isMultitaskingSupported]; + + if (exitsOnSuspend == nil) { // if it's missing, it should be NO (i.e. multi-tasking on by default) + exitsOnSuspend = [NSNumber numberWithBool:NO]; + } + + if (exitsOnSuspend) { + [self backup:nil]; + } else if (isMultitaskingSupported) { + __block UIBackgroundTaskIdentifier backgroundTaskID = UIBackgroundTaskInvalid; + + backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskID]; + backgroundTaskID = UIBackgroundTaskInvalid; + NSLog(@"Background task to backup WebSQL/LocalStorage expired."); + }]; + CDVLocalStorage __weak* weakSelf = self; + [self.commandDelegate runInBackground:^{ + [weakSelf backup:nil]; + + [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskID]; + backgroundTaskID = UIBackgroundTaskInvalid; + }]; + } +} + +- (void)onAppTerminate +{ + [self onResignActive]; +} + +- (void)onReset +{ + [self restore:nil]; +} + +@end + +#pragma mark - +#pragma mark CDVBackupInfo implementation + +@implementation CDVBackupInfo + +@synthesize original, backup, label; + +- (BOOL)file:(NSString*)aPath isNewerThanFile:(NSString*)bPath +{ + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSError* __autoreleasing error = nil; + + NSDictionary* aPathAttribs = [fileManager attributesOfItemAtPath:aPath error:&error]; + NSDictionary* bPathAttribs = [fileManager attributesOfItemAtPath:bPath error:&error]; + + NSDate* aPathModDate = [aPathAttribs objectForKey:NSFileModificationDate]; + NSDate* bPathModDate = [bPathAttribs objectForKey:NSFileModificationDate]; + + if ((nil == aPathModDate) && (nil == bPathModDate)) { + return NO; + } + + return [aPathModDate compare:bPathModDate] == NSOrderedDescending || bPathModDate == nil; +} + +- (BOOL)item:(NSString*)aPath isNewerThanItem:(NSString*)bPath +{ + NSFileManager* fileManager = [NSFileManager defaultManager]; + + BOOL aPathIsDir = NO, bPathIsDir = NO; + BOOL aPathExists = [fileManager fileExistsAtPath:aPath isDirectory:&aPathIsDir]; + + [fileManager fileExistsAtPath:bPath isDirectory:&bPathIsDir]; + + if (!aPathExists) { + return NO; + } + + if (!(aPathIsDir && bPathIsDir)) { // just a file + return [self file:aPath isNewerThanFile:bPath]; + } + + // essentially we want rsync here, but have to settle for our poor man's implementation + // we get the files in aPath, and see if it is newer than the file in bPath + // (it is newer if it doesn't exist in bPath) if we encounter the FIRST file that is newer, + // we return YES + NSDirectoryEnumerator* directoryEnumerator = [fileManager enumeratorAtPath:aPath]; + NSString* path; + + while ((path = [directoryEnumerator nextObject])) { + NSString* aPathFile = [aPath stringByAppendingPathComponent:path]; + NSString* bPathFile = [bPath stringByAppendingPathComponent:path]; + + BOOL isNewer = [self file:aPathFile isNewerThanFile:bPathFile]; + if (isNewer) { + return YES; + } + } + + return NO; +} + +- (BOOL)shouldBackup +{ + return [self item:self.original isNewerThanItem:self.backup]; +} + +- (BOOL)shouldRestore +{ + return [self item:self.backup isNewerThanItem:self.original]; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVLocation.h b/cordova/ios/CordovaLib/Classes/CDVLocation.h new file mode 100755 index 000000000..caf07989c --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVLocation.h @@ -0,0 +1,104 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import "CDVPlugin.h" + +enum CDVHeadingStatus { + HEADINGSTOPPED = 0, + HEADINGSTARTING, + HEADINGRUNNING, + HEADINGERROR +}; +typedef NSUInteger CDVHeadingStatus; + +enum CDVLocationStatus { + PERMISSIONDENIED = 1, + POSITIONUNAVAILABLE, + TIMEOUT +}; +typedef NSUInteger CDVLocationStatus; + +// simple object to keep track of heading information +@interface CDVHeadingData : NSObject {} + +@property (nonatomic, assign) CDVHeadingStatus headingStatus; +@property (nonatomic, strong) CLHeading* headingInfo; +@property (nonatomic, strong) NSMutableArray* headingCallbacks; +@property (nonatomic, copy) NSString* headingFilter; +@property (nonatomic, strong) NSDate* headingTimestamp; +@property (assign) NSInteger timeout; + +@end + +// simple object to keep track of location information +@interface CDVLocationData : NSObject { + CDVLocationStatus locationStatus; + NSMutableArray* locationCallbacks; + NSMutableDictionary* watchCallbacks; + CLLocation* locationInfo; +} + +@property (nonatomic, assign) CDVLocationStatus locationStatus; +@property (nonatomic, strong) CLLocation* locationInfo; +@property (nonatomic, strong) NSMutableArray* locationCallbacks; +@property (nonatomic, strong) NSMutableDictionary* watchCallbacks; + +@end + +@interface CDVLocation : CDVPlugin { + @private BOOL __locationStarted; + @private BOOL __highAccuracyEnabled; + CDVHeadingData* headingData; + CDVLocationData* locationData; +} + +@property (nonatomic, strong) CLLocationManager* locationManager; +@property (strong) CDVHeadingData* headingData; +@property (nonatomic, strong) CDVLocationData* locationData; + +- (BOOL)hasHeadingSupport; +- (void)getLocation:(CDVInvokedUrlCommand*)command; +- (void)addWatch:(CDVInvokedUrlCommand*)command; +- (void)clearWatch:(CDVInvokedUrlCommand*)command; +- (void)returnLocationInfo:(NSString*)callbackId andKeepCallback:(BOOL)keepCallback; +- (void)returnLocationError:(NSUInteger)errorCode withMessage:(NSString*)message; +- (void)startLocation:(BOOL)enableHighAccuracy; + +- (void)locationManager:(CLLocationManager*)manager + didUpdateToLocation:(CLLocation*)newLocation + fromLocation:(CLLocation*)oldLocation; + +- (void)locationManager:(CLLocationManager*)manager + didFailWithError:(NSError*)error; + +- (BOOL)isLocationServicesEnabled; + +- (void)getHeading:(CDVInvokedUrlCommand*)command; +- (void)returnHeadingInfo:(NSString*)callbackId keepCallback:(BOOL)bRetain; +- (void)watchHeadingFilter:(CDVInvokedUrlCommand*)command; +- (void)stopHeading:(CDVInvokedUrlCommand*)command; +- (void)startHeadingWithFilter:(CLLocationDegrees)filter; +- (void)locationManager:(CLLocationManager*)manager + didUpdateHeading:(CLHeading*)heading; + +- (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager*)manager; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVLocation.m b/cordova/ios/CordovaLib/Classes/CDVLocation.m new file mode 100755 index 000000000..ed9ec26ab --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVLocation.m @@ -0,0 +1,623 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVLocation.h" +#import "NSArray+Comparisons.h" + +#pragma mark Constants + +#define kPGLocationErrorDomain @"kPGLocationErrorDomain" +#define kPGLocationDesiredAccuracyKey @"desiredAccuracy" +#define kPGLocationForcePromptKey @"forcePrompt" +#define kPGLocationDistanceFilterKey @"distanceFilter" +#define kPGLocationFrequencyKey @"frequency" + +#pragma mark - +#pragma mark Categories + +@interface NSError (JSONMethods) + +- (NSString*)JSONRepresentation; + +@end + +@interface CLLocation (JSONMethods) + +- (NSString*)JSONRepresentation; + +@end + +@interface CLHeading (JSONMethods) + +- (NSString*)JSONRepresentation; + +@end + +#pragma mark - +#pragma mark CDVHeadingData + +@implementation CDVHeadingData + +@synthesize headingStatus, headingInfo, headingCallbacks, headingFilter, headingTimestamp, timeout; +- (CDVHeadingData*)init +{ + self = (CDVHeadingData*)[super init]; + if (self) { + self.headingStatus = HEADINGSTOPPED; + self.headingInfo = nil; + self.headingCallbacks = nil; + self.headingFilter = nil; + self.headingTimestamp = nil; + self.timeout = 10; + } + return self; +} + +@end + +@implementation CDVLocationData + +@synthesize locationStatus, locationInfo, locationCallbacks, watchCallbacks; +- (CDVLocationData*)init +{ + self = (CDVLocationData*)[super init]; + if (self) { + self.locationInfo = nil; + self.locationCallbacks = nil; + self.watchCallbacks = nil; + } + return self; +} + +@end + +#pragma mark - +#pragma mark CDVLocation + +@implementation CDVLocation + +@synthesize locationManager, headingData, locationData; + +- (CDVPlugin*)initWithWebView:(UIWebView*)theWebView +{ + self = (CDVLocation*)[super initWithWebView:(UIWebView*)theWebView]; + if (self) { + self.locationManager = [[CLLocationManager alloc] init]; + self.locationManager.delegate = self; // Tells the location manager to send updates to this object + __locationStarted = NO; + __highAccuracyEnabled = NO; + self.headingData = nil; + self.locationData = nil; + } + return self; +} + +- (BOOL)hasHeadingSupport +{ + BOOL headingInstancePropertyAvailable = [self.locationManager respondsToSelector:@selector(headingAvailable)]; // iOS 3.x + BOOL headingClassPropertyAvailable = [CLLocationManager respondsToSelector:@selector(headingAvailable)]; // iOS 4.x + + if (headingInstancePropertyAvailable) { // iOS 3.x + return [(id)self.locationManager headingAvailable]; + } else if (headingClassPropertyAvailable) { // iOS 4.x + return [CLLocationManager headingAvailable]; + } else { // iOS 2.x + return NO; + } +} + +- (BOOL)isAuthorized +{ + BOOL authorizationStatusClassPropertyAvailable = [CLLocationManager respondsToSelector:@selector(authorizationStatus)]; // iOS 4.2+ + + if (authorizationStatusClassPropertyAvailable) { + NSUInteger authStatus = [CLLocationManager authorizationStatus]; + return (authStatus == kCLAuthorizationStatusAuthorized) || (authStatus == kCLAuthorizationStatusNotDetermined); + } + + // by default, assume YES (for iOS < 4.2) + return YES; +} + +- (BOOL)isLocationServicesEnabled +{ + BOOL locationServicesEnabledInstancePropertyAvailable = [self.locationManager respondsToSelector:@selector(locationServicesEnabled)]; // iOS 3.x + BOOL locationServicesEnabledClassPropertyAvailable = [CLLocationManager respondsToSelector:@selector(locationServicesEnabled)]; // iOS 4.x + + if (locationServicesEnabledClassPropertyAvailable) { // iOS 4.x + return [CLLocationManager locationServicesEnabled]; + } else if (locationServicesEnabledInstancePropertyAvailable) { // iOS 2.x, iOS 3.x + return [(id)self.locationManager locationServicesEnabled]; + } else { + return NO; + } +} + +- (void)startLocation:(BOOL)enableHighAccuracy +{ + if (![self isLocationServicesEnabled]) { + [self returnLocationError:PERMISSIONDENIED withMessage:@"Location services are not enabled."]; + return; + } + if (![self isAuthorized]) { + NSString* message = nil; + BOOL authStatusAvailable = [CLLocationManager respondsToSelector:@selector(authorizationStatus)]; // iOS 4.2+ + if (authStatusAvailable) { + NSUInteger code = [CLLocationManager authorizationStatus]; + if (code == kCLAuthorizationStatusNotDetermined) { + // could return POSITION_UNAVAILABLE but need to coordinate with other platforms + message = @"User undecided on application's use of location services."; + } else if (code == kCLAuthorizationStatusRestricted) { + message = @"Application's use of location services is restricted."; + } + } + // PERMISSIONDENIED is only PositionError that makes sense when authorization denied + [self returnLocationError:PERMISSIONDENIED withMessage:message]; + + return; + } + + // Tell the location manager to start notifying us of location updates. We + // first stop, and then start the updating to ensure we get at least one + // update, even if our location did not change. + [self.locationManager stopUpdatingLocation]; + [self.locationManager startUpdatingLocation]; + __locationStarted = YES; + if (enableHighAccuracy) { + __highAccuracyEnabled = YES; + // Set to distance filter to "none" - which should be the minimum for best results. + self.locationManager.distanceFilter = kCLDistanceFilterNone; + // Set desired accuracy to Best. + self.locationManager.desiredAccuracy = kCLLocationAccuracyBest; + } else { + __highAccuracyEnabled = NO; + // TODO: Set distance filter to 10 meters? and desired accuracy to nearest ten meters? arbitrary. + self.locationManager.distanceFilter = 10; + self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters; + } +} + +- (void)_stopLocation +{ + if (__locationStarted) { + if (![self isLocationServicesEnabled]) { + return; + } + + [self.locationManager stopUpdatingLocation]; + __locationStarted = NO; + __highAccuracyEnabled = NO; + } +} + +- (void)locationManager:(CLLocationManager*)manager + didUpdateToLocation:(CLLocation*)newLocation + fromLocation:(CLLocation*)oldLocation +{ + CDVLocationData* cData = self.locationData; + + cData.locationInfo = newLocation; + if (self.locationData.locationCallbacks.count > 0) { + for (NSString* callbackId in self.locationData.locationCallbacks) { + [self returnLocationInfo:callbackId andKeepCallback:NO]; + } + + [self.locationData.locationCallbacks removeAllObjects]; + } + if (self.locationData.watchCallbacks.count > 0) { + for (NSString* timerId in self.locationData.watchCallbacks) { + [self returnLocationInfo:[self.locationData.watchCallbacks objectForKey:timerId] andKeepCallback:YES]; + } + } else { + // No callbacks waiting on us anymore, turn off listening. + [self _stopLocation]; + } +} + +- (void)getLocation:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + BOOL enableHighAccuracy = [[command.arguments objectAtIndex:0] boolValue]; + + if ([self isLocationServicesEnabled] == NO) { + NSMutableDictionary* posError = [NSMutableDictionary dictionaryWithCapacity:2]; + [posError setObject:[NSNumber numberWithInt:PERMISSIONDENIED] forKey:@"code"]; + [posError setObject:@"Location services are disabled." forKey:@"message"]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:posError]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + if (!self.locationData) { + self.locationData = [[CDVLocationData alloc] init]; + } + CDVLocationData* lData = self.locationData; + if (!lData.locationCallbacks) { + lData.locationCallbacks = [NSMutableArray arrayWithCapacity:1]; + } + + if (!__locationStarted || (__highAccuracyEnabled != enableHighAccuracy)) { + // add the callbackId into the array so we can call back when get data + if (callbackId != nil) { + [lData.locationCallbacks addObject:callbackId]; + } + // Tell the location manager to start notifying us of heading updates + [self startLocation:enableHighAccuracy]; + } else { + [self returnLocationInfo:callbackId andKeepCallback:NO]; + } + } +} + +- (void)addWatch:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* timerId = [command.arguments objectAtIndex:0]; + BOOL enableHighAccuracy = [[command.arguments objectAtIndex:1] boolValue]; + + if (!self.locationData) { + self.locationData = [[CDVLocationData alloc] init]; + } + CDVLocationData* lData = self.locationData; + + if (!lData.watchCallbacks) { + lData.watchCallbacks = [NSMutableDictionary dictionaryWithCapacity:1]; + } + + // add the callbackId into the dictionary so we can call back whenever get data + [lData.watchCallbacks setObject:callbackId forKey:timerId]; + + if ([self isLocationServicesEnabled] == NO) { + NSMutableDictionary* posError = [NSMutableDictionary dictionaryWithCapacity:2]; + [posError setObject:[NSNumber numberWithInt:PERMISSIONDENIED] forKey:@"code"]; + [posError setObject:@"Location services are disabled." forKey:@"message"]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:posError]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + if (!__locationStarted || (__highAccuracyEnabled != enableHighAccuracy)) { + // Tell the location manager to start notifying us of location updates + [self startLocation:enableHighAccuracy]; + } + } +} + +- (void)clearWatch:(CDVInvokedUrlCommand*)command +{ + NSString* timerId = [command.arguments objectAtIndex:0]; + + if (self.locationData && self.locationData.watchCallbacks && [self.locationData.watchCallbacks objectForKey:timerId]) { + [self.locationData.watchCallbacks removeObjectForKey:timerId]; + } +} + +- (void)stopLocation:(CDVInvokedUrlCommand*)command +{ + [self _stopLocation]; +} + +- (void)returnLocationInfo:(NSString*)callbackId andKeepCallback:(BOOL)keepCallback +{ + CDVPluginResult* result = nil; + CDVLocationData* lData = self.locationData; + + if (lData && !lData.locationInfo) { + // return error + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:POSITIONUNAVAILABLE]; + } else if (lData && lData.locationInfo) { + CLLocation* lInfo = lData.locationInfo; + NSMutableDictionary* returnInfo = [NSMutableDictionary dictionaryWithCapacity:8]; + NSNumber* timestamp = [NSNumber numberWithDouble:([lInfo.timestamp timeIntervalSince1970] * 1000)]; + [returnInfo setObject:timestamp forKey:@"timestamp"]; + [returnInfo setObject:[NSNumber numberWithDouble:lInfo.speed] forKey:@"velocity"]; + [returnInfo setObject:[NSNumber numberWithDouble:lInfo.verticalAccuracy] forKey:@"altitudeAccuracy"]; + [returnInfo setObject:[NSNumber numberWithDouble:lInfo.horizontalAccuracy] forKey:@"accuracy"]; + [returnInfo setObject:[NSNumber numberWithDouble:lInfo.course] forKey:@"heading"]; + [returnInfo setObject:[NSNumber numberWithDouble:lInfo.altitude] forKey:@"altitude"]; + [returnInfo setObject:[NSNumber numberWithDouble:lInfo.coordinate.latitude] forKey:@"latitude"]; + [returnInfo setObject:[NSNumber numberWithDouble:lInfo.coordinate.longitude] forKey:@"longitude"]; + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:returnInfo]; + [result setKeepCallbackAsBool:keepCallback]; + } + if (result) { + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (void)returnLocationError:(NSUInteger)errorCode withMessage:(NSString*)message +{ + NSMutableDictionary* posError = [NSMutableDictionary dictionaryWithCapacity:2]; + + [posError setObject:[NSNumber numberWithInt:errorCode] forKey:@"code"]; + [posError setObject:message ? message:@"" forKey:@"message"]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:posError]; + + for (NSString* callbackId in self.locationData.locationCallbacks) { + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + + [self.locationData.locationCallbacks removeAllObjects]; + + for (NSString* callbackId in self.locationData.watchCallbacks) { + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +// called to get the current heading +// Will call location manager to startUpdatingHeading if necessary + +- (void)getHeading:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSDictionary* options = [command.arguments objectAtIndex:0 withDefault:nil]; + NSNumber* filter = [options valueForKey:@"filter"]; + + if (filter) { + [self watchHeadingFilter:command]; + return; + } + if ([self hasHeadingSupport] == NO) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:20]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + // heading retrieval does is not affected by disabling locationServices and authorization of app for location services + if (!self.headingData) { + self.headingData = [[CDVHeadingData alloc] init]; + } + CDVHeadingData* hData = self.headingData; + + if (!hData.headingCallbacks) { + hData.headingCallbacks = [NSMutableArray arrayWithCapacity:1]; + } + // add the callbackId into the array so we can call back when get data + [hData.headingCallbacks addObject:callbackId]; + + if ((hData.headingStatus != HEADINGRUNNING) && (hData.headingStatus != HEADINGERROR)) { + // Tell the location manager to start notifying us of heading updates + [self startHeadingWithFilter:0.2]; + } else { + [self returnHeadingInfo:callbackId keepCallback:NO]; + } + } +} + +// called to request heading updates when heading changes by a certain amount (filter) +- (void)watchHeadingFilter:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSDictionary* options = [command.arguments objectAtIndex:0 withDefault:nil]; + NSNumber* filter = [options valueForKey:@"filter"]; + CDVHeadingData* hData = self.headingData; + + if ([self hasHeadingSupport] == NO) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:20]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } else { + if (!hData) { + self.headingData = [[CDVHeadingData alloc] init]; + hData = self.headingData; + } + if (hData.headingStatus != HEADINGRUNNING) { + // Tell the location manager to start notifying us of heading updates + [self startHeadingWithFilter:[filter doubleValue]]; + } else { + // if already running check to see if due to existing watch filter + if (hData.headingFilter && ![hData.headingFilter isEqualToString:callbackId]) { + // new watch filter being specified + // send heading data one last time to clear old successCallback + [self returnHeadingInfo:hData.headingFilter keepCallback:NO]; + } + } + // save the new filter callback and update the headingFilter setting + hData.headingFilter = callbackId; + // check if need to stop and restart in order to change value??? + self.locationManager.headingFilter = [filter doubleValue]; + } +} + +- (void)returnHeadingInfo:(NSString*)callbackId keepCallback:(BOOL)bRetain +{ + CDVPluginResult* result = nil; + CDVHeadingData* hData = self.headingData; + + self.headingData.headingTimestamp = [NSDate date]; + + if (hData && (hData.headingStatus == HEADINGERROR)) { + // return error + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:0]; + } else if (hData && (hData.headingStatus == HEADINGRUNNING) && hData.headingInfo) { + // if there is heading info, return it + CLHeading* hInfo = hData.headingInfo; + NSMutableDictionary* returnInfo = [NSMutableDictionary dictionaryWithCapacity:4]; + NSNumber* timestamp = [NSNumber numberWithDouble:([hInfo.timestamp timeIntervalSince1970] * 1000)]; + [returnInfo setObject:timestamp forKey:@"timestamp"]; + [returnInfo setObject:[NSNumber numberWithDouble:hInfo.magneticHeading] forKey:@"magneticHeading"]; + id trueHeading = __locationStarted ? (id)[NSNumber numberWithDouble : hInfo.trueHeading] : (id)[NSNull null]; + [returnInfo setObject:trueHeading forKey:@"trueHeading"]; + [returnInfo setObject:[NSNumber numberWithDouble:hInfo.headingAccuracy] forKey:@"headingAccuracy"]; + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:returnInfo]; + [result setKeepCallbackAsBool:bRetain]; + } + if (result) { + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (void)stopHeading:(CDVInvokedUrlCommand*)command +{ + // CDVHeadingData* hData = self.headingData; + if (self.headingData && (self.headingData.headingStatus != HEADINGSTOPPED)) { + if (self.headingData.headingFilter) { + // callback one last time to clear callback + [self returnHeadingInfo:self.headingData.headingFilter keepCallback:NO]; + self.headingData.headingFilter = nil; + } + [self.locationManager stopUpdatingHeading]; + NSLog(@"heading STOPPED"); + self.headingData = nil; + } +} + +// helper method to check the orientation and start updating headings +- (void)startHeadingWithFilter:(CLLocationDegrees)filter +{ + // FYI UIDeviceOrientation and CLDeviceOrientation enums are currently the same + self.locationManager.headingOrientation = (CLDeviceOrientation)self.viewController.interfaceOrientation; + self.locationManager.headingFilter = filter; + [self.locationManager startUpdatingHeading]; + self.headingData.headingStatus = HEADINGSTARTING; +} + +- (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager*)manager +{ + return YES; +} + +- (void)locationManager:(CLLocationManager*)manager + didUpdateHeading:(CLHeading*)heading +{ + CDVHeadingData* hData = self.headingData; + + // normally we would clear the delegate to stop getting these notifications, but + // we are sharing a CLLocationManager to get location data as well, so we do a nil check here + // ideally heading and location should use their own CLLocationManager instances + if (hData == nil) { + return; + } + + // save the data for next call into getHeadingData + hData.headingInfo = heading; + BOOL bTimeout = NO; + if (!hData.headingFilter && hData.headingTimestamp) { + bTimeout = fabs([hData.headingTimestamp timeIntervalSinceNow]) > hData.timeout; + } + + if (hData.headingStatus == HEADINGSTARTING) { + hData.headingStatus = HEADINGRUNNING; // so returnHeading info will work + + // this is the first update + for (NSString* callbackId in hData.headingCallbacks) { + [self returnHeadingInfo:callbackId keepCallback:NO]; + } + + [hData.headingCallbacks removeAllObjects]; + } + if (hData.headingFilter) { + [self returnHeadingInfo:hData.headingFilter keepCallback:YES]; + } else if (bTimeout) { + [self stopHeading:nil]; + } + hData.headingStatus = HEADINGRUNNING; // to clear any error +} + +- (void)locationManager:(CLLocationManager*)manager didFailWithError:(NSError*)error +{ + NSLog(@"locationManager::didFailWithError %@", [error localizedFailureReason]); + + // Compass Error + if ([error code] == kCLErrorHeadingFailure) { + CDVHeadingData* hData = self.headingData; + if (hData) { + if (hData.headingStatus == HEADINGSTARTING) { + // heading error during startup - report error + for (NSString* callbackId in hData.headingCallbacks) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:0]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + + [hData.headingCallbacks removeAllObjects]; + } // else for frequency watches next call to getCurrentHeading will report error + if (hData.headingFilter) { + CDVPluginResult* resultFilter = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:0]; + [self.commandDelegate sendPluginResult:resultFilter callbackId:hData.headingFilter]; + } + hData.headingStatus = HEADINGERROR; + } + } + // Location Error + else { + CDVLocationData* lData = self.locationData; + if (lData && __locationStarted) { + // TODO: probably have to once over the various error codes and return one of: + // PositionError.PERMISSION_DENIED = 1; + // PositionError.POSITION_UNAVAILABLE = 2; + // PositionError.TIMEOUT = 3; + NSUInteger positionError = POSITIONUNAVAILABLE; + if (error.code == kCLErrorDenied) { + positionError = PERMISSIONDENIED; + } + [self returnLocationError:positionError withMessage:[error localizedDescription]]; + } + } + + [self.locationManager stopUpdatingLocation]; + __locationStarted = NO; +} + +- (void)dealloc +{ + self.locationManager.delegate = nil; +} + +- (void)onReset +{ + [self _stopLocation]; + [self.locationManager stopUpdatingHeading]; + self.headingData = nil; +} + +@end + +#pragma mark - +#pragma mark CLLocation(JSONMethods) + +@implementation CLLocation (JSONMethods) + +- (NSString*)JSONRepresentation +{ + return [NSString stringWithFormat: + @"{ timestamp: %.00f, \ + coords: { latitude: %f, longitude: %f, altitude: %.02f, heading: %.02f, speed: %.02f, accuracy: %.02f, altitudeAccuracy: %.02f } \ + }", + [self.timestamp timeIntervalSince1970] * 1000.0, + self.coordinate.latitude, + self.coordinate.longitude, + self.altitude, + self.course, + self.speed, + self.horizontalAccuracy, + self.verticalAccuracy + ]; +} + +@end + +#pragma mark NSError(JSONMethods) + +@implementation NSError (JSONMethods) + +- (NSString*)JSONRepresentation +{ + return [NSString stringWithFormat: + @"{ code: %d, message: '%@'}", + self.code, + [self localizedDescription] + ]; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVLogger.h b/cordova/ios/CordovaLib/Classes/CDVLogger.h new file mode 100755 index 000000000..eeba63ca4 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVLogger.h @@ -0,0 +1,26 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" + +@interface CDVLogger : CDVPlugin + +- (void)logLevel:(CDVInvokedUrlCommand*)command; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVLogger.m b/cordova/ios/CordovaLib/Classes/CDVLogger.m new file mode 100755 index 000000000..a37cf8ad3 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVLogger.m @@ -0,0 +1,38 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVLogger.h" +#import "CDV.h" + +@implementation CDVLogger + +/* log a message */ +- (void)logLevel:(CDVInvokedUrlCommand*)command +{ + id level = [command.arguments objectAtIndex:0]; + id message = [command.arguments objectAtIndex:1]; + + if ([level isEqualToString:@"LOG"]) { + NSLog(@"%@", message); + } else { + NSLog(@"%@: %@", level, message); + } +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVNotification.h b/cordova/ios/CordovaLib/Classes/CDVNotification.h new file mode 100755 index 000000000..5b5b89fa5 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVNotification.h @@ -0,0 +1,37 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import "CDVPlugin.h" + +@interface CDVNotification : CDVPlugin {} + +- (void)alert:(CDVInvokedUrlCommand*)command; +- (void)confirm:(CDVInvokedUrlCommand*)command; +- (void)prompt:(CDVInvokedUrlCommand*)command; +- (void)vibrate:(CDVInvokedUrlCommand*)command; + +@end + +@interface CDVAlertView : UIAlertView {} +@property (nonatomic, copy) NSString* callbackId; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVNotification.m b/cordova/ios/CordovaLib/Classes/CDVNotification.m new file mode 100755 index 000000000..821cb9f15 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVNotification.m @@ -0,0 +1,126 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVNotification.h" +#import "NSDictionary+Extensions.h" + +#define DIALOG_TYPE_ALERT @"alert" +#define DIALOG_TYPE_PROMPT @"prompt" + +@implementation CDVNotification + +/* + * showDialogWithMessage - Common method to instantiate the alert view for alert, confirm, and prompt notifications. + * Parameters: + * message The alert view message. + * title The alert view title. + * buttons The array of customized strings for the buttons. + * callbackId The commmand callback id. + * dialogType The type of alert view [alert | prompt]. + */ +- (void)showDialogWithMessage:(NSString*)message title:(NSString*)title buttons:(NSArray*)buttons callbackId:(NSString*)callbackId dialogType:(NSString*)dialogType +{ + CDVAlertView* alertView = [[CDVAlertView alloc] + initWithTitle:title + message:message + delegate:self + cancelButtonTitle:nil + otherButtonTitles:nil]; + + alertView.callbackId = callbackId; + + int count = [buttons count]; + + for (int n = 0; n < count; n++) { + [alertView addButtonWithTitle:[buttons objectAtIndex:n]]; + } + + if ([dialogType isEqualToString:DIALOG_TYPE_PROMPT]) { + alertView.alertViewStyle = UIAlertViewStylePlainTextInput; + } + + [alertView show]; +} + +- (void)alert:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* message = [command argumentAtIndex:0]; + NSString* title = [command argumentAtIndex:1]; + NSString* buttons = [command argumentAtIndex:2]; + + [self showDialogWithMessage:message title:title buttons:@[buttons] callbackId:callbackId dialogType:DIALOG_TYPE_ALERT]; +} + +- (void)confirm:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* message = [command argumentAtIndex:0]; + NSString* title = [command argumentAtIndex:1]; + NSArray* buttons = [command argumentAtIndex:2]; + + [self showDialogWithMessage:message title:title buttons:buttons callbackId:callbackId dialogType:DIALOG_TYPE_ALERT]; +} + +- (void)prompt:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* message = [command argumentAtIndex:0]; + NSString* title = [command argumentAtIndex:1]; + NSArray* buttons = [command argumentAtIndex:2]; + + [self showDialogWithMessage:message title:title buttons:buttons callbackId:callbackId dialogType:DIALOG_TYPE_PROMPT]; +} + +/** + * Callback invoked when an alert dialog's buttons are clicked. + */ +- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex +{ + CDVAlertView* cdvAlertView = (CDVAlertView*)alertView; + CDVPluginResult* result; + + // Determine what gets returned to JS based on the alert view type. + if (alertView.alertViewStyle == UIAlertViewStyleDefault) { + // For alert and confirm, return button index as int back to JS. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:buttonIndex + 1]; + } else { + // For prompt, return button index and input text back to JS. + NSString* value0 = [[alertView textFieldAtIndex:0] text]; + NSDictionary* info = @{ + @"buttonIndex":@(buttonIndex + 1), + @"input1":(value0 ? value0 : [NSNull null]) + }; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:info]; + } + [self.commandDelegate sendPluginResult:result callbackId:cdvAlertView.callbackId]; +} + +- (void)vibrate:(CDVInvokedUrlCommand*)command +{ + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); +} + +@end + +@implementation CDVAlertView + +@synthesize callbackId; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVPlugin.h b/cordova/ios/CordovaLib/Classes/CDVPlugin.h new file mode 100755 index 000000000..33ba1c4ba --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVPlugin.h @@ -0,0 +1,64 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import "CDVPluginResult.h" +#import "NSMutableArray+QueueAdditions.h" +#import "CDVCommandDelegate.h" + +extern NSString* const CDVPageDidLoadNotification; +extern NSString* const CDVPluginHandleOpenURLNotification; +extern NSString* const CDVPluginResetNotification; +extern NSString* const CDVLocalNotification; + +@interface CDVPlugin : NSObject {} + +@property (nonatomic, weak) UIWebView* webView; +@property (nonatomic, weak) UIViewController* viewController; +@property (nonatomic, weak) id commandDelegate; + +@property (readonly, assign) BOOL hasPendingOperation; + +- (CDVPlugin*)initWithWebView:(UIWebView*)theWebView; +- (void)pluginInitialize; + +- (void)handleOpenURL:(NSNotification*)notification; +- (void)onAppTerminate; +- (void)onMemoryWarning; +- (void)onReset; +- (void)dispose; + +/* + // see initWithWebView implementation + - (void) onPause {} + - (void) onResume {} + - (void) onOrientationWillChange {} + - (void) onOrientationDidChange {} + - (void)didReceiveLocalNotification:(NSNotification *)notification; + */ + +- (id)appDelegate; + +// TODO(agrieve): Deprecate these in favour of using CDVCommandDelegate directly. +- (NSString*)writeJavascript:(NSString*)javascript; +- (NSString*)success:(CDVPluginResult*)pluginResult callbackId:(NSString*)callbackId; +- (NSString*)error:(CDVPluginResult*)pluginResult callbackId:(NSString*)callbackId; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVPlugin.m b/cordova/ios/CordovaLib/Classes/CDVPlugin.m new file mode 100755 index 000000000..8c932a029 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVPlugin.m @@ -0,0 +1,152 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPlugin.h" + +NSString* const CDVPageDidLoadNotification = @"CDVPageDidLoadNotification"; +NSString* const CDVPluginHandleOpenURLNotification = @"CDVPluginHandleOpenURLNotification"; +NSString* const CDVPluginResetNotification = @"CDVPluginResetNotification"; +NSString* const CDVLocalNotification = @"CDVLocalNotification"; + +@interface CDVPlugin () + +@property (readwrite, assign) BOOL hasPendingOperation; + +@end + +@implementation CDVPlugin +@synthesize webView, viewController, commandDelegate, hasPendingOperation; + +// Do not override these methods. Use pluginInitialize instead. +- (CDVPlugin*)initWithWebView:(UIWebView*)theWebView settings:(NSDictionary*)classSettings +{ + return [self initWithWebView:theWebView]; +} + +- (CDVPlugin*)initWithWebView:(UIWebView*)theWebView +{ + self = [super init]; + if (self) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppTerminate) name:UIApplicationWillTerminateNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleOpenURL:) name:CDVPluginHandleOpenURLNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onReset) name:CDVPluginResetNotification object:theWebView]; + + self.webView = theWebView; + } + return self; +} + +- (void)pluginInitialize +{ + // You can listen to more app notifications, see: + // http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIApplication_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006728-CH3-DontLinkElementID_4 + + // NOTE: if you want to use these, make sure you uncomment the corresponding notification handler + + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPause) name:UIApplicationDidEnterBackgroundNotification object:nil]; + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume) name:UIApplicationWillEnterForegroundNotification object:nil]; + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOrientationWillChange) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil]; + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOrientationDidChange) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; + + // Added in 2.3.0 + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveLocalNotification:) name:CDVLocalNotification object:nil]; + + // Added in 2.5.0 + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pageDidLoad:) name:CDVPageDidLoadNotification object:self.webView]; +} + +- (void)dispose +{ + viewController = nil; + commandDelegate = nil; + webView = nil; +} + +/* +// NOTE: for onPause and onResume, calls into JavaScript must not call or trigger any blocking UI, like alerts +- (void) onPause {} +- (void) onResume {} +- (void) onOrientationWillChange {} +- (void) onOrientationDidChange {} +*/ + +/* NOTE: calls into JavaScript must not call or trigger any blocking UI, like alerts */ +- (void)handleOpenURL:(NSNotification*)notification +{ + // override to handle urls sent to your app + // register your url schemes in your App-Info.plist + + NSURL* url = [notification object]; + + if ([url isKindOfClass:[NSURL class]]) { + /* Do your thing! */ + } +} + +/* NOTE: calls into JavaScript must not call or trigger any blocking UI, like alerts */ +- (void)onAppTerminate +{ + // override this if you need to do any cleanup on app exit +} + +- (void)onMemoryWarning +{ + // override to remove caches, etc +} + +- (void)onReset +{ + // Override to cancel any long-running requests when the WebView navigates or refreshes. +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; // this will remove all notification unless added using addObserverForName:object:queue:usingBlock: +} + +- (id)appDelegate +{ + return [[UIApplication sharedApplication] delegate]; +} + +- (NSString*)writeJavascript:(NSString*)javascript +{ + return [self.webView stringByEvaluatingJavaScriptFromString:javascript]; +} + +- (NSString*)success:(CDVPluginResult*)pluginResult callbackId:(NSString*)callbackId +{ + [self.commandDelegate evalJs:[pluginResult toSuccessCallbackString:callbackId]]; + return @""; +} + +- (NSString*)error:(CDVPluginResult*)pluginResult callbackId:(NSString*)callbackId +{ + [self.commandDelegate evalJs:[pluginResult toErrorCallbackString:callbackId]]; + return @""; +} + +// default implementation does nothing, ideally, we are not registered for notification if we aren't going to do anything. +// - (void)didReceiveLocalNotification:(NSNotification *)notification +// { +// // UILocalNotification* localNotification = [notification object]; // get the payload as a LocalNotification +// } + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVPluginResult.h b/cordova/ios/CordovaLib/Classes/CDVPluginResult.h new file mode 100755 index 000000000..11b537736 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVPluginResult.h @@ -0,0 +1,68 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +typedef enum { + CDVCommandStatus_NO_RESULT = 0, + CDVCommandStatus_OK, + CDVCommandStatus_CLASS_NOT_FOUND_EXCEPTION, + CDVCommandStatus_ILLEGAL_ACCESS_EXCEPTION, + CDVCommandStatus_INSTANTIATION_EXCEPTION, + CDVCommandStatus_MALFORMED_URL_EXCEPTION, + CDVCommandStatus_IO_EXCEPTION, + CDVCommandStatus_INVALID_ACTION, + CDVCommandStatus_JSON_EXCEPTION, + CDVCommandStatus_ERROR +} CDVCommandStatus; + +@interface CDVPluginResult : NSObject {} + +@property (nonatomic, strong, readonly) NSNumber* status; +@property (nonatomic, strong, readonly) id message; +@property (nonatomic, strong) NSNumber* keepCallback; +// This property can be used to scope the lifetime of another object. For example, +// Use it to store the associated NSData when `message` is created using initWithBytesNoCopy. +@property (nonatomic, strong) id associatedObject; + +- (CDVPluginResult*)init; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsString:(NSString*)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArray:(NSArray*)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsInt:(int)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDouble:(double)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsBool:(BOOL)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDictionary:(NSDictionary*)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArrayBuffer:(NSData*)theMessage; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsMultipart:(NSArray*)theMessages; ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageToErrorObject:(int)errorCode; + ++ (void)setVerbose:(BOOL)verbose; ++ (BOOL)isVerbose; + +- (void)setKeepCallbackAsBool:(BOOL)bKeepCallback; + +- (NSString*)argumentsAsJSON; + +// These methods are used by the legacy plugin return result method +- (NSString*)toJSONString; +- (NSString*)toSuccessCallbackString:(NSString*)callbackId; +- (NSString*)toErrorCallbackString:(NSString*)callbackId; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVPluginResult.m b/cordova/ios/CordovaLib/Classes/CDVPluginResult.m new file mode 100755 index 000000000..af7c528be --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVPluginResult.m @@ -0,0 +1,224 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVPluginResult.h" +#import "CDVJSON.h" +#import "CDVDebug.h" +#import "NSData+Base64.h" + +@interface CDVPluginResult () + +- (CDVPluginResult*)initWithStatus:(CDVCommandStatus)statusOrdinal message:(id)theMessage; + +@end + +@implementation CDVPluginResult +@synthesize status, message, keepCallback, associatedObject; + +static NSArray* org_apache_cordova_CommandStatusMsgs; + +id messageFromArrayBuffer(NSData* data) +{ + return @{ + @"CDVType" : @"ArrayBuffer", + @"data" :[data base64EncodedString] + }; +} + +id massageMessage(id message) +{ + if ([message isKindOfClass:[NSData class]]) { + return messageFromArrayBuffer(message); + } + return message; +} + +id messageFromMultipart(NSArray* theMessages) +{ + NSMutableArray* messages = [NSMutableArray arrayWithArray:theMessages]; + + for (NSUInteger i = 0; i < messages.count; ++i) { + [messages replaceObjectAtIndex:i withObject:massageMessage([messages objectAtIndex:i])]; + } + + return @{ + @"CDVType" : @"MultiPart", + @"messages" : messages + }; +} + ++ (void)initialize +{ + org_apache_cordova_CommandStatusMsgs = [[NSArray alloc] initWithObjects:@"No result", + @"OK", + @"Class not found", + @"Illegal access", + @"Instantiation error", + @"Malformed url", + @"IO error", + @"Invalid action", + @"JSON error", + @"Error", + nil]; +} + +- (CDVPluginResult*)init +{ + return [self initWithStatus:CDVCommandStatus_NO_RESULT message:nil]; +} + +- (CDVPluginResult*)initWithStatus:(CDVCommandStatus)statusOrdinal message:(id)theMessage +{ + self = [super init]; + if (self) { + status = [NSNumber numberWithInt:statusOrdinal]; + message = theMessage; + keepCallback = [NSNumber numberWithBool:NO]; + } + return self; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal +{ + return [[self alloc] initWithStatus:statusOrdinal message:[org_apache_cordova_CommandStatusMsgs objectAtIndex:statusOrdinal]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsString:(NSString*)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:theMessage]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArray:(NSArray*)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:theMessage]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsInt:(int)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithInt:theMessage]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDouble:(double)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithDouble:theMessage]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsBool:(BOOL)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:[NSNumber numberWithBool:theMessage]]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsDictionary:(NSDictionary*)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:theMessage]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsArrayBuffer:(NSData*)theMessage +{ + return [[self alloc] initWithStatus:statusOrdinal message:messageFromArrayBuffer(theMessage)]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageAsMultipart:(NSArray*)theMessages +{ + return [[self alloc] initWithStatus:statusOrdinal message:messageFromMultipart(theMessages)]; +} + ++ (CDVPluginResult*)resultWithStatus:(CDVCommandStatus)statusOrdinal messageToErrorObject:(int)errorCode +{ + NSDictionary* errDict = @{@"code" :[NSNumber numberWithInt:errorCode]}; + + return [[self alloc] initWithStatus:statusOrdinal message:errDict]; +} + +- (void)setKeepCallbackAsBool:(BOOL)bKeepCallback +{ + [self setKeepCallback:[NSNumber numberWithBool:bKeepCallback]]; +} + +- (NSString*)argumentsAsJSON +{ + id arguments = (self.message == nil ? [NSNull null] : self.message); + NSArray* argumentsWrappedInArray = [NSArray arrayWithObject:arguments]; + + NSString* argumentsJSON = [argumentsWrappedInArray JSONString]; + + argumentsJSON = [argumentsJSON substringWithRange:NSMakeRange(1, [argumentsJSON length] - 2)]; + + return argumentsJSON; +} + +// These methods are used by the legacy plugin return result method +- (NSString*)toJSONString +{ + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: + self.status, @"status", + self.message ? self. message:[NSNull null], @"message", + self.keepCallback, @"keepCallback", + nil]; + + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:dict + options:NSJSONWritingPrettyPrinted + error:&error]; + NSString* resultString = nil; + + if (error != nil) { + NSLog(@"toJSONString error: %@", [error localizedDescription]); + } else { + resultString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } + + if ([[self class] isVerbose]) { + NSLog(@"PluginResult:toJSONString - %@", resultString); + } + return resultString; +} + +- (NSString*)toSuccessCallbackString:(NSString*)callbackId +{ + NSString* successCB = [NSString stringWithFormat:@"cordova.callbackSuccess('%@',%@);", callbackId, [self toJSONString]]; + + if ([[self class] isVerbose]) { + NSLog(@"PluginResult toSuccessCallbackString: %@", successCB); + } + return successCB; +} + +- (NSString*)toErrorCallbackString:(NSString*)callbackId +{ + NSString* errorCB = [NSString stringWithFormat:@"cordova.callbackError('%@',%@);", callbackId, [self toJSONString]]; + + if ([[self class] isVerbose]) { + NSLog(@"PluginResult toErrorCallbackString: %@", errorCB); + } + return errorCB; +} + +static BOOL gIsVerbose = NO; ++ (void)setVerbose:(BOOL)verbose +{ + gIsVerbose = verbose; +} + ++ (BOOL)isVerbose +{ + return gIsVerbose; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVReachability.h b/cordova/ios/CordovaLib/Classes/CDVReachability.h new file mode 100755 index 000000000..01a95c355 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVReachability.h @@ -0,0 +1,85 @@ +/* + + File: Reachability.h + Abstract: Basic demonstration of how to use the SystemConfiguration Reachability APIs. + Version: 2.2 + + Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. + ("Apple") in consideration of your agreement to the following terms, and your + use, installation, modification or redistribution of this Apple software + constitutes acceptance of these terms. If you do not agree with these terms, + please do not use, install, modify or redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and subject + to these terms, Apple grants you a personal, non-exclusive license, under + Apple's copyrights in this original Apple software (the "Apple Software"), to + use, reproduce, modify and redistribute the Apple Software, with or without + modifications, in source and/or binary forms; provided that if you redistribute + the Apple Software in its entirety and without modifications, you must retain + this notice and the following text and disclaimers in all such redistributions + of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may be used + to endorse or promote products derived from the Apple Software without specific + prior written permission from Apple. Except as expressly stated in this notice, + no other rights or licenses, express or implied, are granted by Apple herein, + including but not limited to any patent rights that may be infringed by your + derivative works or by other works in which the Apple Software may be + incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO + WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED + WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN + COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR + DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF + CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF + APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Copyright (C) 2010 Apple Inc. All Rights Reserved. + +*/ + +#import +#import +#import + +typedef enum { + NotReachable = 0, + ReachableViaWWAN, // this value has been swapped with ReachableViaWiFi for Cordova backwards compat. reasons + ReachableViaWiFi // this value has been swapped with ReachableViaWWAN for Cordova backwards compat. reasons +} NetworkStatus; +#define kReachabilityChangedNotification @"kNetworkReachabilityChangedNotification" + +@interface CDVReachability : NSObject +{ + BOOL localWiFiRef; + SCNetworkReachabilityRef reachabilityRef; +} + +// reachabilityWithHostName- Use to check the reachability of a particular host name. ++ (CDVReachability*)reachabilityWithHostName:(NSString*)hostName; + +// reachabilityWithAddress- Use to check the reachability of a particular IP address. ++ (CDVReachability*)reachabilityWithAddress:(const struct sockaddr_in*)hostAddress; + +// reachabilityForInternetConnection- checks whether the default route is available. +// Should be used by applications that do not connect to a particular host ++ (CDVReachability*)reachabilityForInternetConnection; + +// reachabilityForLocalWiFi- checks whether a local wifi connection is available. ++ (CDVReachability*)reachabilityForLocalWiFi; + +// Start listening for reachability notifications on the current run loop +- (BOOL)startNotifier; +- (void)stopNotifier; + +- (NetworkStatus)currentReachabilityStatus; +// WWAN may be available, but not active until a connection has been established. +// WiFi may require a connection for VPN on Demand. +- (BOOL)connectionRequired; +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVReachability.m b/cordova/ios/CordovaLib/Classes/CDVReachability.m new file mode 100755 index 000000000..89f4ec9b7 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVReachability.m @@ -0,0 +1,260 @@ +/* + + File: Reachability.m + Abstract: Basic demonstration of how to use the SystemConfiguration Reachability APIs. + Version: 2.2 + + Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. + ("Apple") in consideration of your agreement to the following terms, and your + use, installation, modification or redistribution of this Apple software + constitutes acceptance of these terms. If you do not agree with these terms, + please do not use, install, modify or redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and subject + to these terms, Apple grants you a personal, non-exclusive license, under + Apple's copyrights in this original Apple software (the "Apple Software"), to + use, reproduce, modify and redistribute the Apple Software, with or without + modifications, in source and/or binary forms; provided that if you redistribute + the Apple Software in its entirety and without modifications, you must retain + this notice and the following text and disclaimers in all such redistributions + of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may be used + to endorse or promote products derived from the Apple Software without specific + prior written permission from Apple. Except as expressly stated in this notice, + no other rights or licenses, express or implied, are granted by Apple herein, + including but not limited to any patent rights that may be infringed by your + derivative works or by other works in which the Apple Software may be + incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO + WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED + WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN + COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR + DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF + CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF + APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Copyright (C) 2010 Apple Inc. All Rights Reserved. + +*/ + +#import +#import +#import +#import +#import +#import + +#import + +#import "CDVReachability.h" + +#define kShouldPrintReachabilityFlags 0 + +static void CDVPrintReachabilityFlags(SCNetworkReachabilityFlags flags, const char* comment) +{ +#if kShouldPrintReachabilityFlags + NSLog(@"Reachability Flag Status: %c%c %c%c%c%c%c%c%c %s\n", + (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', + (flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-', + + (flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', + (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', + (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', + (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-', + comment + ); +#endif +} + +@implementation CDVReachability + +static void CDVReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) +{ +#pragma unused (target, flags) + // NSCAssert(info != NULL, @"info was NULL in ReachabilityCallback"); + // NSCAssert([(NSObject*) info isKindOfClass: [Reachability class]], @"info was wrong class in ReachabilityCallback"); + + // Converted the asserts above to conditionals, with safe return from the function + if (info == NULL) { + NSLog(@"info was NULL in ReachabilityCallback"); + return; + } + + if (![(__bridge NSObject*)info isKindOfClass :[CDVReachability class]]) { + NSLog(@"info was wrong class in ReachabilityCallback"); + return; + } + + // We're on the main RunLoop, so an NSAutoreleasePool is not necessary, but is added defensively + // in case someon uses the Reachability object in a different thread. + @autoreleasepool { + CDVReachability* noteObject = (__bridge CDVReachability*)info; + // Post a notification to notify the client that the network reachability changed. + [[NSNotificationCenter defaultCenter] postNotificationName:kReachabilityChangedNotification object:noteObject]; + } +} + +- (BOOL)startNotifier +{ + BOOL retVal = NO; + SCNetworkReachabilityContext context = {0, (__bridge void*)(self), NULL, NULL, NULL}; + + if (SCNetworkReachabilitySetCallback(reachabilityRef, CDVReachabilityCallback, &context)) { + if (SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)) { + retVal = YES; + } + } + return retVal; +} + +- (void)stopNotifier +{ + if (reachabilityRef != NULL) { + SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + } +} + +- (void)dealloc +{ + [self stopNotifier]; + if (reachabilityRef != NULL) { + CFRelease(reachabilityRef); + } +} + ++ (CDVReachability*)reachabilityWithHostName:(NSString*)hostName; +{ + CDVReachability* retVal = NULL; + SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(NULL, [hostName UTF8String]); + if (reachability != NULL) { + retVal = [[self alloc] init]; + if (retVal != NULL) { + retVal->reachabilityRef = reachability; + retVal->localWiFiRef = NO; + } + } + return retVal; +} + ++ (CDVReachability*)reachabilityWithAddress:(const struct sockaddr_in*)hostAddress; +{ + SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress); + CDVReachability* retVal = NULL; + if (reachability != NULL) { + retVal = [[self alloc] init]; + if (retVal != NULL) { + retVal->reachabilityRef = reachability; + retVal->localWiFiRef = NO; + } + } + return retVal; +} + ++ (CDVReachability*)reachabilityForInternetConnection; +{ + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + return [self reachabilityWithAddress:&zeroAddress]; +} + ++ (CDVReachability*)reachabilityForLocalWiFi; +{ + struct sockaddr_in localWifiAddress; + bzero(&localWifiAddress, sizeof(localWifiAddress)); + localWifiAddress.sin_len = sizeof(localWifiAddress); + localWifiAddress.sin_family = AF_INET; + // IN_LINKLOCALNETNUM is defined in as 169.254.0.0 + localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM); + CDVReachability* retVal = [self reachabilityWithAddress:&localWifiAddress]; + if (retVal != NULL) { + retVal->localWiFiRef = YES; + } + return retVal; +} + +#pragma mark Network Flag Handling + +- (NetworkStatus)localWiFiStatusForFlags:(SCNetworkReachabilityFlags)flags +{ + CDVPrintReachabilityFlags(flags, "localWiFiStatusForFlags"); + + BOOL retVal = NotReachable; + if ((flags & kSCNetworkReachabilityFlagsReachable) && (flags & kSCNetworkReachabilityFlagsIsDirect)) { + retVal = ReachableViaWiFi; + } + return retVal; +} + +- (NetworkStatus)networkStatusForFlags:(SCNetworkReachabilityFlags)flags +{ + CDVPrintReachabilityFlags(flags, "networkStatusForFlags"); + if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) { + // if target host is not reachable + return NotReachable; + } + + BOOL retVal = NotReachable; + + if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) { + // if target host is reachable and no connection is required + // then we'll assume (for now) that your on Wi-Fi + retVal = ReachableViaWiFi; + } + + if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand) != 0) || + ((flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0))) { + // ... and the connection is on-demand (or on-traffic) if the + // calling application is using the CFSocketStream or higher APIs + + if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) { + // ... and no [user] intervention is needed + retVal = ReachableViaWiFi; + } + } + + if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) { + // ... but WWAN connections are OK if the calling application + // is using the CFNetwork (CFSocketStream?) APIs. + retVal = ReachableViaWWAN; + } + return retVal; +} + +- (BOOL)connectionRequired; +{ + NSAssert(reachabilityRef != NULL, @"connectionRequired called with NULL reachabilityRef"); + SCNetworkReachabilityFlags flags; + if (SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) { + return flags & kSCNetworkReachabilityFlagsConnectionRequired; + } + return NO; +} + +- (NetworkStatus)currentReachabilityStatus +{ + NSAssert(reachabilityRef != NULL, @"currentNetworkStatus called with NULL reachabilityRef"); + NetworkStatus retVal = NotReachable; + SCNetworkReachabilityFlags flags; + if (SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) { + if (localWiFiRef) { + retVal = [self localWiFiStatusForFlags:flags]; + } else { + retVal = [self networkStatusForFlags:flags]; + } + } + return retVal; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVScreenOrientationDelegate.h b/cordova/ios/CordovaLib/Classes/CDVScreenOrientationDelegate.h new file mode 100755 index 000000000..7226205a5 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVScreenOrientationDelegate.h @@ -0,0 +1,28 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@protocol CDVScreenOrientationDelegate + +- (NSUInteger)supportedInterfaceOrientations; +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; +- (BOOL)shouldAutorotate; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVSound.h b/cordova/ios/CordovaLib/Classes/CDVSound.h new file mode 100755 index 000000000..8dcf98e01 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVSound.h @@ -0,0 +1,116 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import + +#import "CDVPlugin.h" + +enum CDVMediaError { + MEDIA_ERR_ABORTED = 1, + MEDIA_ERR_NETWORK = 2, + MEDIA_ERR_DECODE = 3, + MEDIA_ERR_NONE_SUPPORTED = 4 +}; +typedef NSUInteger CDVMediaError; + +enum CDVMediaStates { + MEDIA_NONE = 0, + MEDIA_STARTING = 1, + MEDIA_RUNNING = 2, + MEDIA_PAUSED = 3, + MEDIA_STOPPED = 4 +}; +typedef NSUInteger CDVMediaStates; + +enum CDVMediaMsg { + MEDIA_STATE = 1, + MEDIA_DURATION = 2, + MEDIA_POSITION = 3, + MEDIA_ERROR = 9 +}; +typedef NSUInteger CDVMediaMsg; + +@interface CDVAudioPlayer : AVAudioPlayer +{ + NSString* mediaId; +} +@property (nonatomic, copy) NSString* mediaId; +@end + +@interface CDVAudioRecorder : AVAudioRecorder +{ + NSString* mediaId; +} +@property (nonatomic, copy) NSString* mediaId; +@end + +@interface CDVAudioFile : NSObject +{ + NSString* resourcePath; + NSURL* resourceURL; + CDVAudioPlayer* player; + CDVAudioRecorder* recorder; + NSNumber* volume; +} + +@property (nonatomic, strong) NSString* resourcePath; +@property (nonatomic, strong) NSURL* resourceURL; +@property (nonatomic, strong) CDVAudioPlayer* player; +@property (nonatomic, strong) NSNumber* volume; + +@property (nonatomic, strong) CDVAudioRecorder* recorder; + +@end + +@interface CDVSound : CDVPlugin +{ + NSMutableDictionary* soundCache; + AVAudioSession* avSession; +} +@property (nonatomic, strong) NSMutableDictionary* soundCache; +@property (nonatomic, strong) AVAudioSession* avSession; + +- (void)startPlayingAudio:(CDVInvokedUrlCommand*)command; +- (void)pausePlayingAudio:(CDVInvokedUrlCommand*)command; +- (void)stopPlayingAudio:(CDVInvokedUrlCommand*)command; +- (void)seekToAudio:(CDVInvokedUrlCommand*)command; +- (void)release:(CDVInvokedUrlCommand*)command; +- (void)getCurrentPositionAudio:(CDVInvokedUrlCommand*)command; + +- (BOOL)hasAudioSession; + +// helper methods +- (NSURL*)urlForRecording:(NSString*)resourcePath; +- (NSURL*)urlForPlaying:(NSString*)resourcePath; +- (NSURL*)urlForResource:(NSString*)resourcePath CDV_DEPRECATED(2.5, "Use specific api for playing or recording"); + +- (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId CDV_DEPRECATED(2.5, "Use updated audioFileForResource api"); + +- (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord; +- (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId; +- (NSString*)createMediaErrorWithCode:(CDVMediaError)code message:(NSString*)message; + +- (void)startRecordingAudio:(CDVInvokedUrlCommand*)command; +- (void)stopRecordingAudio:(CDVInvokedUrlCommand*)command; + +- (void)setVolume:(CDVInvokedUrlCommand*)command; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVSound.m b/cordova/ios/CordovaLib/Classes/CDVSound.m new file mode 100755 index 000000000..71eab5915 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVSound.m @@ -0,0 +1,702 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVSound.h" +#import "NSArray+Comparisons.h" +#import "CDVJSON.h" + +#define DOCUMENTS_SCHEME_PREFIX @"documents://" +#define HTTP_SCHEME_PREFIX @"http://" +#define HTTPS_SCHEME_PREFIX @"https://" +#define RECORDING_WAV @"wav" + +@implementation CDVSound + +@synthesize soundCache, avSession; + +- (NSURL*)urlForResource:(NSString*)resourcePath +{ + NSURL* resourceURL = nil; + NSString* filePath = nil; + + // first try to find HTTP:// or Documents:// resources + + if ([resourcePath hasPrefix:HTTP_SCHEME_PREFIX] || [resourcePath hasPrefix:HTTPS_SCHEME_PREFIX]) { + // if it is a http url, use it + NSLog(@"Will use resource '%@' from the Internet.", resourcePath); + resourceURL = [NSURL URLWithString:resourcePath]; + } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { + NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; + NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); + } else { + // attempt to find file path in www directory + filePath = [self.commandDelegate pathForResource:resourcePath]; + if (filePath != nil) { + NSLog(@"Found resource '%@' in the web folder.", filePath); + } else { + filePath = resourcePath; + NSLog(@"Will attempt to use file resource '%@'", filePath); + } + } + // check that file exists for all but HTTP_SHEME_PREFIX + if (filePath != nil) { + // try to access file + NSFileManager* fMgr = [[NSFileManager alloc] init]; + if (![fMgr fileExistsAtPath:filePath]) { + resourceURL = nil; + NSLog(@"Unknown resource '%@'", resourcePath); + } else { + // it's a valid file url, use it + resourceURL = [NSURL fileURLWithPath:filePath]; + } + } + return resourceURL; +} + +// Maps a url for a resource path for recording +- (NSURL*)urlForRecording:(NSString*)resourcePath +{ + NSURL* resourceURL = nil; + NSString* filePath = nil; + NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + + // first check for correct extension + if ([[resourcePath pathExtension] caseInsensitiveCompare:RECORDING_WAV] != NSOrderedSame) { + resourceURL = nil; + NSLog(@"Resource for recording must have %@ extension", RECORDING_WAV); + } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { + // try to find Documents:// resources + filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; + NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); + } else { + // if resourcePath is not from FileSystem put in tmp dir, else attempt to use provided resource path + NSString* tmpPath = [NSTemporaryDirectory()stringByStandardizingPath]; + BOOL isTmp = [resourcePath rangeOfString:tmpPath].location != NSNotFound; + BOOL isDoc = [resourcePath rangeOfString:docsPath].location != NSNotFound; + if (!isTmp && !isDoc) { + // put in temp dir + filePath = [NSString stringWithFormat:@"%@/%@", tmpPath, resourcePath]; + } else { + filePath = resourcePath; + } + } + + if (filePath != nil) { + // create resourceURL + resourceURL = [NSURL fileURLWithPath:filePath]; + } + return resourceURL; +} + +// Maps a url for a resource path for playing +// "Naked" resource paths are assumed to be from the www folder as its base +- (NSURL*)urlForPlaying:(NSString*)resourcePath +{ + NSURL* resourceURL = nil; + NSString* filePath = nil; + + // first try to find HTTP:// or Documents:// resources + + if ([resourcePath hasPrefix:HTTP_SCHEME_PREFIX] || [resourcePath hasPrefix:HTTPS_SCHEME_PREFIX]) { + // if it is a http url, use it + NSLog(@"Will use resource '%@' from the Internet.", resourcePath); + resourceURL = [NSURL URLWithString:resourcePath]; + } else if ([resourcePath hasPrefix:DOCUMENTS_SCHEME_PREFIX]) { + NSString* docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + filePath = [resourcePath stringByReplacingOccurrencesOfString:DOCUMENTS_SCHEME_PREFIX withString:[NSString stringWithFormat:@"%@/", docsPath]]; + NSLog(@"Will use resource '%@' from the documents folder with path = %@", resourcePath, filePath); + } else { + // attempt to find file path in www directory or LocalFileSystem.TEMPORARY directory + filePath = [self.commandDelegate pathForResource:resourcePath]; + if (filePath == nil) { + // see if this exists in the documents/temp directory from a previous recording + NSString* testPath = [NSString stringWithFormat:@"%@/%@", [NSTemporaryDirectory()stringByStandardizingPath], resourcePath]; + if ([[NSFileManager defaultManager] fileExistsAtPath:testPath]) { + // inefficient as existence will be checked again below but only way to determine if file exists from previous recording + filePath = testPath; + NSLog(@"Will attempt to use file resource from LocalFileSystem.TEMPORARY directory"); + } else { + // attempt to use path provided + filePath = resourcePath; + NSLog(@"Will attempt to use file resource '%@'", filePath); + } + } else { + NSLog(@"Found resource '%@' in the web folder.", filePath); + } + } + // check that file exists for all but HTTP_SHEME_PREFIX + if (filePath != nil) { + // create resourceURL + resourceURL = [NSURL fileURLWithPath:filePath]; + // try to access file + NSFileManager* fMgr = [NSFileManager defaultManager]; + if (![fMgr fileExistsAtPath:filePath]) { + resourceURL = nil; + NSLog(@"Unknown resource '%@'", resourcePath); + } + } + + return resourceURL; +} + +- (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId +{ + // will maintain backwards compatibility with original implementation + return [self audioFileForResource:resourcePath withId:mediaId doValidation:YES forRecording:NO]; +} + +// Creates or gets the cached audio file resource object +- (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord +{ + BOOL bError = NO; + CDVMediaError errcode = MEDIA_ERR_NONE_SUPPORTED; + NSString* errMsg = @""; + NSString* jsString = nil; + CDVAudioFile* audioFile = nil; + NSURL* resourceURL = nil; + + if ([self soundCache] == nil) { + [self setSoundCache:[NSMutableDictionary dictionaryWithCapacity:1]]; + } else { + audioFile = [[self soundCache] objectForKey:mediaId]; + } + if (audioFile == nil) { + // validate resourcePath and create + if ((resourcePath == nil) || ![resourcePath isKindOfClass:[NSString class]] || [resourcePath isEqualToString:@""]) { + bError = YES; + errcode = MEDIA_ERR_ABORTED; + errMsg = @"invalid media src argument"; + } else { + audioFile = [[CDVAudioFile alloc] init]; + audioFile.resourcePath = resourcePath; + audioFile.resourceURL = nil; // validate resourceURL when actually play or record + [[self soundCache] setObject:audioFile forKey:mediaId]; + } + } + if (bValidate && (audioFile.resourceURL == nil)) { + if (bRecord) { + resourceURL = [self urlForRecording:resourcePath]; + } else { + resourceURL = [self urlForPlaying:resourcePath]; + } + if (resourceURL == nil) { + bError = YES; + errcode = MEDIA_ERR_ABORTED; + errMsg = [NSString stringWithFormat:@"Cannot use audio file from resource '%@'", resourcePath]; + } else { + audioFile.resourceURL = resourceURL; + } + } + + if (bError) { + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:errcode message:errMsg]]; + [self.commandDelegate evalJs:jsString]; + } + + return audioFile; +} + +// returns whether or not audioSession is available - creates it if necessary +- (BOOL)hasAudioSession +{ + BOOL bSession = YES; + + if (!self.avSession) { + NSError* error = nil; + + self.avSession = [AVAudioSession sharedInstance]; + if (error) { + // is not fatal if can't get AVAudioSession , just log the error + NSLog(@"error creating audio session: %@", [[error userInfo] description]); + self.avSession = nil; + bSession = NO; + } + } + return bSession; +} + +// helper function to create a error object string +- (NSString*)createMediaErrorWithCode:(CDVMediaError)code message:(NSString*)message +{ + NSMutableDictionary* errorDict = [NSMutableDictionary dictionaryWithCapacity:2]; + + [errorDict setObject:[NSNumber numberWithUnsignedInt:code] forKey:@"code"]; + [errorDict setObject:message ? message:@"" forKey:@"message"]; + return [errorDict JSONString]; +} + +- (void)create:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command.arguments objectAtIndex:0]; + NSString* resourcePath = [command.arguments objectAtIndex:1]; + + CDVAudioFile* audioFile = [self audioFileForResource:resourcePath withId:mediaId doValidation:NO forRecording:NO]; + + if (audioFile == nil) { + NSString* errorMessage = [NSString stringWithFormat:@"Failed to initialize Media file with path %@", resourcePath]; + NSString* jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMessage]]; + [self.commandDelegate evalJs:jsString]; + } else { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } +} + +- (void)setVolume:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + NSString* mediaId = [command.arguments objectAtIndex:0]; + NSNumber* volume = [command.arguments objectAtIndex:1 withDefault:[NSNumber numberWithFloat:1.0]]; + + CDVAudioFile* audioFile; + if ([self soundCache] == nil) { + [self setSoundCache:[NSMutableDictionary dictionaryWithCapacity:1]]; + } else { + audioFile = [[self soundCache] objectForKey:mediaId]; + audioFile.volume = volume; + if (audioFile.player) { + audioFile.player.volume = [volume floatValue]; + } + [[self soundCache] setObject:audioFile forKey:mediaId]; + } + + // don't care for any callbacks +} + +- (void)startPlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + NSString* mediaId = [command.arguments objectAtIndex:0]; + NSString* resourcePath = [command.arguments objectAtIndex:1]; + NSDictionary* options = [command.arguments objectAtIndex:2 withDefault:nil]; + + BOOL bError = NO; + NSString* jsString = nil; + + CDVAudioFile* audioFile = [self audioFileForResource:resourcePath withId:mediaId doValidation:YES forRecording:NO]; + if ((audioFile != nil) && (audioFile.resourceURL != nil)) { + if (audioFile.player == nil) { + bError = [self prepareToPlay:audioFile withId:mediaId]; + } + if (!bError) { + // audioFile.player != nil or player was successfully created + // get the audioSession and set the category to allow Playing when device is locked or ring/silent switch engaged + if ([self hasAudioSession]) { + NSError* __autoreleasing err = nil; + NSNumber* playAudioWhenScreenIsLocked = [options objectForKey:@"playAudioWhenScreenIsLocked"]; + BOOL bPlayAudioWhenScreenIsLocked = YES; + if (playAudioWhenScreenIsLocked != nil) { + bPlayAudioWhenScreenIsLocked = [playAudioWhenScreenIsLocked boolValue]; + } + + NSString* sessionCategory = bPlayAudioWhenScreenIsLocked ? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient; + [self.avSession setCategory:sessionCategory error:&err]; + if (![self.avSession setActive:YES error:&err]) { + // other audio with higher priority that does not allow mixing could cause this to fail + NSLog(@"Unable to play audio: %@", [err localizedFailureReason]); + bError = YES; + } + } + if (!bError) { + NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); + NSNumber* loopOption = [options objectForKey:@"numberOfLoops"]; + NSInteger numberOfLoops = 0; + if (loopOption != nil) { + numberOfLoops = [loopOption intValue] - 1; + } + audioFile.player.numberOfLoops = numberOfLoops; + if (audioFile.player.isPlaying) { + [audioFile.player stop]; + audioFile.player.currentTime = 0; + } + if (audioFile.volume != nil) { + audioFile.player.volume = [audioFile.volume floatValue]; + } + + [audioFile.player play]; + double position = round(audioFile.player.duration * 1000) / 1000; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);\n%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_DURATION, position, @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_STATE, MEDIA_RUNNING]; + [self.commandDelegate evalJs:jsString]; + } + } + if (bError) { + /* I don't see a problem playing previously recorded audio so removing this section - BG + NSError* error; + // try loading it one more time, in case the file was recorded previously + audioFile.player = [[ AVAudioPlayer alloc ] initWithContentsOfURL:audioFile.resourceURL error:&error]; + if (error != nil) { + NSLog(@"Failed to initialize AVAudioPlayer: %@\n", error); + audioFile.player = nil; + } else { + NSLog(@"Playing audio sample '%@'", audioFile.resourcePath); + audioFile.player.numberOfLoops = numberOfLoops; + [audioFile.player play]; + } */ + // error creating the session or player + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_NONE_SUPPORTED]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_NONE_SUPPORTED message:nil]]; + [self.commandDelegate evalJs:jsString]; + } + } + // else audioFile was nil - error already returned from audioFile for resource + return; +} + +- (BOOL)prepareToPlay:(CDVAudioFile*)audioFile withId:(NSString*)mediaId +{ + BOOL bError = NO; + NSError* __autoreleasing playerError = nil; + + // create the player + NSURL* resourceURL = audioFile.resourceURL; + + if ([resourceURL isFileURL]) { + audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:resourceURL error:&playerError]; + } else { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:resourceURL]; + NSString* userAgent = [self.commandDelegate userAgent]; + if (userAgent) { + [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + + NSURLResponse* __autoreleasing response = nil; + NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&playerError]; + if (playerError) { + NSLog(@"Unable to download audio from: %@", [resourceURL absoluteString]); + } else { + // bug in AVAudioPlayer when playing downloaded data in NSData - we have to download the file and play from disk + CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); + CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); + NSString* filePath = [NSString stringWithFormat:@"%@/%@", [NSTemporaryDirectory()stringByStandardizingPath], uuidString]; + CFRelease(uuidString); + CFRelease(uuidRef); + + [data writeToFile:filePath atomically:YES]; + NSURL* fileURL = [NSURL fileURLWithPath:filePath]; + audioFile.player = [[CDVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&playerError]; + } + } + + if (playerError != nil) { + NSLog(@"Failed to initialize AVAudioPlayer: %@\n", [playerError localizedDescription]); + audioFile.player = nil; + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + bError = YES; + } else { + audioFile.player.mediaId = mediaId; + audioFile.player.delegate = self; + bError = ![audioFile.player prepareToPlay]; + } + return bError; +} + +- (void)stopPlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command.arguments objectAtIndex:0]; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSLog(@"Stopped playing audio sample '%@'", audioFile.resourcePath); + [audioFile.player stop]; + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } // ignore if no media playing + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)pausePlayingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command.arguments objectAtIndex:0]; + NSString* jsString = nil; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSLog(@"Paused playing audio sample '%@'", audioFile.resourcePath); + [audioFile.player pause]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_STATE, MEDIA_PAUSED]; + } + // ignore if no media playing + + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)seekToAudio:(CDVInvokedUrlCommand*)command +{ + // args: + // 0 = Media id + // 1 = path to resource + // 2 = seek to location in milliseconds + + NSString* mediaId = [command.arguments objectAtIndex:0]; + + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + double position = [[command.arguments objectAtIndex:1] doubleValue]; + + if ((audioFile != nil) && (audioFile.player != nil)) { + NSString* jsString; + double posInSeconds = position / 1000; + if (posInSeconds >= audioFile.player.duration) { + // The seek is past the end of file. Stop media and reset to beginning instead of seeking past the end. + [audioFile.player stop]; + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);\n%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_POSITION, 0.0, @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + // NSLog(@"seekToEndJsString=%@",jsString); + } else { + audioFile.player.currentTime = posInSeconds; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%f);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_POSITION, posInSeconds]; + // NSLog(@"seekJsString=%@",jsString); + } + + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)release:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command.arguments objectAtIndex:0]; + + if (mediaId != nil) { + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + if (audioFile != nil) { + if (audioFile.player && [audioFile.player isPlaying]) { + [audioFile.player stop]; + } + if (audioFile.recorder && [audioFile.recorder isRecording]) { + [audioFile.recorder stop]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + self.avSession = nil; + } + [[self soundCache] removeObjectForKey:mediaId]; + NSLog(@"Media with id %@ released", mediaId); + } + } +} + +- (void)getCurrentPositionAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* mediaId = [command.arguments objectAtIndex:0]; + +#pragma unused(mediaId) + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + double position = -1; + + if ((audioFile != nil) && (audioFile.player != nil) && [audioFile.player isPlaying]) { + position = round(audioFile.player.currentTime * 1000) / 1000; + } + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:position]; + NSString* jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%.3f);\n%@", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_POSITION, position, [result toSuccessCallbackString:callbackId]]; + [self.commandDelegate evalJs:jsString]; +} + +- (void)startRecordingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + +#pragma unused(callbackId) + + NSString* mediaId = [command.arguments objectAtIndex:0]; + CDVAudioFile* audioFile = [self audioFileForResource:[command.arguments objectAtIndex:1] withId:mediaId doValidation:YES forRecording:YES]; + NSString* jsString = nil; + NSString* errorMsg = @""; + + if ((audioFile != nil) && (audioFile.resourceURL != nil)) { + NSError* __autoreleasing error = nil; + + if (audioFile.recorder != nil) { + [audioFile.recorder stop]; + audioFile.recorder = nil; + } + // get the audioSession and set the category to allow recording when device is locked or ring/silent switch engaged + if ([self hasAudioSession]) { + [self.avSession setCategory:AVAudioSessionCategoryRecord error:nil]; + if (![self.avSession setActive:YES error:&error]) { + // other audio with higher priority that does not allow mixing could cause this to fail + errorMsg = [NSString stringWithFormat:@"Unable to record audio: %@", [error localizedFailureReason]]; + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_ABORTED]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + [self.commandDelegate evalJs:jsString]; + return; + } + } + + // create a new recorder for each start record + audioFile.recorder = [[CDVAudioRecorder alloc] initWithURL:audioFile.resourceURL settings:nil error:&error]; + + bool recordingSuccess = NO; + if (error == nil) { + audioFile.recorder.delegate = self; + audioFile.recorder.mediaId = mediaId; + recordingSuccess = [audioFile.recorder record]; + if (recordingSuccess) { + NSLog(@"Started recording audio sample '%@'", audioFile.resourcePath); + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_STATE, MEDIA_RUNNING]; + } + } + + if ((error != nil) || (recordingSuccess == NO)) { + if (error != nil) { + errorMsg = [NSString stringWithFormat:@"Failed to initialize AVAudioRecorder: %@\n", [error localizedFailureReason]]; + } else { + errorMsg = @"Failed to start recording using AVAudioRecorder"; + } + audioFile.recorder = nil; + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + } + } else { + // file did not validate + NSString* errorMsg = [NSString stringWithFormat:@"Could not record audio at '%@'", audioFile.resourcePath]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_ABORTED message:errorMsg]]; + } + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } + return; +} + +- (void)stopRecordingAudio:(CDVInvokedUrlCommand*)command +{ + NSString* mediaId = [command.arguments objectAtIndex:0]; + + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if ((audioFile != nil) && (audioFile.recorder != nil)) { + NSLog(@"Stopped recording audio sample '%@'", audioFile.resourcePath); + [audioFile.recorder stop]; + // no callback - that will happen in audioRecorderDidFinishRecording + } + // ignore if no media recording + if (jsString) { + [self.commandDelegate evalJs:jsString]; + } +} + +- (void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfully:(BOOL)flag +{ + CDVAudioRecorder* aRecorder = (CDVAudioRecorder*)recorder; + NSString* mediaId = aRecorder.mediaId; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if (audioFile != nil) { + NSLog(@"Finished recording audio sample '%@'", audioFile.resourcePath); + } + if (flag) { + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } else { + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_DECODE]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_DECODE message:nil]]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + [self.commandDelegate evalJs:jsString]; +} + +- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer*)player successfully:(BOOL)flag +{ + CDVAudioPlayer* aPlayer = (CDVAudioPlayer*)player; + NSString* mediaId = aPlayer.mediaId; + CDVAudioFile* audioFile = [[self soundCache] objectForKey:mediaId]; + NSString* jsString = nil; + + if (audioFile != nil) { + NSLog(@"Finished playing audio sample '%@'", audioFile.resourcePath); + } + if (flag) { + audioFile.player.currentTime = 0; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_STATE, MEDIA_STOPPED]; + } else { + // jsString = [NSString stringWithFormat: @"%@(\"%@\",%d,%d);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, MEDIA_ERR_DECODE]; + jsString = [NSString stringWithFormat:@"%@(\"%@\",%d,%@);", @"cordova.require('cordova/plugin/Media').onStatus", mediaId, MEDIA_ERROR, [self createMediaErrorWithCode:MEDIA_ERR_DECODE message:nil]]; + } + if (self.avSession) { + [self.avSession setActive:NO error:nil]; + } + [self.commandDelegate evalJs:jsString]; +} + +- (void)onMemoryWarning +{ + [[self soundCache] removeAllObjects]; + [self setSoundCache:nil]; + [self setAvSession:nil]; + + [super onMemoryWarning]; +} + +- (void)dealloc +{ + [[self soundCache] removeAllObjects]; +} + +- (void)onReset +{ + for (CDVAudioFile* audioFile in [[self soundCache] allValues]) { + if (audioFile != nil) { + if (audioFile.player != nil) { + [audioFile.player stop]; + audioFile.player.currentTime = 0; + } + if (audioFile.recorder != nil) { + [audioFile.recorder stop]; + } + } + } + + [[self soundCache] removeAllObjects]; +} + +@end + +@implementation CDVAudioFile + +@synthesize resourcePath; +@synthesize resourceURL; +@synthesize player, volume; +@synthesize recorder; + +@end +@implementation CDVAudioPlayer +@synthesize mediaId; + +@end + +@implementation CDVAudioRecorder +@synthesize mediaId; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVSplashScreen.h b/cordova/ios/CordovaLib/Classes/CDVSplashScreen.h new file mode 100755 index 000000000..704ab4315 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVSplashScreen.h @@ -0,0 +1,33 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +@interface CDVSplashScreen : CDVPlugin { + UIActivityIndicatorView* _activityView; + UIImageView* _imageView; + NSString* _curImageName; + BOOL _visible; +} + +- (void)show:(CDVInvokedUrlCommand*)command; +- (void)hide:(CDVInvokedUrlCommand*)command; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVSplashScreen.m b/cordova/ios/CordovaLib/Classes/CDVSplashScreen.m new file mode 100755 index 000000000..45889a0e7 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVSplashScreen.m @@ -0,0 +1,225 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVSplashScreen.h" + +#define kSplashScreenDurationDefault 0.25f + +@implementation CDVSplashScreen + +- (void)pluginInitialize +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pageDidLoad) name:CDVPageDidLoadNotification object:self.webView]; + + [self setVisible:YES]; +} + +- (void)show:(CDVInvokedUrlCommand*)command +{ + [self setVisible:YES]; +} + +- (void)hide:(CDVInvokedUrlCommand*)command +{ + [self setVisible:NO]; +} + +- (void)pageDidLoad +{ + id autoHideSplashScreenValue = [self.commandDelegate.settings objectForKey:@"AutoHideSplashScreen"]; + + // if value is missing, default to yes + if ((autoHideSplashScreenValue == nil) || [autoHideSplashScreenValue boolValue]) { + [self setVisible:NO]; + } +} + +- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context +{ + [self updateImage]; +} + +- (void)createViews +{ + /* + * 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.commandDelegate.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; + } + + UIView* parentView = self.viewController.view; + parentView.userInteractionEnabled = NO; // disable user interaction while splashscreen is shown + _activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:topActivityIndicatorStyle]; + _activityView.center = CGPointMake(parentView.bounds.size.width / 2, parentView.bounds.size.height / 2); + _activityView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin + | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin; + [_activityView startAnimating]; + + // Set the frame & image later. + _imageView = [[UIImageView alloc] init]; + [parentView addSubview:_imageView]; + + id showSplashScreenSpinnerValue = [self.commandDelegate.settings objectForKey:@"ShowSplashScreenSpinner"]; + // backwards compatibility - if key is missing, default to true + if ((showSplashScreenSpinnerValue == nil) || [showSplashScreenSpinnerValue boolValue]) { + [parentView addSubview:_activityView]; + } + + // Frame is required when launching in portrait mode. + // Bounds for landscape since it captures the rotation. + [parentView addObserver:self forKeyPath:@"frame" options:0 context:nil]; + [parentView addObserver:self forKeyPath:@"bounds" options:0 context:nil]; + + [self updateImage]; +} + +- (void)destroyViews +{ + [_imageView removeFromSuperview]; + [_activityView removeFromSuperview]; + _imageView = nil; + _activityView = nil; + _curImageName = nil; + + self.viewController.view.userInteractionEnabled = YES; // re-enable user interaction upon completion + [self.viewController.view removeObserver:self forKeyPath:@"frame"]; + [self.viewController.view removeObserver:self forKeyPath:@"bounds"]; +} + +// Sets the view's frame and image. +- (void)updateImage +{ + UIInterfaceOrientation orientation = self.viewController.interfaceOrientation; + + // Use UILaunchImageFile if specified in plist. Otherwise, use Default. + NSString* imageName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UILaunchImageFile"]; + + if (imageName) { + imageName = [imageName stringByDeletingPathExtension]; + } else { + imageName = @"Default"; + } + + if (CDV_IsIPhone5()) { + imageName = [imageName stringByAppendingString:@"-568h"]; + } else if (CDV_IsIPad()) { + switch (orientation) { + case UIInterfaceOrientationLandscapeLeft: + case UIInterfaceOrientationLandscapeRight: + imageName = [imageName stringByAppendingString:@"-Landscape"]; + break; + + case UIInterfaceOrientationPortrait: + case UIInterfaceOrientationPortraitUpsideDown: + default: + imageName = [imageName stringByAppendingString:@"-Portrait"]; + break; + } + } + + if (![imageName isEqualToString:_curImageName]) { + UIImage* img = [UIImage imageNamed:imageName]; + _imageView.image = img; + _curImageName = imageName; + } + + [self updateBounds]; +} + +- (void)updateBounds +{ + UIImage* img = _imageView.image; + CGRect imgBounds = CGRectMake(0, 0, img.size.width, img.size.height); + + CGSize screenSize = [self.viewController.view convertRect:[UIScreen mainScreen].bounds fromView:nil].size; + + // There's a special case when the image is the size of the screen. + if (CGSizeEqualToSize(screenSize, imgBounds.size)) { + CGRect statusFrame = [self.viewController.view convertRect:[UIApplication sharedApplication].statusBarFrame fromView:nil]; + imgBounds.origin.y -= statusFrame.size.height; + } else { + CGRect viewBounds = self.viewController.view.bounds; + CGFloat imgAspect = imgBounds.size.width / imgBounds.size.height; + CGFloat viewAspect = viewBounds.size.width / viewBounds.size.height; + // This matches the behaviour of the native splash screen. + CGFloat ratio; + if (viewAspect > imgAspect) { + ratio = viewBounds.size.width / imgBounds.size.width; + } else { + ratio = viewBounds.size.height / imgBounds.size.height; + } + imgBounds.size.height *= ratio; + imgBounds.size.width *= ratio; + } + + _imageView.frame = imgBounds; +} + +- (void)setVisible:(BOOL)visible +{ + if (visible == _visible) { + return; + } + _visible = visible; + + id fadeSplashScreenValue = [self.commandDelegate.settings objectForKey:@"FadeSplashScreen"]; + id fadeSplashScreenDuration = [self.commandDelegate.settings objectForKey:@"FadeSplashScreenDuration"]; + + float fadeDuration = fadeSplashScreenDuration == nil ? kSplashScreenDurationDefault : [fadeSplashScreenDuration floatValue]; + + if ((fadeSplashScreenValue == nil) || ![fadeSplashScreenValue boolValue]) { + fadeDuration = 0; + } + + // Never animate the showing of the splash screen. + if (visible) { + if (_imageView == nil) { + [self createViews]; + } + } else if (fadeDuration == 0) { + [self destroyViews]; + } else { + [UIView transitionWithView:self.viewController.view + duration:fadeDuration + options:UIViewAnimationOptionTransitionNone + animations:^(void) { + [_imageView setAlpha:0]; + [_activityView setAlpha:0]; + } + + completion:^(BOOL finished) { + [self destroyViews]; + }]; + } +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVTimer.h b/cordova/ios/CordovaLib/Classes/CDVTimer.h new file mode 100755 index 000000000..6d31593f2 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVTimer.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVTimer : NSObject + ++ (void)start:(NSString*)name; ++ (void)stop:(NSString*)name; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVTimer.m b/cordova/ios/CordovaLib/Classes/CDVTimer.m new file mode 100755 index 000000000..784e94d38 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVTimer.m @@ -0,0 +1,123 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVTimer.h" + +#pragma mark CDVTimerItem + +@interface CDVTimerItem : NSObject + +@property (nonatomic, strong) NSString* name; +@property (nonatomic, strong) NSDate* started; +@property (nonatomic, strong) NSDate* ended; + +- (void)log; + +@end + +@implementation CDVTimerItem + +- (void)log +{ + NSLog(@"[CDVTimer][%@] %fms", self.name, [self.ended timeIntervalSinceDate:self.started] * 1000.0); +} + +@end + +#pragma mark CDVTimer + +@interface CDVTimer () + +@property (nonatomic, strong) NSMutableDictionary* items; + +@end + +@implementation CDVTimer + +#pragma mark object methods + +- (id)init +{ + if (self = [super init]) { + self.items = [NSMutableDictionary dictionaryWithCapacity:6]; + } + + return self; +} + +- (void)add:(NSString*)name +{ + if ([self.items objectForKey:[name lowercaseString]] == nil) { + CDVTimerItem* item = [CDVTimerItem new]; + item.name = name; + item.started = [NSDate new]; + [self.items setObject:item forKey:[name lowercaseString]]; + } else { + NSLog(@"Timer called '%@' already exists.", name); + } +} + +- (void)remove:(NSString*)name +{ + CDVTimerItem* item = [self.items objectForKey:[name lowercaseString]]; + + if (item != nil) { + item.ended = [NSDate new]; + [item log]; + [self.items removeObjectForKey:[name lowercaseString]]; + } else { + NSLog(@"Timer called '%@' does not exist.", name); + } +} + +- (void)removeAll +{ + [self.items removeAllObjects]; +} + +#pragma mark class methods + ++ (void)start:(NSString*)name +{ + [[CDVTimer sharedInstance] add:name]; +} + ++ (void)stop:(NSString*)name +{ + [[CDVTimer sharedInstance] remove:name]; +} + ++ (void)clearAll +{ + [[CDVTimer sharedInstance] removeAll]; +} + ++ (CDVTimer*)sharedInstance +{ + static dispatch_once_t pred = 0; + __strong static CDVTimer* _sharedObject = nil; + + dispatch_once(&pred, ^{ + _sharedObject = [[self alloc] init]; + }); + + return _sharedObject; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVURLProtocol.h b/cordova/ios/CordovaLib/Classes/CDVURLProtocol.h new file mode 100755 index 000000000..5444f6d19 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVURLProtocol.h @@ -0,0 +1,29 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVAvailability.h" + +@class CDVViewController; + +@interface CDVURLProtocol : NSURLProtocol {} + ++ (void)registerViewController:(CDVViewController*)viewController; ++ (void)unregisterViewController:(CDVViewController*)viewController; +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVURLProtocol.m b/cordova/ios/CordovaLib/Classes/CDVURLProtocol.m new file mode 100755 index 000000000..afc10dedb --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVURLProtocol.m @@ -0,0 +1,230 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import +#import "CDVURLProtocol.h" +#import "CDVCommandQueue.h" +#import "CDVWhitelist.h" +#import "CDVViewController.h" +#import "CDVFile.h" + +@interface CDVHTTPURLResponse : NSHTTPURLResponse +@property (nonatomic) NSInteger statusCode; +@end + +static CDVWhitelist* gWhitelist = nil; +// Contains a set of NSNumbers of addresses of controllers. It doesn't store +// the actual pointer to avoid retaining. +static NSMutableSet* gRegisteredControllers = nil; + +// Returns the registered view controller that sent the given request. +// If the user-agent is not from a UIWebView, or if it's from an unregistered one, +// then nil is returned. +static CDVViewController *viewControllerForRequest(NSURLRequest* request) +{ + // The exec bridge explicitly sets the VC address in a header. + // This works around the User-Agent not being set for file: URLs. + NSString* addrString = [request valueForHTTPHeaderField:@"vc"]; + + if (addrString == nil) { + NSString* userAgent = [request valueForHTTPHeaderField:@"User-Agent"]; + if (userAgent == nil) { + return nil; + } + NSUInteger bracketLocation = [userAgent rangeOfString:@"(" options:NSBackwardsSearch].location; + if (bracketLocation == NSNotFound) { + return nil; + } + addrString = [userAgent substringFromIndex:bracketLocation + 1]; + } + + long long viewControllerAddress = [addrString longLongValue]; + @synchronized(gRegisteredControllers) { + if (![gRegisteredControllers containsObject:[NSNumber numberWithLongLong:viewControllerAddress]]) { + return nil; + } + } + + return (__bridge CDVViewController*)(void*)viewControllerAddress; +} + +@implementation CDVURLProtocol + ++ (void)registerPGHttpURLProtocol {} + ++ (void)registerURLProtocol {} + +// Called to register the URLProtocol, and to make it away of an instance of +// a ViewController. ++ (void)registerViewController:(CDVViewController*)viewController +{ + if (gRegisteredControllers == nil) { + [NSURLProtocol registerClass:[CDVURLProtocol class]]; + gRegisteredControllers = [[NSMutableSet alloc] initWithCapacity:8]; + // The whitelist doesn't change, so grab the first one and store it. + gWhitelist = viewController.whitelist; + + // Note that we grab the whitelist from the first viewcontroller for now - but this will change + // when we allow a registered viewcontroller to have its own whitelist (e.g InAppBrowser) + // Differentiating the requests will be through the 'vc' http header below as used for the js->objc bridge. + // The 'vc' value is generated by casting the viewcontroller object to a (long long) value (see CDVViewController::webViewDidFinishLoad) + if (gWhitelist == nil) { + NSLog(@"WARNING: NO whitelist has been set in CDVURLProtocol."); + } + } + + @synchronized(gRegisteredControllers) { + [gRegisteredControllers addObject:[NSNumber numberWithLongLong:(long long)viewController]]; + } +} + ++ (void)unregisterViewController:(CDVViewController*)viewController +{ + @synchronized(gRegisteredControllers) { + [gRegisteredControllers removeObject:[NSNumber numberWithLongLong:(long long)viewController]]; + } +} + ++ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest +{ + NSURL* theUrl = [theRequest URL]; + CDVViewController* viewController = viewControllerForRequest(theRequest); + + if ([[theUrl absoluteString] hasPrefix:kCDVAssetsLibraryPrefix]) { + return YES; + } else if (viewController != nil) { + if ([[theUrl path] isEqualToString:@"/!gap_exec"]) { + NSString* queuedCommandsJSON = [theRequest valueForHTTPHeaderField:@"cmds"]; + NSString* requestId = [theRequest valueForHTTPHeaderField:@"rc"]; + if (requestId == nil) { + NSLog(@"!cordova request missing rc header"); + return NO; + } + BOOL hasCmds = [queuedCommandsJSON length] > 0; + if (hasCmds) { + SEL sel = @selector(enqueCommandBatch:); + [viewController.commandQueue performSelectorOnMainThread:sel withObject:queuedCommandsJSON waitUntilDone:NO]; + } else { + SEL sel = @selector(maybeFetchCommandsFromJs:); + [viewController.commandQueue performSelectorOnMainThread:sel withObject:[NSNumber numberWithInteger:[requestId integerValue]] waitUntilDone:NO]; + } + // Returning NO here would be 20% faster, but it spams WebInspector's console with failure messages. + // If JS->Native bridge speed is really important for an app, they should use the iframe bridge. + // Returning YES here causes the request to come through canInitWithRequest two more times. + // For this reason, we return NO when cmds exist. + return !hasCmds; + } + // we only care about http and https connections. + // CORS takes care of http: trying to access file: URLs. + if ([gWhitelist schemeIsAllowed:[theUrl scheme]]) { + // if it FAILS the whitelist, we return TRUE, so we can fail the connection later + return ![gWhitelist URLIsAllowed:theUrl]; + } + } + + return NO; +} + ++ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request +{ + // NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd)); + return request; +} + +- (void)startLoading +{ + // NSLog(@"%@ received %@ - start", self, NSStringFromSelector(_cmd)); + NSURL* url = [[self request] URL]; + + if ([[url path] isEqualToString:@"/!gap_exec"]) { + [self sendResponseWithResponseCode:200 data:nil mimeType:nil]; + return; + } else if ([[url absoluteString] hasPrefix:kCDVAssetsLibraryPrefix]) { + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Get the data and send it along. + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + NSString* MIMEType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)[assetRepresentation UTI], kUTTagClassMIMEType); + Byte* buffer = (Byte*)malloc([assetRepresentation size]); + NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil]; + NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; + [self sendResponseWithResponseCode:200 data:data mimeType:MIMEType]; + } else { + // Retrieving the asset failed for some reason. Send an error. + [self sendResponseWithResponseCode:404 data:nil mimeType:nil]; + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send an error. + [self sendResponseWithResponseCode:401 data:nil mimeType:nil]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:url resultBlock:resultBlock failureBlock:failureBlock]; + return; + } + + NSString* body = [gWhitelist errorStringForURL:url]; + [self sendResponseWithResponseCode:401 data:[body dataUsingEncoding:NSASCIIStringEncoding] mimeType:nil]; +} + +- (void)stopLoading +{ + // do any cleanup here +} + ++ (BOOL)requestIsCacheEquivalent:(NSURLRequest*)requestA toRequest:(NSURLRequest*)requestB +{ + return NO; +} + +- (void)sendResponseWithResponseCode:(NSInteger)statusCode data:(NSData*)data mimeType:(NSString*)mimeType +{ + if (mimeType == nil) { + mimeType = @"text/plain"; + } + NSString* encodingName = [@"text/plain" isEqualToString : mimeType] ? @"UTF-8" : nil; + CDVHTTPURLResponse* response = + [[CDVHTTPURLResponse alloc] initWithURL:[[self request] URL] + MIMEType:mimeType + expectedContentLength:[data length] + textEncodingName:encodingName]; + response.statusCode = statusCode; + + [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + if (data != nil) { + [[self client] URLProtocol:self didLoadData:data]; + } + [[self client] URLProtocolDidFinishLoading:self]; +} + +@end + +@implementation CDVHTTPURLResponse +@synthesize statusCode; + +- (NSDictionary*)allHeaderFields +{ + return nil; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVUserAgentUtil.h b/cordova/ios/CordovaLib/Classes/CDVUserAgentUtil.h new file mode 100755 index 000000000..4de382f0b --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVUserAgentUtil.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVUserAgentUtil : NSObject ++ (NSString*)originalUserAgent; ++ (void)acquireLock:(void (^)(NSInteger lockToken))block; ++ (void)releaseLock:(NSInteger*)lockToken; ++ (void)setUserAgent:(NSString*)value lockToken:(NSInteger)lockToken; +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVUserAgentUtil.m b/cordova/ios/CordovaLib/Classes/CDVUserAgentUtil.m new file mode 100755 index 000000000..9923d4704 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVUserAgentUtil.m @@ -0,0 +1,120 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVUserAgentUtil.h" + +#import + +// #define VerboseLog NSLog +#define VerboseLog(...) do {} while (0) + +static NSString* const kCdvUserAgentKey = @"Cordova-User-Agent"; +static NSString* const kCdvUserAgentVersionKey = @"Cordova-User-Agent-Version"; + +static NSString* gOriginalUserAgent = nil; +static NSInteger gNextLockToken = 0; +static NSInteger gCurrentLockToken = 0; +static NSMutableArray* gPendingSetUserAgentBlocks = nil; + +@implementation CDVUserAgentUtil + ++ (NSString*)originalUserAgent +{ + if (gOriginalUserAgent == nil) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppLocaleDidChange:) + name:NSCurrentLocaleDidChangeNotification object:nil]; + + NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults]; + NSString* systemVersion = [[UIDevice currentDevice] systemVersion]; + NSString* localeStr = [[NSLocale currentLocale] localeIdentifier]; + NSString* systemAndLocale = [NSString stringWithFormat:@"%@ %@", systemVersion, localeStr]; + + NSString* cordovaUserAgentVersion = [userDefaults stringForKey:kCdvUserAgentVersionKey]; + gOriginalUserAgent = [userDefaults stringForKey:kCdvUserAgentKey]; + BOOL cachedValueIsOld = ![systemAndLocale isEqualToString:cordovaUserAgentVersion]; + + if ((gOriginalUserAgent == nil) || cachedValueIsOld) { + UIWebView* sampleWebView = [[UIWebView alloc] initWithFrame:CGRectZero]; + gOriginalUserAgent = [sampleWebView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"]; + + [userDefaults setObject:gOriginalUserAgent forKey:kCdvUserAgentKey]; + [userDefaults setObject:systemAndLocale forKey:kCdvUserAgentVersionKey]; + + [userDefaults synchronize]; + } + } + return gOriginalUserAgent; +} + ++ (void)onAppLocaleDidChange:(NSNotification*)notification +{ + // TODO: We should figure out how to update the user-agent of existing UIWebViews when this happens. + // Maybe use the PDF bug (noted in setUserAgent:). + gOriginalUserAgent = nil; +} + ++ (void)acquireLock:(void (^)(NSInteger lockToken))block +{ + if (gCurrentLockToken == 0) { + gCurrentLockToken = ++gNextLockToken; + VerboseLog(@"Gave lock %d", gCurrentLockToken); + block(gCurrentLockToken); + } else { + if (gPendingSetUserAgentBlocks == nil) { + gPendingSetUserAgentBlocks = [[NSMutableArray alloc] initWithCapacity:4]; + } + VerboseLog(@"Waiting for lock"); + [gPendingSetUserAgentBlocks addObject:block]; + } +} + ++ (void)releaseLock:(NSInteger*)lockToken +{ + if (*lockToken == 0) { + return; + } + NSAssert(gCurrentLockToken == *lockToken, @"Got token %d, expected %d", *lockToken, gCurrentLockToken); + + VerboseLog(@"Released lock %d", *lockToken); + if ([gPendingSetUserAgentBlocks count] > 0) { + void (^block)() = [gPendingSetUserAgentBlocks objectAtIndex:0]; + [gPendingSetUserAgentBlocks removeObjectAtIndex:0]; + gCurrentLockToken = ++gNextLockToken; + NSLog(@"Gave lock %d", gCurrentLockToken); + block(gCurrentLockToken); + } else { + gCurrentLockToken = 0; + } + *lockToken = 0; +} + ++ (void)setUserAgent:(NSString*)value lockToken:(NSInteger)lockToken +{ + NSAssert(gCurrentLockToken == lockToken, @"Got token %d, expected %d", lockToken, gCurrentLockToken); + VerboseLog(@"User-Agent set to: %@", value); + + // Setting the UserAgent must occur before a UIWebView is instantiated. + // It is read per instantiation, so it does not affect previously created views. + // Except! When a PDF is loaded, all currently active UIWebViews reload their + // User-Agent from the NSUserDefaults some time after the DidFinishLoad of the PDF bah! + NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:value, @"UserAgent", nil]; + [[NSUserDefaults standardUserDefaults] registerDefaults:dict]; +} + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVViewController.h b/cordova/ios/CordovaLib/Classes/CDVViewController.h new file mode 100755 index 000000000..2338bafb1 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVViewController.h @@ -0,0 +1,73 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import "CDVAvailability.h" +#import "CDVInvokedUrlCommand.h" +#import "CDVCommandDelegate.h" +#import "CDVCommandQueue.h" +#import "CDVWhitelist.h" +#import "CDVScreenOrientationDelegate.h" +#import "CDVPlugin.h" + +@interface CDVViewController : UIViewController { + @protected + id _commandDelegate; + @protected + CDVCommandQueue* _commandQueue; + NSString* _userAgent; +} + +@property (nonatomic, strong) IBOutlet UIWebView* webView; + +@property (nonatomic, readonly, strong) NSMutableDictionary* pluginObjects; +@property (nonatomic, readonly, strong) NSDictionary* pluginsMap; +@property (nonatomic, readonly, strong) NSMutableDictionary* settings; +@property (nonatomic, readonly, strong) NSXMLParser* configParser; +@property (nonatomic, readonly, strong) CDVWhitelist* whitelist; // readonly for public +@property (nonatomic, readonly, assign) BOOL loadFromString; +@property (nonatomic, readwrite, assign) BOOL useSplashScreen CDV_DEPRECATED(2.5, "Add/Remove the SplashScreen plugin instead of setting this property."); + +@property (nonatomic, readwrite, copy) NSString* wwwFolderName; +@property (nonatomic, readwrite, copy) NSString* startPage; +@property (nonatomic, readonly, strong) CDVCommandQueue* commandQueue; +@property (nonatomic, readonly, strong) id commandDelegate; +@property (nonatomic, readonly) NSString* userAgent; + ++ (NSDictionary*)getBundlePlist:(NSString*)plistName; ++ (NSString*)applicationDocumentsDirectory; + +- (void)printMultitaskingInfo; +- (void)createGapView; +- (UIWebView*)newCordovaViewWithFrame:(CGRect)bounds; + +- (void)javascriptAlert:(NSString*)text; +- (NSString*)appURLScheme; + +- (NSArray*)parseInterfaceOrientations:(NSArray*)orientations; +- (BOOL)supportsOrientation:(UIInterfaceOrientation)orientation; + +- (id)getCommandInstance:(NSString*)pluginName; +- (void)registerPlugin:(CDVPlugin*)plugin withClassName:(NSString*)className; +- (void)registerPlugin:(CDVPlugin*)plugin withPluginName:(NSString*)pluginName; + +- (BOOL)URLisAllowed:(NSURL*)url; + +@end diff --git a/cordova/ios/CordovaLib/Classes/CDVViewController.m b/cordova/ios/CordovaLib/Classes/CDVViewController.m new file mode 100755 index 000000000..94f455222 --- /dev/null +++ b/cordova/ios/CordovaLib/Classes/CDVViewController.m @@ -0,0 +1,933 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDV.h" +#import "CDVCommandDelegateImpl.h" +#import "CDVConfigParser.h" +#import "CDVUserAgentUtil.h" +#import "CDVWebViewDelegate.h" + +#define degreesToRadian(x) (M_PI * (x) / 180.0) + +@interface CDVViewController () { + NSInteger _userAgentLockToken; + CDVWebViewDelegate* _webViewDelegate; +} + +@property (nonatomic, readwrite, strong) NSXMLParser* configParser; +@property (nonatomic, readwrite, strong) NSMutableDictionary* settings; +@property (nonatomic, readwrite, strong) CDVWhitelist* whitelist; +@property (nonatomic, readwrite, strong) NSMutableDictionary* pluginObjects; +@property (nonatomic, readwrite, strong) NSArray* startupPluginNames; +@property (nonatomic, readwrite, strong) NSDictionary* pluginsMap; +@property (nonatomic, readwrite, strong) NSArray* supportedOrientations; +@property (nonatomic, readwrite, assign) BOOL loadFromString; + +@property (readwrite, assign) BOOL initialized; + +@property (atomic, strong) NSURL* openURL; + +@end + +@implementation CDVViewController + +@synthesize webView, supportedOrientations; +@synthesize pluginObjects, pluginsMap, whitelist, startupPluginNames; +@synthesize configParser, settings, loadFromString; +@synthesize useSplashScreen; +@synthesize wwwFolderName, startPage, initialized, openURL; +@synthesize commandDelegate = _commandDelegate; +@synthesize commandQueue = _commandQueue; + +- (void)__init +{ + if ((self != nil) && !self.initialized) { + _commandQueue = [[CDVCommandQueue alloc] initWithViewController:self]; + _commandDelegate = [[CDVCommandDelegateImpl alloc] initWithViewController:self]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillTerminate:) + name:UIApplicationWillTerminateNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillResignActive:) + name:UIApplicationWillResignActiveNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleOpenURL:) name:CDVPluginHandleOpenURLNotification object:nil]; + + // read from UISupportedInterfaceOrientations (or UISupportedInterfaceOrientations~iPad, if its iPad) from -Info.plist + self.supportedOrientations = [self parseInterfaceOrientations: + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UISupportedInterfaceOrientations"]]; + + [self printMultitaskingInfo]; + [self printDeprecationNotice]; + self.initialized = YES; + + // load config.xml settings + [self loadSettings]; + useSplashScreen = YES; + } +} + +- (id)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + [self __init]; + return self; +} + +- (id)init +{ + self = [super init]; + [self __init]; + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:self + selector:@selector(keyboardWillShowOrHide:) + name:UIKeyboardWillShowNotification + object:nil]; + [nc addObserver:self + selector:@selector(keyboardWillShowOrHide:) + name:UIKeyboardWillHideNotification + object:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver:self name:UIKeyboardWillShowNotification object:nil]; + [nc removeObserver:self name:UIKeyboardWillHideNotification object:nil]; +} + +- (void)keyboardWillShowOrHide:(NSNotification*)notif +{ + if (![@"true" isEqualToString : self.settings[@"KeyboardShrinksView"]]) { + return; + } + BOOL showEvent = [notif.name isEqualToString:UIKeyboardWillShowNotification]; + + CGRect keyboardFrame = [notif.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + keyboardFrame = [self.view convertRect:keyboardFrame fromView:nil]; + + CGRect newFrame = self.view.bounds; + if (showEvent) { + newFrame.size.height -= keyboardFrame.size.height; + } + self.webView.frame = newFrame; + self.webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, -keyboardFrame.size.height, 0); +} + +- (void)printDeprecationNotice +{ + if (!IsAtLeastiOSVersion(@"5.0")) { + NSLog(@"CRITICAL: For Cordova 2.0, you will need to upgrade to at least iOS 5.0 or greater. Your current version of iOS is %@.", + [[UIDevice currentDevice] systemVersion] + ); + } +} + +- (void)printMultitaskingInfo +{ + UIDevice* device = [UIDevice currentDevice]; + BOOL backgroundSupported = NO; + + if ([device respondsToSelector:@selector(isMultitaskingSupported)]) { + backgroundSupported = device.multitaskingSupported; + } + + NSNumber* exitsOnSuspend = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationExitsOnSuspend"]; + if (exitsOnSuspend == nil) { // if it's missing, it should be NO (i.e. multi-tasking on by default) + exitsOnSuspend = [NSNumber numberWithBool:NO]; + } + + NSLog(@"Multi-tasking -> Device: %@, App: %@", (backgroundSupported ? @"YES" : @"NO"), (![exitsOnSuspend intValue]) ? @"YES" : @"NO"); +} + +- (BOOL)URLisAllowed:(NSURL*)url +{ + if (self.whitelist == nil) { + return YES; + } + + return [self.whitelist URLIsAllowed:url]; +} + +- (void)loadSettings +{ + CDVConfigParser* delegate = [[CDVConfigParser alloc] init]; + + // read from config.xml in the app bundle + NSString* path = [[NSBundle mainBundle] pathForResource:@"config" ofType:@"xml"]; + + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSAssert(NO, @"ERROR: config.xml does not exist. Please run cordova-ios/bin/cordova_plist_to_config_xml path/to/project."); + return; + } + + NSURL* url = [NSURL fileURLWithPath:path]; + + configParser = [[NSXMLParser alloc] initWithContentsOfURL:url]; + if (configParser == nil) { + NSLog(@"Failed to initialize XML parser."); + return; + } + [configParser setDelegate:((id < NSXMLParserDelegate >)delegate)]; + [configParser parse]; + + // Get the plugin dictionary, whitelist and settings from the delegate. + self.pluginsMap = delegate.pluginsDict; + self.startupPluginNames = delegate.startupPluginNames; + self.whitelist = [[CDVWhitelist alloc] initWithArray:delegate.whitelistHosts]; + self.settings = delegate.settings; + + // And the start folder/page. + self.wwwFolderName = @"www"; + self.startPage = delegate.startPage; + if (self.startPage == nil) { + self.startPage = @"index.html"; + } + + // Initialize the plugin objects dict. + self.pluginObjects = [[NSMutableDictionary alloc] initWithCapacity:20]; +} + +// Implement viewDidLoad to do additional setup after loading the view, typically from a nib. +- (void)viewDidLoad +{ + [super viewDidLoad]; + + NSURL* appURL = nil; + NSString* loadErr = nil; + + if ([self.startPage rangeOfString:@"://"].location != NSNotFound) { + appURL = [NSURL URLWithString:self.startPage]; + } else if ([self.wwwFolderName rangeOfString:@"://"].location != NSNotFound) { + appURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", self.wwwFolderName, self.startPage]]; + } else { + NSString* startFilePath = [self.commandDelegate pathForResource:self.startPage]; + if (startFilePath == nil) { + loadErr = [NSString stringWithFormat:@"ERROR: Start Page at '%@/%@' was not found.", self.wwwFolderName, self.startPage]; + NSLog(@"%@", loadErr); + self.loadFromString = YES; + appURL = nil; + } else { + appURL = [NSURL fileURLWithPath:startFilePath]; + } + } + + // // Fix the iOS 5.1 SECURITY_ERR bug (CB-347), this must be before the webView is instantiated //// + + NSString* backupWebStorageType = @"cloud"; // default value + + id backupWebStorage = self.settings[@"BackupWebStorage"]; + if ([backupWebStorage isKindOfClass:[NSString class]]) { + backupWebStorageType = backupWebStorage; + } + self.settings[@"BackupWebStorage"] = backupWebStorageType; + + if (IsAtLeastiOSVersion(@"5.1")) { + [CDVLocalStorage __fixupDatabaseLocationsWithBackupType:backupWebStorageType]; + } + + // // Instantiate the WebView /////////////// + + [self createGapView]; + + // ///////////////// + + NSNumber* enableLocation = [self.settings objectForKey:@"EnableLocation"]; + NSString* enableViewportScale = [self.settings objectForKey:@"EnableViewportScale"]; + NSNumber* allowInlineMediaPlayback = [self.settings objectForKey:@"AllowInlineMediaPlayback"]; + BOOL mediaPlaybackRequiresUserAction = YES; // default value + if ([self.settings objectForKey:@"MediaPlaybackRequiresUserAction"]) { + mediaPlaybackRequiresUserAction = [(NSNumber*)[settings objectForKey:@"MediaPlaybackRequiresUserAction"] boolValue]; + } + BOOL hideKeyboardFormAccessoryBar = NO; // default value + if ([self.settings objectForKey:@"HideKeyboardFormAccessoryBar"]) { + hideKeyboardFormAccessoryBar = [(NSNumber*)[settings objectForKey:@"HideKeyboardFormAccessoryBar"] boolValue]; + } + + self.webView.scalesPageToFit = [enableViewportScale boolValue]; + + /* + * Fire up the GPS Service right away as it takes a moment for data to come back. + */ + + if ([enableLocation boolValue]) { + NSLog(@"Deprecated: The 'EnableLocation' boolean property is deprecated in 2.5.0, and will be removed in 3.0.0. Use the 'onload' boolean attribute (of the CDVLocation plugin."); + [[self.commandDelegate getCommandInstance:@"Geolocation"] getLocation:[CDVInvokedUrlCommand new]]; + } + + if (hideKeyboardFormAccessoryBar) { + __weak CDVViewController* weakSelf = self; + [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* notification) { + // we can't hide it here because the accessory bar hasn't been created yet, so we delay on the queue + [weakSelf performSelector:@selector(hideKeyboardFormAccessoryBar) withObject:nil afterDelay:0]; + }]; + } + + /* + * Fire up CDVLocalStorage to work-around WebKit storage limitations: on all iOS 5.1+ versions for local-only backups, but only needed on iOS 5.1 for cloud backup. + */ + if (IsAtLeastiOSVersion(@"5.1") && (([backupWebStorageType isEqualToString:@"local"]) || + ([backupWebStorageType isEqualToString:@"cloud"] && !IsAtLeastiOSVersion(@"6.0")))) { + [self registerPlugin:[[CDVLocalStorage alloc] initWithWebView:self.webView] withClassName:NSStringFromClass([CDVLocalStorage class])]; + } + + /* + * This is for iOS 4.x, where you can allow inline