From cdc3f5b56210c6538a4e66d1915593f02442bfbd Mon Sep 17 00:00:00 2001 From: Ben Bahrenburg Date: Sun, 28 Jun 2015 12:54:56 -0700 Subject: [PATCH 1/5] add UserActivity Proxies --- iphone/Classes/TiAppiOSUserActivityProxy.h | 21 + iphone/Classes/TiAppiOSUserActivityProxy.m | 393 ++++++++++++++++++ .../iphone/Titanium.xcodeproj/project.pbxproj | 10 + 3 files changed, 424 insertions(+) create mode 100644 iphone/Classes/TiAppiOSUserActivityProxy.h create mode 100644 iphone/Classes/TiAppiOSUserActivityProxy.m diff --git a/iphone/Classes/TiAppiOSUserActivityProxy.h b/iphone/Classes/TiAppiOSUserActivityProxy.h new file mode 100644 index 00000000000..854ca979276 --- /dev/null +++ b/iphone/Classes/TiAppiOSUserActivityProxy.h @@ -0,0 +1,21 @@ +/** + * Appcelerator Titanium Mobile + * Copyright (c) 2015 by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +#import "TiProxy.h" + +#ifdef USE_TI_APPIOS + +@interface TiAppiOSUserActivityProxy : TiProxy { + @private + BOOL _isValid; + BOOL _supported; +} + +@property(nonatomic,retain) NSUserActivity *userActivity; + +@end + +#endif \ No newline at end of file diff --git a/iphone/Classes/TiAppiOSUserActivityProxy.m b/iphone/Classes/TiAppiOSUserActivityProxy.m new file mode 100644 index 00000000000..7dfbf24d309 --- /dev/null +++ b/iphone/Classes/TiAppiOSUserActivityProxy.m @@ -0,0 +1,393 @@ +/** + * Appcelerator Titanium Mobile + * Copyright (c) 2015 by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ + +#import "TiAppiOSUserActivityProxy.h" +#import "TiUtils.h" + +#ifdef USE_TI_APPIOS + +@implementation TiAppiOSUserActivityProxy + +#pragma mark Titanium Proxy components + +-(NSString*)apiName +{ + return @"Ti.App.iOS.UserActivity"; +} + +-(void)_initWithProperties:(NSDictionary *)properties +{ + _supported = [self determineMinRequirements:properties]; + if(_supported) + { + [self buildInitialActivity:properties]; + } + + [super _initWithProperties:properties]; +} + +-(void)dealloc +{ + if(_supported) + { + [self clean]; + } + [super dealloc]; +} + +#pragma mark internal helpers + +-(BOOL)activityTypeValid:(NSString*)activityType +{ + NSArray *supportedActivityTypes = [[NSBundle mainBundle] + objectForInfoDictionaryKey:@"NSUserActivityTypes"]; + + return[supportedActivityTypes containsObject:activityType]; +} + +-(BOOL) determineMinRequirements:(NSDictionary *)props +{ + _isValid = NO; + if([TiUtils isIOS8OrGreater]) + { + if([props objectForKey:@"activityType"]) + { + NSArray *supportedActivityTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSUserActivityTypes"]; + + if(![self activityTypeValid:[TiUtils stringValue:@"activityType" properties:props]]) + { + NSLog(@"[ERROR] activityType provided is not defined in your projects tiapp.xml file"); + return NO; + } + else + { + _isValid = YES; + return YES; + } + } + else + { + NSLog(@"[ERROR] activityType property is required on creation"); + return NO; + } + } + + NSLog(@"[ERROR] %@ requires iOS8 or greater", [self apiName]); + return NO; +} + +-(void) buildInitialActivity:(NSDictionary *)props +{ + NSString* activityType = [TiUtils stringValue:@"activityType" properties:props]; + + [self instanceMake:activityType]; + + if([props objectForKey:@"title"]) + { + [_userActivity setTitle:[TiUtils stringValue:@"title" properties:props]]; + } + + if([props objectForKey:@"userInfo"]) + { + [_userActivity addUserInfoEntriesFromDictionary:[props objectForKey:@"userInfo"]]; + } + + if([props objectForKey:@"webpageURL"]) + { + [_userActivity setWebpageURL:[NSURL URLWithString:[[TiUtils stringValue:@"webpageURL" properties:props] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; + } + + if([props objectForKey:@"needsSave"]) + { + [_userActivity setNeedsSave:[TiUtils boolValue:@"needsSave" properties:props]]; + } + + if([TiUtils isIOS9OrGreater]) + { + if([props objectForKey:@"eligibleForPublicIndexing"]) + { + [_userActivity setEligibleForPublicIndexing:[TiUtils boolValue:@"eligibleForPublicIndexing" properties:props]]; + } + + if([props objectForKey:@"eligibleForSearch"]) + { + [_userActivity setEligibleForSearch:[TiUtils boolValue:@"eligibleForSearch" properties:props]]; + } + + if([props objectForKey:@"eligibleForHandoff"]) + { + [_userActivity setEligibleForHandoff:[TiUtils boolValue:@"eligibleForHandoff" properties:props]]; + } + + if([props objectForKey:@"expirationDate"]) + { + [_userActivity setExpirationDate:[TiUtils dateForUTCDate: + [TiUtils stringValue:@"expirationDate" properties:props]]]; + } + } + + _userActivity.delegate = self; +} + +-(NSDictionary*)copyActivity +{ + NSMutableDictionary *dict = [[NSMutableDictionary + dictionaryWithObjectsAndKeys:[_userActivity activityType],@"activityType", + nil] autorelease]; + + if([_userActivity title] !=nil){ + [dict setObject:[_userActivity title] forKey:@"title"]; + } + + if([_userActivity webpageURL] !=nil){ + [dict setObject:[[_userActivity webpageURL] absoluteString] forKey:@"webpageURL"]; + } + + if([_userActivity userInfo] !=nil){ + [dict setObject:[_userActivity userInfo] forKey:@"userInfo"]; + } + + return dict; +} + +#pragma mark Properties used for housekeeping + +-(void)clean +{ + if(_userActivity !=nil) + { + _userActivity.delegate = nil; + RELEASE_TO_NIL(_userActivity); + } +} + +-(void)instanceMake:(NSString*)activityType +{ + if(_userActivity !=nil) + { + [_userActivity invalidate]; + } + + [self clean]; + + _userActivity = [[NSUserActivity alloc] + initWithActivityType:activityType]; + _userActivity.delegate = self; + +} + +-(NSNumber*)isValid +{ + return NUMBOOL(_isValid); +} + +-(NSNumber*)supported +{ + return NUMBOOL(_supported); +} + +#pragma mark Delegate methods used to raise events + +/* The user activity will be saved (to be continued or persisted). The receiver should update the activity with current activity state. + */ +- (void)userActivityWillSave:(NSUserActivity *)userActivity +{ + if([self _hasListeners:@"useractivitywillsave"]) + { + [self fireEvent:@"useractivitywillsave" withObject:[self copyActivity]]; + } +} + +/* The user activity was continued on another device. + */ +- (void)userActivityWasContinued:(NSUserActivity *)userActivity +{ + if([self _hasListeners:@"useractivitywascontinued"]) + { + [self fireEvent:@"useractivitywascontinued" withObject:[self copyActivity]]; + } +} + +#pragma mark iOS 8 UserActivity Methods + +-(NSString*)activityType +{ + return [_userActivity activityType]; +} + +-(void)setActivityType:(NSString*)value +{ + ENSURE_UI_THREAD(setActivityType,value); + ENSURE_TYPE(value, NSString); + + if(![self activityTypeValid:value]) + { + NSLog(@"[ERROR] activityType provided is not defined in your projects tiapp.xml file"); + return; + } + + if(_userActivity.activityType != value) + { + [self instanceMake:value]; + + } +} + +-(NSString*)title +{ + return [_userActivity title]; +} + +-(void)setTitle:(NSString*)value +{ + ENSURE_UI_THREAD(setTitle,value); + [_userActivity setTitle:value]; +} + +-(NSDictionary*)userInfo +{ + return [_userActivity userInfo]; +} + +-(void)setUserInfo:(NSDictionary*)info +{ + ENSURE_UI_THREAD(setUserInfo,info); + [_userActivity setUserInfo:info]; +} + +-(NSString*)webpageURL +{ + return [[_userActivity webpageURL] absoluteString]; +} + +-(void)setWebpageURL:(NSString*)value +{ + ENSURE_UI_THREAD(setWebpageURL,value); + ENSURE_TYPE(value, NSString); + + [_userActivity setWebpageURL: + [NSURL URLWithString:[value stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; +} + +-(NSNumber*)needsSave +{ + return NUMBOOL([_userActivity needsSave]); +} + +-(void)setNeedsSave:(NSNumber*)value +{ + ENSURE_UI_THREAD(setNeedsSave,value); + [_userActivity setNeedsSave:[TiUtils boolValue:value]]; +} + +-(void)becomeCurrent:(id)unused +{ + ENSURE_UI_THREAD(becomeCurrent,unused); + [_userActivity becomeCurrent]; +} + +-(void)invalidate:(id)unused +{ + ENSURE_UI_THREAD(invalidate,unused); + [_userActivity invalidate]; +} + +#pragma mark iOS 9 UserActivity Methods + +-(NSNumber*)eligibleForPublicIndexing +{ + if(![TiUtils isIOS9OrGreater]) + { + return NUMBOOL(NO); + } + + return NUMBOOL(_userActivity.eligibleForPublicIndexing); +} + +-(void)setEligibleForPublicIndexing:(NSNumber*)value +{ + ENSURE_UI_THREAD(setEligibleForPublicIndexing,value); + if(![TiUtils isIOS9OrGreater]) + { + return; + } + [_userActivity setEligibleForPublicIndexing:[TiUtils boolValue:value]]; +} + +-(NSNumber*)eligibleForSearch +{ + if(![TiUtils isIOS9OrGreater]) + { + return NUMBOOL(NO); + } + + return NUMBOOL(_userActivity.eligibleForSearch); +} + +-(void)setEligibleForSearch:(NSNumber*)value +{ + ENSURE_UI_THREAD(setEligibleForSearch,value); + if(![TiUtils isIOS9OrGreater]) + { + return; + } + [_userActivity setEligibleForSearch:[TiUtils boolValue:value]]; +} + +-(NSNumber*)eligibleForHandoff +{ + if(![TiUtils isIOS9OrGreater]) + { + return NUMBOOL(NO); + } + return NUMBOOL(_userActivity.eligibleForHandoff); +} + +-(void)setEligibleForHandoff:(NSNumber*)value +{ + ENSURE_UI_THREAD(setEligibleForHandoff,value); + if(![TiUtils isIOS9OrGreater]) + { + return; + } + [_userActivity setEligibleForHandoff:[TiUtils boolValue:value]]; +} + +-(NSString*)expirationDate +{ + if(![TiUtils isIOS9OrGreater] || _userActivity.expirationDate == nil) + { + return nil; + } + + return [TiUtils UTCDateForDate:_userActivity.expirationDate]; +} + +-(void)setExpirationDate:(NSString*)UTCDateFormat +{ + ENSURE_UI_THREAD(setExpirationDate,UTCDateFormat); + if(![TiUtils isIOS9OrGreater]) + { + return; + } + + [_userActivity setExpirationDate:[TiUtils dateForUTCDate:UTCDateFormat]]; +} + +-(void)resignCurrent:(id)unused +{ + ENSURE_UI_THREAD(resignCurrent,unused); + if(![TiUtils isIOS9OrGreater]) + { + return; + } + [_userActivity resignCurrent]; +} + +@end + +#endif \ No newline at end of file diff --git a/iphone/iphone/Titanium.xcodeproj/project.pbxproj b/iphone/iphone/Titanium.xcodeproj/project.pbxproj index 82f9637315f..cf69412bfd1 100644 --- a/iphone/iphone/Titanium.xcodeproj/project.pbxproj +++ b/iphone/iphone/Titanium.xcodeproj/project.pbxproj @@ -622,6 +622,9 @@ BBD6C2931A3AA22C00FA700E /* TiUIAttributedStringProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = BBDD81331A2C71C9003CDA10 /* TiUIAttributedStringProxy.m */; }; BBD6C2941A3AA22D00FA700E /* TiUIAttributedStringProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = BBDD81331A2C71C9003CDA10 /* TiUIAttributedStringProxy.m */; }; BBDD81341A2C71C9003CDA10 /* TiUIAttributedStringProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = BBDD81331A2C71C9003CDA10 /* TiUIAttributedStringProxy.m */; }; + CA178E9D1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = CA178E9C1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m */; }; + CA178E9E1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = CA178E9C1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m */; }; + CA178E9F1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = CA178E9C1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m */; }; CEBC15711A95452000CB7B66 /* TiAppiOSUserDefaultsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBC15701A95452000CB7B66 /* TiAppiOSUserDefaultsProxy.m */; }; CEBC15721A95452000CB7B66 /* TiAppiOSUserDefaultsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBC15701A95452000CB7B66 /* TiAppiOSUserDefaultsProxy.m */; }; CEBC15731A95452000CB7B66 /* TiAppiOSUserDefaultsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBC15701A95452000CB7B66 /* TiAppiOSUserDefaultsProxy.m */; }; @@ -1521,6 +1524,8 @@ BB26FC7F19AB13B9007A35AF /* TiAppiOSNotificationCategoryProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TiAppiOSNotificationCategoryProxy.m; sourceTree = ""; }; BBDD81321A2C71C9003CDA10 /* TiUIAttributedStringProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TiUIAttributedStringProxy.h; sourceTree = ""; }; BBDD81331A2C71C9003CDA10 /* TiUIAttributedStringProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TiUIAttributedStringProxy.m; sourceTree = ""; }; + CA178E9B1B4087F5002A7702 /* TiAppiOSUserActivityProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TiAppiOSUserActivityProxy.h; sourceTree = ""; }; + CA178E9C1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TiAppiOSUserActivityProxy.m; sourceTree = ""; }; CEBC156F1A95452000CB7B66 /* TiAppiOSUserDefaultsProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TiAppiOSUserDefaultsProxy.h; sourceTree = ""; }; CEBC15701A95452000CB7B66 /* TiAppiOSUserDefaultsProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TiAppiOSUserDefaultsProxy.m; sourceTree = ""; }; D4AB36F3123DE20200DED4A0 /* TiUIClipboardProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TiUIClipboardProxy.h; sourceTree = ""; }; @@ -1749,6 +1754,8 @@ 24ADC50F1299F5A50014DB75 /* iOS */ = { isa = PBXGroup; children = ( + CA178E9B1B4087F5002A7702 /* TiAppiOSUserActivityProxy.h */, + CA178E9C1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m */, 24ADC5101299F60C0014DB75 /* TiAppiOSProxy.h */, 24ADC5111299F60C0014DB75 /* TiAppiOSProxy.m */, 24ADC5141299F6AA0014DB75 /* TiAppiOSBackgroundServiceProxy.h */, @@ -3479,6 +3486,7 @@ 2B94603713F0A2AE000C5BEA /* TiUIiOSCoverFlowViewProxy.m in Sources */, 1DCC542F13FF0B0800DF3EE5 /* TIDOMCharacterDataProxy.m in Sources */, 1D19459613FF3BC400E2B4D0 /* TIDOMDOMImplementationProxy.m in Sources */, + CA178E9D1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m in Sources */, 1D19459B13FF3BE500E2B4D0 /* TIDOMDocumentTypeProxy.m in Sources */, 3186097F192BDB4E00093482 /* AnalyticsModule.m in Sources */, EF0ADCFA143F9ABF00977386 /* NSData+Additions.m in Sources */, @@ -3758,6 +3766,7 @@ DADD76EA13CCF4FF00CD03AC /* MGSplitView.m in Sources */, 2B94603813F0A2AE000C5BEA /* TiUIiOSCoverFlowView.m in Sources */, 2B94603913F0A2AE000C5BEA /* TiUIiOSCoverFlowViewProxy.m in Sources */, + CA178E9E1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m in Sources */, 1DCC543013FF0B0800DF3EE5 /* TIDOMCharacterDataProxy.m in Sources */, 31860980192BDB4E00093482 /* AnalyticsModule.m in Sources */, 1D19459713FF3BC400E2B4D0 /* TIDOMDOMImplementationProxy.m in Sources */, @@ -4037,6 +4046,7 @@ DADD76EB13CCF4FF00CD03AC /* MGSplitView.m in Sources */, 2B94603A13F0A2AE000C5BEA /* TiUIiOSCoverFlowView.m in Sources */, 2B94603B13F0A2AE000C5BEA /* TiUIiOSCoverFlowViewProxy.m in Sources */, + CA178E9F1B4087F5002A7702 /* TiAppiOSUserActivityProxy.m in Sources */, 1DCC543113FF0B0800DF3EE5 /* TIDOMCharacterDataProxy.m in Sources */, 31860981192BDB4E00093482 /* AnalyticsModule.m in Sources */, 1D19459813FF3BC400E2B4D0 /* TIDOMDOMImplementationProxy.m in Sources */, From 9dd7f20cb249f634dd9a634a05894b05c743be71 Mon Sep 17 00:00:00 2001 From: Ben Bahrenburg Date: Sun, 28 Jun 2015 19:53:26 -0700 Subject: [PATCH 2/5] update TiAppiOSProxy to reference create user activity proxy --- iphone/Classes/TiAppiOSProxy.m | 13 ++++++++++++- iphone/Classes/TiAppiOSUserActivityProxy.h | 2 +- iphone/Classes/TiAppiOSUserActivityProxy.m | 15 ++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/iphone/Classes/TiAppiOSProxy.m b/iphone/Classes/TiAppiOSProxy.m index cac29414584..fbe2f6e9569 100644 --- a/iphone/Classes/TiAppiOSProxy.m +++ b/iphone/Classes/TiAppiOSProxy.m @@ -15,7 +15,7 @@ #import "TiAppiOSNotificationActionProxy.h" #import "TiAppiOSNotificationCategoryProxy.h" #import "TiAppiOSUserDefaultsProxy.h" - +#import "TiAppiOSUserActivityProxy.h" @implementation TiAppiOSProxy @@ -139,6 +139,17 @@ -(void)_listenerRemoved:(NSString*)type count:(int)count #pragma mark Public +-(id)createUserActivity:(id)args +{ + NSString* activityType; + ENSURE_SINGLE_ARG(args,NSDictionary); + ENSURE_ARG_FOR_KEY(activityType, args, @"activityType", NSString); + + TiAppiOSUserActivityProxy *userActivityProxy = [[[TiAppiOSUserActivityProxy alloc] initWithOptions:args] autorelease]; + + return userActivityProxy; +} + -(id)createUserDefaults:(id)args { NSString *suiteName; diff --git a/iphone/Classes/TiAppiOSUserActivityProxy.h b/iphone/Classes/TiAppiOSUserActivityProxy.h index 854ca979276..73fdf7a3c91 100644 --- a/iphone/Classes/TiAppiOSUserActivityProxy.h +++ b/iphone/Classes/TiAppiOSUserActivityProxy.h @@ -13,7 +13,7 @@ BOOL _isValid; BOOL _supported; } - +-(id)initWithOptions:(NSDictionary*)props; @property(nonatomic,retain) NSUserActivity *userActivity; @end diff --git a/iphone/Classes/TiAppiOSUserActivityProxy.m b/iphone/Classes/TiAppiOSUserActivityProxy.m index 7dfbf24d309..5381d103601 100644 --- a/iphone/Classes/TiAppiOSUserActivityProxy.m +++ b/iphone/Classes/TiAppiOSUserActivityProxy.m @@ -19,15 +19,16 @@ -(NSString*)apiName return @"Ti.App.iOS.UserActivity"; } --(void)_initWithProperties:(NSDictionary *)properties +-(id)initWithOptions:(NSDictionary*)props { - _supported = [self determineMinRequirements:properties]; - if(_supported) - { - [self buildInitialActivity:properties]; + if (self = [super init]) { + _supported = [self determineMinRequirements:props]; + if(_supported) + { + [self buildInitialActivity:props]; + } } - - [super _initWithProperties:properties]; + return self; } -(void)dealloc From cc9bee12a763e2bcb87a459a92eefdfa14f8f333 Mon Sep 17 00:00:00 2001 From: Ben Bahrenburg Date: Mon, 29 Jun 2015 14:02:12 -0700 Subject: [PATCH 3/5] add documentation --- apidoc/Titanium/App/iOS/UserActivity.yml | 173 +++++++++++++++++++++ iphone/Classes/TiAppiOSUserActivityProxy.m | 52 +++++++ 2 files changed, 225 insertions(+) create mode 100644 apidoc/Titanium/App/iOS/UserActivity.yml diff --git a/apidoc/Titanium/App/iOS/UserActivity.yml b/apidoc/Titanium/App/iOS/UserActivity.yml new file mode 100644 index 00000000000..faa41bf4130 --- /dev/null +++ b/apidoc/Titanium/App/iOS/UserActivity.yml @@ -0,0 +1,173 @@ +--- +name: Titanium.App.iOS.UserActivity +description: | + The UserActivity module is used to enable device Handoff and to create User Activites for the CoreSpotLight feature first + introducted in iOS9. UserActivity requires the activityType to be specified at creation time. Additional properties can be + created either at creation or set individually after creation. +extends: Titanium.Proxy +platforms: [iphone,ipad] +since: "4.3.0" +createable: false +properties: + - name: activityType + summary: The activityType the user activity was created with. + type: String + availability: creation + osver: {ios: {min: "8.0"}} + platforms: [iphone,ipad] + - name: title + summary: An optional, user-visible title for this activity such as a document name or web page title. + type: String + osver: {ios: {min: "8.0"}} + platforms: [iphone,ipad] + - name: userInfo + summary: The userInfo dictionary contains application-specific state needed to continue an activity on another device. + type: Dictionary + osver: {ios: {min: "8.0"}} + platforms: [iphone,ipad] + - name: webpageURL + summary: When no suitable application is installed on a resuming device and the webPageURL is set the user activity will instead be continued in a web browser by loading this resource. + type: String + osver: {ios: {min: "8.0"}} + platforms: [iphone,ipad] + - name: needsSave + summary: If set to true then this user activity will receive a useractivitywillsave event before being sent for continuation on another device. + type: Boolean + osver: {ios: {min: "8.0"}} + platforms: [iphone,ipad] + - name: keyWords + summary: An array of String keywords representing words or phrases that might help the user to find this activity in the application history. + type: Array + osver: {ios: {min: "9.0"}} + platforms: [iphone,ipad] + - name: requiredUserInfoKeys + summary: An array of String keys from the userInfo property which represent the minimal information about this user activity that should be stored for later restoration. + type: Array + osver: {ios: {min: "9.0"}} + platforms: [iphone,ipad] + - name: eligibleForPublicIndexing + summary: A local URL to a JavaScript file containing the code to run in the background. + type: Boolean + osver: {ios: {min: "9.0"}} + platforms: [iphone,ipad] + - name: eligibleForSearch + summary: Set to true if this user activity should be eligible for indexing for any user of this application + description: | + Set to true if this user activity should be eligible for indexing for any user of this application, + on any device, or false if the activity contains private or sensitive information or which would not be useful to other + users if indexed. The activity must also have requiredUserActivityKeys or a webpageURL + type: Boolean + osver: {ios: {min: "9.0"}} + platforms: [iphone,ipad] + - name: eligibleForHandoff + summary: Set to true if this user activity should be eligible to be handed off to another device + type: Boolean + osver: {ios: {min: "9.0"}} + platforms: [iphone,ipad] + - name: expirationDate + summary: Absolute date after which this activity is no longer eligible to be indexed or handed off. + description: | + Absolute date after which this activity is no longer eligible to be indexed or handed off. + + When setting this property the Date format accepted is: "_yyyy_-_MM_-_dd_**T**_HH_**:**_mm_**:**_ss_**.**_SSS_**+0000**" + + When reading this property Date values will be returned as String in the format: "_yyyy_-_MM_-_dd_**T**_HH_**:**_mm_**:**_ss_**.**_SSS_**+0000**" + + type: String + osver: {ios: {min: "9.0"}} + platforms: [iphone,ipad] +methods: + - name: becomeCurrent + summary: | + Marks the receiver as the activity currently in use by the user. + description: | + Marks the receiver as the activity currently in use by the user, for example, the activity associated with the active window. + A newly created activity is eligible for continuation on another device after the first time it becomes current. + platforms: [iphone, ipad] + osver: {ios: {min: "8.0"}} + - name: invalidate + summary: | + Invalidate an activity when it's no longer eligible for continuation. + description: | + Invalidate an activity when it's no longer eligible for continuation, for example, when the window + associated with an activity is closed. An invalid activity cannot become current. + platforms: [iphone, ipad] + osver: {ios: {min: "8.0"}} + - name: resignCurrent + summary: | + If this activity is the current activity, it should stop being so and set the current activity to nothing. + platforms: [iphone, ipad] + osver: {ios: {min: "9.0"}} +events: + - name: useractivitywillsave + summary: | + The user activity will be saved (to be continued or persisted). The receiver should update the activity with current activity state. + properties: + - name: activityType + summary: the activityType of the User Activity triggering the event + type: String + platforms: [iphone, ipad] + + - name: title + summary: The title of the User Activity if defined + type: String + platforms: [iphone, ipad] + + - name: webpageURL + summary: The webpageURL of the User Activity if defined + type: String + platforms: [iphone, ipad] + + - name: userInfo + summary: Dictionary object containing the userInfo data of the User Activity. + type: Dictionary + platforms: [iphone, ipad] + - name: useractivitywascontinued + summary: | + The user activity was continued on another device. + properties: + - name: activityType + summary: the activityType of the User Activity triggering the event + type: String + platforms: [iphone, ipad] + + - name: title + summary: The title of the User Activity if defined + type: String + platforms: [iphone, ipad] + + - name: webpageURL + summary: The webpageURL of the User Activity if defined + type: String + platforms: [iphone, ipad] + + - name: userInfo + summary: Dictionary object containing the userInfo data of the User Activity. + type: Dictionary + platforms: [iphone, ipad] +examples: + - title: Creating a new UserActivity Example + example: | + The following example demonstrates how to create a new UserActivity and mark the activity as + the current activity Handoff should be using when switching between devices. + + It is important to note that all activityTypes must be defined in your tiapp.xml before this + feature can be supported. It is important to check the supported property on your UserActivity + to ensure the activity created is supported on your device. + + #### app.js + var activity = Ti.App.iOS.createUserActivity({ + activityType:'com.setdirection.home', + title:'activity 1', + userInfo:{ + msg:"hello world" + } + }); + + if(!activity.supported){ + alert('activity not supported'); + return; + } + activity.becomeCurrent(); + + diff --git a/iphone/Classes/TiAppiOSUserActivityProxy.m b/iphone/Classes/TiAppiOSUserActivityProxy.m index 5381d103601..e5567f021c2 100644 --- a/iphone/Classes/TiAppiOSUserActivityProxy.m +++ b/iphone/Classes/TiAppiOSUserActivityProxy.m @@ -129,6 +129,16 @@ -(void) buildInitialActivity:(NSDictionary *)props [_userActivity setExpirationDate:[TiUtils dateForUTCDate: [TiUtils stringValue:@"expirationDate" properties:props]]]; } + + if([props objectForKey:@"keyWords"]) + { + [_userActivity setKeywords:[NSSet setWithArray:[props objectForKey:@"keyWords"]]]; + } + + if([props objectForKey:@"requiredUserInfoKeys"]) + { + [_userActivity setRequiredUserInfoKeys:[NSSet setWithArray:[props objectForKey:@"requiredUserInfoKeys"]]]; + } } _userActivity.delegate = self; @@ -379,6 +389,48 @@ -(void)setExpirationDate:(NSString*)UTCDateFormat [_userActivity setExpirationDate:[TiUtils dateForUTCDate:UTCDateFormat]]; } +-(NSArray*)requiredUserInfoKeys +{ + if(![TiUtils isIOS9OrGreater]) + { + return; + } + NSArray *r = [[_userActivity requiredUserInfoKeys] allObjects]; + return r; +} + +-(void)setRequiredUserInfoKeys:(id)keys +{ + ENSURE_TYPE(keys, NSArray); + ENSURE_UI_THREAD(setRequiredUserInfoKeys,keys); + if(![TiUtils isIOS9OrGreater]) + { + return; + } + [_userActivity setRequiredUserInfoKeys:[NSSet setWithArray:keys]]; +} + +-(NSArray*)keyWords +{ + if(![TiUtils isIOS9OrGreater]) + { + return; + } + NSArray *r = [[_userActivity keywords] allObjects]; + return r; +} + +-(void)setKeywords:(id)keys +{ + ENSURE_TYPE(keys, NSArray); + ENSURE_UI_THREAD(setKeywords,keys); + if(![TiUtils isIOS9OrGreater]) + { + return; + } + [_userActivity setKeywords:[NSSet setWithArray:keys]]; +} + -(void)resignCurrent:(id)unused { ENSURE_UI_THREAD(resignCurrent,unused); From 606b7f4fc7ea52b436e5854d9af6a4e172ebd127 Mon Sep 17 00:00:00 2001 From: Ben Bahrenburg Date: Mon, 29 Jun 2015 16:28:46 -0700 Subject: [PATCH 4/5] add handoff details --- apidoc/Titanium/App/iOS/UserActivity.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apidoc/Titanium/App/iOS/UserActivity.yml b/apidoc/Titanium/App/iOS/UserActivity.yml index faa41bf4130..f57d117a64e 100644 --- a/apidoc/Titanium/App/iOS/UserActivity.yml +++ b/apidoc/Titanium/App/iOS/UserActivity.yml @@ -4,6 +4,16 @@ description: | The UserActivity module is used to enable device Handoff and to create User Activites for the CoreSpotLight feature first introducted in iOS9. UserActivity requires the activityType to be specified at creation time. Additional properties can be created either at creation or set individually after creation. + + Handoff will not work in the simulator. You must build and run on a compatibile device. + + Handoff functionality depends on few things: + * You must be logged in to the same iCloud account on each device you wish to use Handoff. + * Handoff broadcasts activities via Bluetooth LE signals, so both the broadcasting and receiving devices must have Bluetooth LE 4.0 support. + * Devices should have been already paired through iCloud. Make sure you login using the same iCloud account. + + Make sure you have two Handoff compatible devices running iOS 8 or later that are logged onto the same iCloud account. + extends: Titanium.Proxy platforms: [iphone,ipad] since: "4.3.0" From 002e3b9aa668623b32d294b7371dd6b2cfccf3f0 Mon Sep 17 00:00:00 2001 From: Ben Bahrenburg Date: Thu, 2 Jul 2015 10:03:52 -0700 Subject: [PATCH 5/5] updates to address remarks --- apidoc/Titanium/App/iOS/UserActivity.yml | 8 +- iphone/Classes/TiAppiOSUserActivityProxy.h | 2 +- iphone/Classes/TiAppiOSUserActivityProxy.m | 157 +++++++++------------ 3 files changed, 69 insertions(+), 98 deletions(-) diff --git a/apidoc/Titanium/App/iOS/UserActivity.yml b/apidoc/Titanium/App/iOS/UserActivity.yml index f57d117a64e..1e6359fcee9 100644 --- a/apidoc/Titanium/App/iOS/UserActivity.yml +++ b/apidoc/Titanium/App/iOS/UserActivity.yml @@ -1,11 +1,11 @@ --- name: Titanium.App.iOS.UserActivity description: | - The UserActivity module is used to enable device Handoff and to create User Activites for the CoreSpotLight feature first - introducted in iOS9. UserActivity requires the activityType to be specified at creation time. Additional properties can be + The UserActivity module is used to enable device Handoff and to create User Activites first introduced in iOS8 and the CoreSpotLight feature + introduced in iOS9. UserActivity requires the activityType to be specified at creation time. Additional properties can be created either at creation or set individually after creation. - Handoff will not work in the simulator. You must build and run on a compatibile device. + Handoff will not work in the simulator. You must build and run on a compatible device. Handoff functionality depends on few things: * You must be logged in to the same iCloud account on each device you wish to use Handoff. @@ -17,7 +17,7 @@ description: | extends: Titanium.Proxy platforms: [iphone,ipad] since: "4.3.0" -createable: false +createable: true properties: - name: activityType summary: The activityType the user activity was created with. diff --git a/iphone/Classes/TiAppiOSUserActivityProxy.h b/iphone/Classes/TiAppiOSUserActivityProxy.h index 73fdf7a3c91..b142f2d6941 100644 --- a/iphone/Classes/TiAppiOSUserActivityProxy.h +++ b/iphone/Classes/TiAppiOSUserActivityProxy.h @@ -1,6 +1,6 @@ /** * Appcelerator Titanium Mobile - * Copyright (c) 2015 by Appcelerator, Inc. All Rights Reserved. + * Copyright (c) 2009-2015 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Apache Public License * Please see the LICENSE included with this distribution for details. */ diff --git a/iphone/Classes/TiAppiOSUserActivityProxy.m b/iphone/Classes/TiAppiOSUserActivityProxy.m index e5567f021c2..a53052963d5 100644 --- a/iphone/Classes/TiAppiOSUserActivityProxy.m +++ b/iphone/Classes/TiAppiOSUserActivityProxy.m @@ -1,6 +1,6 @@ /** * Appcelerator Titanium Mobile - * Copyright (c) 2015 by Appcelerator, Inc. All Rights Reserved. + * Copyright (c) 2009-2015 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Apache Public License * Please see the LICENSE included with this distribution for details. */ @@ -23,8 +23,7 @@ -(id)initWithOptions:(NSDictionary*)props { if (self = [super init]) { _supported = [self determineMinRequirements:props]; - if(_supported) - { + if(_supported){ [self buildInitialActivity:props]; } } @@ -33,8 +32,7 @@ -(id)initWithOptions:(NSDictionary*)props -(void)dealloc { - if(_supported) - { + if(_supported){ [self clean]; } [super dealloc]; @@ -53,26 +51,19 @@ -(BOOL)activityTypeValid:(NSString*)activityType -(BOOL) determineMinRequirements:(NSDictionary *)props { _isValid = NO; - if([TiUtils isIOS8OrGreater]) - { - if([props objectForKey:@"activityType"]) - { + if([TiUtils isIOS8OrGreater]){ + if([props objectForKey:@"activityType"]){ NSArray *supportedActivityTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSUserActivityTypes"]; - if(![self activityTypeValid:[TiUtils stringValue:@"activityType" properties:props]]) - { - NSLog(@"[ERROR] activityType provided is not defined in your projects tiapp.xml file"); + if(![self activityTypeValid:[TiUtils stringValue:@"activityType" properties:props]]){ + DebugLog(@"[ERROR] activityType provided is not defined in your projects tiapp.xml file"); return NO; - } - else - { + }else{ _isValid = YES; return YES; } - } - else - { - NSLog(@"[ERROR] activityType property is required on creation"); + }else{ + DebugLog(@"[ERROR] activityType property is required on creation"); return NO; } } @@ -87,56 +78,45 @@ -(void) buildInitialActivity:(NSDictionary *)props [self instanceMake:activityType]; - if([props objectForKey:@"title"]) - { + if([props objectForKey:@"title"]){ [_userActivity setTitle:[TiUtils stringValue:@"title" properties:props]]; } - if([props objectForKey:@"userInfo"]) - { + if([props objectForKey:@"userInfo"]){ [_userActivity addUserInfoEntriesFromDictionary:[props objectForKey:@"userInfo"]]; } - if([props objectForKey:@"webpageURL"]) - { + if([props objectForKey:@"webpageURL"]){ [_userActivity setWebpageURL:[NSURL URLWithString:[[TiUtils stringValue:@"webpageURL" properties:props] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; } - if([props objectForKey:@"needsSave"]) - { + if([props objectForKey:@"needsSave"]){ [_userActivity setNeedsSave:[TiUtils boolValue:@"needsSave" properties:props]]; } - if([TiUtils isIOS9OrGreater]) - { - if([props objectForKey:@"eligibleForPublicIndexing"]) - { + if([TiUtils isIOS9OrGreater]){ + if([props objectForKey:@"eligibleForPublicIndexing"]){ [_userActivity setEligibleForPublicIndexing:[TiUtils boolValue:@"eligibleForPublicIndexing" properties:props]]; } - if([props objectForKey:@"eligibleForSearch"]) - { + if([props objectForKey:@"eligibleForSearch"]){ [_userActivity setEligibleForSearch:[TiUtils boolValue:@"eligibleForSearch" properties:props]]; } - if([props objectForKey:@"eligibleForHandoff"]) - { + if([props objectForKey:@"eligibleForHandoff"]){ [_userActivity setEligibleForHandoff:[TiUtils boolValue:@"eligibleForHandoff" properties:props]]; } - if([props objectForKey:@"expirationDate"]) - { + if([props objectForKey:@"expirationDate"]){ [_userActivity setExpirationDate:[TiUtils dateForUTCDate: [TiUtils stringValue:@"expirationDate" properties:props]]]; } - if([props objectForKey:@"keyWords"]) - { + if([props objectForKey:@"keyWords"]){ [_userActivity setKeywords:[NSSet setWithArray:[props objectForKey:@"keyWords"]]]; } - if([props objectForKey:@"requiredUserInfoKeys"]) - { + if([props objectForKey:@"requiredUserInfoKeys"]){ [_userActivity setRequiredUserInfoKeys:[NSSet setWithArray:[props objectForKey:@"requiredUserInfoKeys"]]]; } } @@ -169,8 +149,7 @@ -(NSDictionary*)copyActivity -(void)clean { - if(_userActivity !=nil) - { + if(_userActivity !=nil){ _userActivity.delegate = nil; RELEASE_TO_NIL(_userActivity); } @@ -178,8 +157,7 @@ -(void)clean -(void)instanceMake:(NSString*)activityType { - if(_userActivity !=nil) - { + if(_userActivity !=nil){ [_userActivity invalidate]; } @@ -207,8 +185,7 @@ -(NSNumber*)supported */ - (void)userActivityWillSave:(NSUserActivity *)userActivity { - if([self _hasListeners:@"useractivitywillsave"]) - { + if([self _hasListeners:@"useractivitywillsave"]){ [self fireEvent:@"useractivitywillsave" withObject:[self copyActivity]]; } } @@ -217,8 +194,7 @@ - (void)userActivityWillSave:(NSUserActivity *)userActivity */ - (void)userActivityWasContinued:(NSUserActivity *)userActivity { - if([self _hasListeners:@"useractivitywascontinued"]) - { + if([self _hasListeners:@"useractivitywascontinued"]){ [self fireEvent:@"useractivitywascontinued" withObject:[self copyActivity]]; } } @@ -230,19 +206,17 @@ -(NSString*)activityType return [_userActivity activityType]; } --(void)setActivityType:(NSString*)value +-(void)setActivityType:(id)value { + ENSURE_SINGLE_ARG(value,NSString); ENSURE_UI_THREAD(setActivityType,value); - ENSURE_TYPE(value, NSString); - if(![self activityTypeValid:value]) - { - NSLog(@"[ERROR] activityType provided is not defined in your projects tiapp.xml file"); + if(![self activityTypeValid:value]){ + DebugLog(@"[ERROR] activityType provided is not defined in your projects tiapp.xml file"); return; } - if(_userActivity.activityType != value) - { + if(_userActivity.activityType != value){ [self instanceMake:value]; } @@ -253,9 +227,11 @@ -(NSString*)title return [_userActivity title]; } --(void)setTitle:(NSString*)value +-(void)setTitle:(id)value { + ENSURE_SINGLE_ARG(value,NSString); ENSURE_UI_THREAD(setTitle,value); + [_userActivity setTitle:value]; } @@ -264,9 +240,11 @@ -(NSDictionary*)userInfo return [_userActivity userInfo]; } --(void)setUserInfo:(NSDictionary*)info +-(void)setUserInfo:(id)info { + ENSURE_SINGLE_ARG(info,NSDictionary); ENSURE_UI_THREAD(setUserInfo,info); + [_userActivity setUserInfo:info]; } @@ -275,10 +253,10 @@ -(NSString*)webpageURL return [[_userActivity webpageURL] absoluteString]; } --(void)setWebpageURL:(NSString*)value +-(void)setWebpageURL:(id)value { + ENSURE_SINGLE_ARG(value, NSString); ENSURE_UI_THREAD(setWebpageURL,value); - ENSURE_TYPE(value, NSString); [_userActivity setWebpageURL: [NSURL URLWithString:[value stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; @@ -289,9 +267,11 @@ -(NSNumber*)needsSave return NUMBOOL([_userActivity needsSave]); } --(void)setNeedsSave:(NSNumber*)value +-(void)setNeedsSave:(id)value { + ENSURE_SINGLE_ARG(value, NSString); ENSURE_UI_THREAD(setNeedsSave,value); + [_userActivity setNeedsSave:[TiUtils boolValue:value]]; } @@ -311,19 +291,18 @@ -(void)invalidate:(id)unused -(NSNumber*)eligibleForPublicIndexing { - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return NUMBOOL(NO); } return NUMBOOL(_userActivity.eligibleForPublicIndexing); } --(void)setEligibleForPublicIndexing:(NSNumber*)value +-(void)setEligibleForPublicIndexing:(id)value { + ENSURE_SINGLE_ARG(value, NSNumber); ENSURE_UI_THREAD(setEligibleForPublicIndexing,value); - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } [_userActivity setEligibleForPublicIndexing:[TiUtils boolValue:value]]; @@ -331,19 +310,18 @@ -(void)setEligibleForPublicIndexing:(NSNumber*)value -(NSNumber*)eligibleForSearch { - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return NUMBOOL(NO); } return NUMBOOL(_userActivity.eligibleForSearch); } --(void)setEligibleForSearch:(NSNumber*)value +-(void)setEligibleForSearch:(id)value { + ENSURE_SINGLE_ARG(value, NSNumber); ENSURE_UI_THREAD(setEligibleForSearch,value); - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } [_userActivity setEligibleForSearch:[TiUtils boolValue:value]]; @@ -351,18 +329,17 @@ -(void)setEligibleForSearch:(NSNumber*)value -(NSNumber*)eligibleForHandoff { - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return NUMBOOL(NO); } return NUMBOOL(_userActivity.eligibleForHandoff); } --(void)setEligibleForHandoff:(NSNumber*)value +-(void)setEligibleForHandoff:(id)value { + ENSURE_SINGLE_ARG(value, NSNumber); ENSURE_UI_THREAD(setEligibleForHandoff,value); - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } [_userActivity setEligibleForHandoff:[TiUtils boolValue:value]]; @@ -370,19 +347,18 @@ -(void)setEligibleForHandoff:(NSNumber*)value -(NSString*)expirationDate { - if(![TiUtils isIOS9OrGreater] || _userActivity.expirationDate == nil) - { + if(![TiUtils isIOS9OrGreater] || _userActivity.expirationDate == nil){ return nil; } return [TiUtils UTCDateForDate:_userActivity.expirationDate]; } --(void)setExpirationDate:(NSString*)UTCDateFormat +-(void)setExpirationDate:(id)UTCDateFormat { + ENSURE_SINGLE_ARG(UTCDateFormat, NSString); ENSURE_UI_THREAD(setExpirationDate,UTCDateFormat); - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } @@ -391,8 +367,7 @@ -(void)setExpirationDate:(NSString*)UTCDateFormat -(NSArray*)requiredUserInfoKeys { - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } NSArray *r = [[_userActivity requiredUserInfoKeys] allObjects]; @@ -401,10 +376,9 @@ -(NSArray*)requiredUserInfoKeys -(void)setRequiredUserInfoKeys:(id)keys { - ENSURE_TYPE(keys, NSArray); + ENSURE_SINGLE_ARG(keys, NSArray); ENSURE_UI_THREAD(setRequiredUserInfoKeys,keys); - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } [_userActivity setRequiredUserInfoKeys:[NSSet setWithArray:keys]]; @@ -412,8 +386,7 @@ -(void)setRequiredUserInfoKeys:(id)keys -(NSArray*)keyWords { - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } NSArray *r = [[_userActivity keywords] allObjects]; @@ -422,10 +395,9 @@ -(NSArray*)keyWords -(void)setKeywords:(id)keys { - ENSURE_TYPE(keys, NSArray); + ENSURE_SINGLE_ARG(keys, NSArray); ENSURE_UI_THREAD(setKeywords,keys); - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } [_userActivity setKeywords:[NSSet setWithArray:keys]]; @@ -434,8 +406,7 @@ -(void)setKeywords:(id)keys -(void)resignCurrent:(id)unused { ENSURE_UI_THREAD(resignCurrent,unused); - if(![TiUtils isIOS9OrGreater]) - { + if(![TiUtils isIOS9OrGreater]){ return; } [_userActivity resignCurrent];