diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..cf48aac --- /dev/null +++ b/.jshintrc @@ -0,0 +1,16 @@ +{ + "browser": true + , "devel": true + , "bitwise": true + , "undef": true + , "trailing": true + , "quotmark": false + , "indent": 4 + , "unused": "vars" + , "latedef": "nofunc" + , "globals": { + "module": false, + "exports": false, + "require": false + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9c7af50 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: objective-c +node_js: + - "4.2" +install: + - npm install -g cordova + - npm install -g ios-sim + - npm install -g ios-deploy + - npm install -g cordova-paramedic + - npm install +script: + - npm test + - cordova-paramedic --platform ios --plugin /Users/travis/build/johanblomgren/cordova-plugin-indexappcontent diff --git a/README.md b/README.md index b044eab..9641c27 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,33 @@ +|Travis CI| +|:-:| +|[![Build Status](https://travis-ci.org/johanblomgren/cordova-plugin-indexappcontent.svg?branch=master)](https://travis-ci.org/johanblomgren/cordova-plugin-indexappcontent)| + +## Overview +This Cordova Plugin gives you a Javascript API to interact with [Core Spotlight](https://developer.apple.com/reference/corespotlight) on iOS (=> iOS 9). You can add, update and delete items to the spotlight search index. [Spotlight](https://en.wikipedia.org/wiki/Spotlight_(software)) Search will include these items in the result list. You can deep-link the search results with your app. + ## Installation Install using ``cordova`` CLI. * Run ``cordova plugin add https://github.com/johanblomgren/cordova-plugin-indexappcontent.git`` ## Usage -Plugin should be installed on ``window.plugins.indexAppContent``. +This plugin defines a global `window.plugins.indexAppContent` object. + +### Is Indexing Available +The option to index app content might not be available at all due to device limitations or user settings. Therefore it's highly recommended to check upfront if indexing is possible. -### Initialization (required) -Calling ``window.plugins.indexAppContent.init()`` will explicitly tell the native component to initialize. +``window.plugins.indexAppContent.isIndexingAvailable(fnCallback)`` will invoke the callback with a boolean value to indicate if indexing is possible or not. +``` +window.plugins.indexAppContent.isIndexingAvailable(function(bIsAvailable){ + if (bIsAvailable === true) { + // let's go ahead and index your content + } +}) +``` + +Please note that the function does not consider possible time restrictions imposed by ``setIndexingInterval``. ### Set items -``window.plugins.indexAppContent.setItems(items, success, error)`` expects at least one parameter, ``items``, which is an array of objects with the following structure: +Call ``window.plugins.indexAppContent.setItems(aItems, success, error)`` to add or change items to the spotlight index. Function expects at least one parameter, ``aItems``, which is an array of objects with the following structure: ``` { domain: 'com.my.domain', @@ -27,7 +45,7 @@ Calling ``window.plugins.indexAppContent.init()`` will explicitly tell the nativ Example: ``` -var items = [ +var aItems = [ { domain: 'com.my.domain', identifier: '88asdf7dsf', @@ -44,16 +62,18 @@ var items = [ } ]; -window.plugins.indexAppContent.setItems(items, function() { - console.log('Successfully set items'); - }, function(error) { - // Handle error - }); +window.plugins.indexAppContent.setItems(aItems, function() { + console.log('Successfully set items'); +}, function(sError) { + console.log("Error when trying to add/modify index: " + sError); +}); ``` Image data will be downloaded and stored in the background. -### Set handler +### On Item Pressed +If user taps on a search result in spotlight then the app will be launched. You can register a Javascript handler to get informed when this happens. + Assign a handler function to ``window.plugins.indexAppContent.onItemPressed`` that takes the payload as argument, like so: ``` @@ -62,47 +82,65 @@ window.plugins.indexAppContent.onItemPressed = function(payload) { } ``` -This handler will be called when launching the app by pressing an item in spotlight search results. - -NOTE: Set this handler before calling ``window.plugins.indexAppContent.init()``. A call to ``init()`` will tell the native code that the handler is ready to be used when the app is launched by tapping on a search result. - ### Clear items -Call ``window.plugins.indexAppContent.clearItemsForDomains(domains, success, error)`` to clear all items stored for a given array of domains. +Call ``window.plugins.indexAppContent.clearItemsForDomains(aDomains, fnSuccess, fnError)`` to clear all items stored for a given array of domains. Example: ``` window.plugins.indexAppContent.clearItemsForDomains(['com.my.domain', 'com.my.other.domain'], function() { console.log('Items removed'); - }, function(error) { - // Handle error - }); - +}, function(sError) { + console.log("Error when trying to clear items: " + sError); +}); ``` -Call ``window.plugins.indexAppContent.clearItemsForIdentifiers(identifiers, success, error)`` to clear all items stored for a given array of identifiers. +Call ``window.plugins.indexAppContent.clearItemsForIdentifiers(aIdentifiers, fnSuccess, fnError)`` to clear all items stored for a given array of identifiers. Example: ``` window.plugins.indexAppContent.clearItemsForIdentifiers(['id1', 'id2'], function() { console.log('Items removed'); - }, function(error) { - // Handle error - }); +}, function(sError) { + console.log("Error when trying to clear items: " + sError); +}); ``` ### Set indexing interval -Call ``window.plugins.indexAppContent.setIndexingInterval(interval, success, error)`` to configure the interval (in minutes) for how often indexing should be allowed. +You might want to avoid to update spotlight index too frequently. Call ``window.plugins.indexAppContent.setIndexingInterval(iIntervalInMinutes, fnSuccess, fnError)`` to configure a time interval (in minutes) to define when indexing operations are allowed since your last spotlight index update. First parameter must be numeric and => 0. Example: ``` window.plugins.indexAppContent.setIndexingInterval(60, function() { - // Console.log('Successfully set interval'); - }, function(error) { - // Handle error - }); + console.log('Successfully set interval'); +}, function(sError) { + console.log("Error when trying to set time interval: " + sError); +}); ``` + +Without calling this function a subsequent call to manipulate the index is only possible after 1440 minutes (= 24 hours) ! + +Example: +- You call ```setIndexingInterval``` and specify 5min. You call ```setItems``` for the first time and function will be executed successfully. +- You call ```setItems``` again after 2min. Spotlight index will NOT be updated and error callback gets invoked. +- You call ```setItems``` after 6 min and function will be executed successfully. + +## Tests + +The plugin is covered by automatic and manual tests implemented in Jasmine and following the [Cordova test framework](https://github.com/apache/cordova-plugin-test-framework) approach. + +You can create a test application with the tests by doing the following steps: + +``` +cordova create indexAppContentTestApp --template cordova-template-test-framework +cd indexAppContentTestApp +cordova platform add ios +cordova plugin add https://github.com/johanblomgren/cordova-plugin-indexappcontent +cordova plugin add https://github.com/johanblomgren/cordova-plugin-indexappcontent/tests +``` + +As an alternative you can use [Cordova Paramedic](https://github.com/apache/cordova-paramedic) to run them. diff --git a/package.json b/package.json index 602ef41..3dd3ef7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-indexappcontent", - "version": "0.1.0", + "version": "0.5.0", "description": "Enable on-device searchable app content indexing.", "cordova": { "id": "cordova-plugin-indexappcontent", @@ -25,9 +25,16 @@ "version": ">=3.5.0" } ], + "scripts": { + "test": "npm run jshint", + "jshint": "node node_modules/jshint/bin/jshint www && node node_modules/jshint/bin/jshint src && node node_modules/jshint/bin/jshint tests" + }, "author": "Johan Blomgren ", "license": "MIT", "bugs": { "url": "https://github.com/johanblomgren/cordova-plugin-indexappcontent/issues" + }, + "devDependencies": { + "jshint": "^2.6.0" } } diff --git a/plugin.xml b/plugin.xml index d895260..c559d8f 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ Index App Content @@ -18,12 +18,12 @@ - - - - - + + + + + @@ -35,10 +35,10 @@ - + - + diff --git a/src/ios/app/AppDelegate+IndexAppContent.m b/src/ios/app/AppDelegate+IndexAppContent.m index db256aa..aa29aa9 100644 --- a/src/ios/app/AppDelegate+IndexAppContent.m +++ b/src/ios/app/AppDelegate+IndexAppContent.m @@ -7,41 +7,100 @@ #import "AppDelegate+IndexAppContent.h" #import "IndexAppContent.h" +#import #define kCALL_DELAY_MILLISECONDS 25 @implementation AppDelegate (IndexAppContent) -- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler +/* + handle the case that another category or class in the class hierachy of AppDelegate already implements the UIApplicationDelegate and handOff method "application:continueUserActivity:restorationHandler:" + Use method swizzling to archieve the following: + - call the original implementation (which handles other use cases like universal links) + - call our own implementation to handle CSSearchableItemActionType (if userActivity was not yet handled) + */ ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + SEL originalHandOffSEL, swizzledHandOffSEL, spotlightHandOffSEL; + Method originalHandOffMethod, swizzledHandOffMethod, spotlightHandOffMethod; + + Class thisClass = [self class]; + + originalHandOffSEL = @selector(application:continueUserActivity:restorationHandler:); + swizzledHandOffSEL = @selector(swizzledHandOff:continueUserActivity:restorationHandler:); + spotlightHandOffSEL = @selector(indexAppContent_application:continueUserActivity:restorationHandler:); + + BOOL originalHandOffExists = [self instancesRespondToSelector:originalHandOffSEL]; + + originalHandOffMethod = class_getInstanceMethod(thisClass, originalHandOffSEL); + swizzledHandOffMethod = class_getInstanceMethod(thisClass, swizzledHandOffSEL); + spotlightHandOffMethod = class_getInstanceMethod(thisClass, spotlightHandOffSEL); + + BOOL didAddMethod = class_addMethod(thisClass, originalHandOffSEL, method_getImplementation(swizzledHandOffMethod), method_getTypeEncoding(swizzledHandOffMethod)); + + if (didAddMethod) { + if (!originalHandOffExists) { + class_replaceMethod(thisClass, swizzledHandOffSEL, method_getImplementation(spotlightHandOffMethod), method_getTypeEncoding(spotlightHandOffMethod)); + } else { + class_replaceMethod(thisClass, swizzledHandOffSEL, method_getImplementation(originalHandOffMethod), method_getTypeEncoding(originalHandOffMethod)); + } + } else { + method_exchangeImplementations(originalHandOffMethod, swizzledHandOffMethod); + } + }); +} + +- (BOOL)swizzledHandOff:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler { + + BOOL orginalHandOffImplementedHandledCase = [self swizzledHandOff:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + if (orginalHandOffImplementedHandledCase == NO) { + return [self indexAppContent_application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + } else { + NSLog(@"Another implementation (e.g. plugin) already handled that userActivity"); + return YES; + } +} + +- (BOOL)indexAppContent_application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler { if ([userActivity.activityType isEqualToString:CSSearchableItemActionType]) { - // Get the item identifier and use it NSString *identifier = userActivity.userInfo[CSSearchableItemActivityIdentifier]; - NSString *jsFunction = @"window.plugins.indexAppContent.onItemPressed"; NSString *params = [NSString stringWithFormat:@"{'identifier':'%@'}", identifier]; NSString *result = [NSString stringWithFormat:@"%@(%@)", jsFunction, params]; [self callJavascriptFunctionWhenAvailable:result]; + return YES; + } else { + NSLog(@"userActivity is not related to spotlight and therefore does not get handled"); + return NO; } - - return YES; } - (void)callJavascriptFunctionWhenAvailable:(NSString *)function { - IndexAppContent *indexAppContent = [self.viewController getCommandInstance:@"IndexAppContent"]; - if (indexAppContent.initDone && indexAppContent.ready) { - [self sendCommand:function webViewEngine:indexAppContent.webViewEngine]; - } else { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kCALL_DELAY_MILLISECONDS * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ - [self callJavascriptFunctionWhenAvailable:function]; - }); - } + __weak __typeof(self) weakSelf = self; + __block NSString *command = function; + + __block void (^checkAndExecute)( ) = ^void( ) { + NSString *check = @"(window && window.plugins && window.plugins.indexAppContent && typeof window.plugins.indexAppContent.onItemPressed == 'function') ? true : false"; + IndexAppContent *indexAppContent = [weakSelf.viewController getCommandInstance:@"IndexAppContent"]; + [weakSelf sendCommand:check webViewEngine:indexAppContent.webViewEngine completionHandler:^(id returnValue, NSError * error) { + if (error || [returnValue boolValue] == NO) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kCALL_DELAY_MILLISECONDS * NSEC_PER_MSEC), dispatch_get_main_queue(), checkAndExecute); + } else if ([returnValue boolValue] == YES) { + [self sendCommand:command webViewEngine:indexAppContent.webViewEngine completionHandler:nil]; + } + }]; + }; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_MSEC), dispatch_get_main_queue(), checkAndExecute); } -- (void)sendCommand:(NSString *)command webViewEngine:(id)webViewEngine +- (void)sendCommand:(NSString *)command webViewEngine:(id)webViewEngine completionHandler:(void (^)(id, NSError*))completionHandler { - [webViewEngine evaluateJavaScript:command completionHandler:nil]; + [webViewEngine evaluateJavaScript:command completionHandler:completionHandler]; } @end diff --git a/src/ios/app/IndexAppContent.h b/src/ios/app/IndexAppContent.h index c884d48..d6e9095 100644 --- a/src/ios/app/IndexAppContent.h +++ b/src/ios/app/IndexAppContent.h @@ -9,15 +9,9 @@ #import #import -UIKIT_EXTERN NSString *kIndexAppContentDelayExecutionNotification; -UIKIT_EXTERN NSString *kIndexAppContentExecutionDelayKey; - @interface IndexAppContent : CDVPlugin -@property BOOL initDone; -@property BOOL ready; - -- (void)deviceIsReady:(CDVInvokedUrlCommand *)command; +- (void)isIndexingAvailable:(CDVInvokedUrlCommand *)command; - (void)setItems:(CDVInvokedUrlCommand *)command; - (void)clearItemsForDomains:(CDVInvokedUrlCommand *)command; - (void)clearItemsForIdentifiers:(CDVInvokedUrlCommand *)command; diff --git a/src/ios/app/IndexAppContent.m b/src/ios/app/IndexAppContent.m index efff5b5..e124a0c 100644 --- a/src/ios/app/IndexAppContent.m +++ b/src/ios/app/IndexAppContent.m @@ -10,9 +10,6 @@ #import "UIKit/UITouch.h" #import "IndexAppContent.h" -NSString *kIndexAppContentDelayExecutionNotification = @"kIndexAppContentDelayExecutionNotification"; -NSString *kIndexAppContentExecutionDelayKey = @"kIndexAppContentExecutionDelayKey"; - @interface IndexAppContent () { dispatch_group_t _group; dispatch_queue_t _queue; @@ -29,23 +26,17 @@ @interface IndexAppContent () { @implementation IndexAppContent -#pragma mark - Public +#pragma mark - Public (Overriden) -- (void)deviceIsReady:(CDVInvokedUrlCommand *)command -{ - self.initDone = YES; +- (void)onAppTerminate { + [[NSUserDefaults standardUserDefaults] synchronize]; } -- (void)pluginInitialize -{ - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIndexAppContentDelayExecutionNotification:) name:kIndexAppContentDelayExecutionNotification object:nil]; - - self.ready = YES; -} +#pragma mark - Public -- (void)dealloc +- (void)isIndexingAvailable:(CDVInvokedUrlCommand *)command { - [[NSNotificationCenter defaultCenter] removeObserver:self name:kIndexAppContentDelayExecutionNotification object:nil]; + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:[CSSearchableIndex isIndexingAvailable]] callbackId:command.callbackId]; } - (void)setItems:(CDVInvokedUrlCommand *)command @@ -99,9 +90,9 @@ - (void)setItems:(CDVInvokedUrlCommand *)command [[CSSearchableIndex defaultSearchableIndex] indexSearchableItems:searchableItems completionHandler:^(NSError * _Nullable error) { if (error) { NSLog(@"%@", error); - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR] callbackId:command.callbackId]; + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error localizedDescription]] callbackId:command.callbackId]; } else { - NSLog(@"Indexing complete. Next indexing at %@", [self _getTimestamp]); + NSLog(@"Indexing complete. Next indexing possible in %zd", [self _getIndexingInterval]); [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; } }]; @@ -115,7 +106,7 @@ - (void)clearItemsForDomains:(CDVInvokedUrlCommand *)command [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithDomainIdentifiers:domains completionHandler:^(NSError * _Nullable error) { if (error) { NSLog(@"%@", error); - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR] callbackId:command.callbackId]; + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error localizedDescription]] callbackId:command.callbackId]; } else { NSLog(@"Index removed for domains %@", domains); [self _clearTimestamp]; @@ -131,7 +122,7 @@ - (void)clearItemsForIdentifiers:(CDVInvokedUrlCommand *)command [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithIdentifiers:identifiers completionHandler:^(NSError * _Nullable error) { if (error) { NSLog(@"%@", error); - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR] callbackId:command.callbackId]; + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error localizedDescription]] callbackId:command.callbackId]; } else { NSLog(@"Items removed with identifiers %@", identifiers); [self _clearTimestamp]; @@ -147,33 +138,21 @@ - (void)setIndexingInterval:(CDVInvokedUrlCommand *)command if ([self _setIndexingInterval:interval]) { [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; } else { - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR] callbackId:command.callbackId]; + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Invalid time interval"] callbackId:command.callbackId]; } } -#pragma mark - Notifications - -- (void)handleIndexAppContentDelayExecutionNotification:(NSNotification *)notification -{ - float delay = [notification.userInfo[kIndexAppContentExecutionDelayKey] floatValue]; - - self.ready = NO; - - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC); - dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ - self.ready = YES; - }); -} - #pragma mark - Private - (BOOL)_shouldUpdateIndex { - NSDate *updatedAt = [self _getTimestamp]; + NSDate *indexLastUpdatedAt = [self _getTimestamp]; + NSInteger timeIntervalInMinutesUntilNextUpdateIsProhibited = [self _getIndexingInterval]; + int minutesSinceLastIndexUpdate = [self _getMinutesSince:indexLastUpdatedAt]; BOOL shouldUpdate = YES; - if (updatedAt && [updatedAt compare:[NSDate date]] == NSOrderedDescending) { - NSLog(@"Will not update index. Last update: %@", updatedAt); + if (indexLastUpdatedAt && minutesSinceLastIndexUpdate < timeIntervalInMinutesUntilNextUpdateIsProhibited) { + NSLog(@"Will not update index. Last update: %@", indexLastUpdatedAt); shouldUpdate = NO; } else { [self _setTimestamp]; @@ -184,10 +163,11 @@ - (BOOL)_shouldUpdateIndex - (void)_setTimestamp { - NSDate *nextDate = [self _dateByMinuteOffset:[self _getIndexingInterval]]; + [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:kINDEX_TIMESTAMP_KEY]; +} - [[NSUserDefaults standardUserDefaults] setObject:nextDate forKey:kINDEX_TIMESTAMP_KEY]; - [[NSUserDefaults standardUserDefaults] synchronize]; +- (int)_getMinutesSince:(NSDate*)date { + return [[NSDate date] timeIntervalSinceDate:date] / 60; } - (NSDate *)_dateByMinuteOffset:(NSInteger)minuteOffset @@ -210,19 +190,20 @@ - (void)_clearTimestamp - (NSInteger)_getIndexingInterval { - NSInteger interval = [[[NSUserDefaults standardUserDefaults] objectForKey:kINDEXING_INTERVAL_KEY] integerValue]; - - return interval ? interval : kDEFAULT_INDEXING_INTERVAL; + if (![[NSUserDefaults standardUserDefaults] objectForKey:kINDEXING_INTERVAL_KEY]) { // nil + return kDEFAULT_INDEXING_INTERVAL; + } else { // 0 or greater + return [[[NSUserDefaults standardUserDefaults] objectForKey:kINDEXING_INTERVAL_KEY] integerValue]; + } } - (BOOL)_setIndexingInterval:(NSInteger)interval { - if (!interval) { + if (interval < 0) { return NO; } [[NSUserDefaults standardUserDefaults] setObject:@(interval) forKey:kINDEXING_INTERVAL_KEY]; - [[NSUserDefaults standardUserDefaults] synchronize]; return YES; } diff --git a/tests/plugin.xml b/tests/plugin.xml new file mode 100644 index 0000000..3396c73 --- /dev/null +++ b/tests/plugin.xml @@ -0,0 +1,6 @@ + + + Index App Content Tests + + + diff --git a/tests/tests.js b/tests/tests.js new file mode 100644 index 0000000..17ddbae --- /dev/null +++ b/tests/tests.js @@ -0,0 +1,230 @@ +/* jshint jasmine: true */ +exports.defineAutoTests = function() { + + describe('Plugin', function() { + it("should exist", function() { + expect(window.plugins.indexAppContent).toBeDefined(); + }); + + it("should offer a isIndexingAvailable function", function() { + expect(window.plugins.indexAppContent.isIndexingAvailable).toBeDefined(); + expect(typeof window.plugins.indexAppContent.isIndexingAvailable == 'function').toBe(true); + }); + + it("should offer a setItems function", function() { + expect(window.plugins.indexAppContent.setItems).toBeDefined(); + expect(typeof window.plugins.indexAppContent.setItems == 'function').toBe(true); + }); + + it("should offer a clearItemsForDomains function", function() { + expect(window.plugins.indexAppContent.clearItemsForDomains).toBeDefined(); + expect(typeof window.plugins.indexAppContent.clearItemsForDomains == 'function').toBe(true); + }); + + it("should offer a clearItemsForIdentifiers function", function() { + expect(window.plugins.indexAppContent.clearItemsForIdentifiers).toBeDefined(); + expect(typeof window.plugins.indexAppContent.clearItemsForIdentifiers == 'function').toBe(true); + }); + + it("should offer a setIndexingInterval function", function() { + expect(window.plugins.indexAppContent.setIndexingInterval).toBeDefined(); + expect(typeof window.plugins.indexAppContent.setIndexingInterval == 'function').toBe(true); + }); + }); + + describe('setItems', function() { + + it("shall not harm when calling without any parameters", function() { + window.plugins.indexAppContent.setItems(); + expect("noJsError").toBeTruthy(); + }); + + it("shall invoke error callback in case of no input", function(done) { + window.plugins.indexAppContent.setItems(undefined, function() { + done.fail(); + }, function(sError) { + expect(sError).toBe("No items"); + done(); + }); + + }); + + it("shall invoke error callback in case of incorrect input", function(done) { + window.plugins.indexAppContent.setItems("No Array with Item", function() { + done.fail(); + }, function(sError) { + expect(sError).toBe("No items"); + done(); + }); + }); + }); + + describe('clearItemsForDomains', function() { + + it("shall not harm when calling without any parameters", function() { + window.plugins.indexAppContent.clearItemsForDomains(); + expect("noJsError").toBeTruthy(); + }); + + it("shall invoke error callback in case of no input", function(done) { + window.plugins.indexAppContent.clearItemsForDomains(undefined, function() { + done.fail(); + }, function(sError) { + expect(sError).toBe("No domains"); + done(); + }); + + }); + + it("shall invoke error callback in case of incorrect input", function(done) { + window.plugins.indexAppContent.clearItemsForDomains("No Array with domains", function() { + done.fail(); + }, function(sError) { + expect(sError).toBe("No domains"); + done(); + }); + }); + }); + + describe('clearItemsForIdentifiers', function() { + + it("shall not harm when calling without any parameters", function() { + window.plugins.indexAppContent.clearItemsForIdentifiers(); + expect("noJsError").toBeTruthy(); + }); + + it("shall invoke error callback in case of no input", function(done) { + window.plugins.indexAppContent.clearItemsForIdentifiers(undefined, function() { + done.fail(); + }, function(sError) { + expect(sError).toBe("No identifiers"); + done(); + }); + + }); + + it("shall invoke error callback in case of incorrect input", function(done) { + window.plugins.indexAppContent.clearItemsForIdentifiers("No Array with identifiers", function() { + done.fail(); + }, function(sError) { + expect(sError).toBe("No identifiers"); + done(); + }); + }); + }); + + describe('setIndexingInterval', function() { + + it("shall not harm when calling without any parameters", function() { + window.plugins.indexAppContent.setIndexingInterval(); + expect("noJsError").toBeTruthy(); + }); + + it("shall invoke error callback in case of values < 0", function(done) { + window.plugins.indexAppContent.setIndexingInterval(-1, function() { + done.fail(); + }, function(sError) { + expect(sError).toBeDefined(); + done(); + }); + }); + + it("shall accept 0 as value and invoke success callback", function(done) { + window.plugins.indexAppContent.setIndexingInterval(0, function() { + expect("success").toBeTruthy(); + done(); + }, function() { + done.fail(); + }); + }); + + it("shall accept values > 0 and invoke success callback", function(done) { + window.plugins.indexAppContent.setIndexingInterval(77, function() { + expect("success").toBeTruthy(); + done(); + }, function() { + done.fail(); + }); + }); + }); + + describe('Integration test', function() { + + it("shall perform basic sequence of calls", function(done) { + + var fnCreateAndDelete = function() { + var oItem = { + domain: 'com.my.domain', + identifier: '88asdf7dsf', + title: 'Foo', + description: 'Bar', + url: 'http://location/of/my/image.jpg', + keywords: ['This', 'is', 'optional'], // Item keywords (optional) + lifetime: 1440 // Lifetime in minutes (optional) + }; + window.plugins.indexAppContent.setItems([oItem], function() { + window.plugins.indexAppContent.clearItemsForDomains(['com.my.domain'], function() { + expect("everythingSeemsToWork").toBeTruthy(); + done(); + }, function() { + done.fail(); + }); + }, function(sError) { + done.fail(); + }); + }; + + window.plugins.indexAppContent.setIndexingInterval(0, function() { + window.plugins.indexAppContent.clearItemsForIdentifiers(['88asdf7dsf'], function() { + fnCreateAndDelete(); + }, function() { + fnCreateAndDelete(); + }); + }, function(sError) { + done.fail(); + }); + }); + }); +}; + +exports.defineManualTests = function(contentEl, createActionButton) { + + createActionButton('Is Index Available?', function() { + window.plugins.indexAppContent.isIndexingAvailable(function(bIsAvailable) { + console.log('Indexing available: ' + bIsAvailable); + }); + }); + + createActionButton('Add item', function() { + var oItem = { + domain: 'com.my.domain', + identifier: '88asdf7dsf', + title: 'Foo', + description: 'Bar', + url: 'http://location/of/my/image.jpg', + keywords: ['This', 'is', 'optional'], // Item keywords (optional) + lifetime: 1440 // Lifetime in minutes (optional) + }; + window.plugins.indexAppContent.setItems([oItem], function() { + console.log("Item with identifier 88asdf7dsf created"); + }, function(sError) { + console.log("error: " + sError); + }); + }); + + createActionButton('Set OnItemPressed handler', function() { + + window.plugins.indexAppContent.onItemPressed = function(oItem) { + console.log("Item with identifier " + oItem.identifier + " was invoked from spotlight search"); + }; + console.log("Javascript handler set"); + console.log("If you click on spotligh item (use manual test to create one first) then info gets logged by JS handler"); + }); + + createActionButton('Clear OnItemPressed handler', function() { + window.plugins.indexAppContent.onItemPressed = {}; + console.log("Javascript handler cleared'"); + console.log("Clicking on spotlight item will still launch the app. As soon as handler gets set then handler will be called"); + }); + +}; diff --git a/www/IndexAppContent.js b/www/IndexAppContent.js index ead153e..13a61e7 100644 --- a/www/IndexAppContent.js +++ b/www/IndexAppContent.js @@ -2,24 +2,60 @@ var exec = require("cordova/exec"); var IndexAppContent = function () {}; -IndexAppContent.prototype.init = function (onSuccess, onError) { - exec(null, null, "IndexAppContent", "deviceIsReady", []); +IndexAppContent.prototype.init = function () { + // TODO remove function in future release; leave it for now to ensure compatibility with older versions }; -IndexAppContent.prototype.setItems = function (items, onSuccess, onError) { - exec(onSuccess, onError, "IndexAppContent", "setItems", [items]); +IndexAppContent.prototype.isIndexingAvailable = function (fnCallback) { + exec(fnCallback, undefined, "IndexAppContent", "isIndexingAvailable", []); }; -IndexAppContent.prototype.clearItemsForDomains = function (domains, onSuccess, onError) { - exec(onSuccess, onError, "IndexAppContent", "clearItemsForDomains", [domains]); +IndexAppContent.prototype.setItems = function (aItems, onSuccess, onError) { + if (!onError) { + onError = function() {}; + } + if (!aItems || !Array.isArray(aItems)|| aItems.length===0) { + onError("No items"); + return; + } + exec(onSuccess, onError, "IndexAppContent", "setItems", [aItems]); }; -IndexAppContent.prototype.clearItemsForIdentifiers = function (identifiers, onSuccess, onError) { - exec(onSuccess, onError, "IndexAppContent", "clearItemsForIdentifiers", [identifiers]); +IndexAppContent.prototype.clearItemsForDomains = function (aDomains, onSuccess, onError) { + if (!onError) { + onError = function() {}; + } + if (!aDomains || !Array.isArray(aDomains) || aDomains.length===0) { + onError("No domains"); + return; + } + exec(onSuccess, onError, "IndexAppContent", "clearItemsForDomains", [aDomains]); }; -IndexAppContent.prototype.setIndexingInterval = function (interval, onSuccess, onError) { - exec(onSuccess, onError, "IndexAppContent", "setIndexingInterval", [interval]); +IndexAppContent.prototype.clearItemsForIdentifiers = function (aIdentifiers, onSuccess, onError) { + if (!onError) { + onError = function() {}; + } + if (!aIdentifiers || !Array.isArray(aIdentifiers) || aIdentifiers.length===0) { + onError("No identifiers"); + return; + } + exec(onSuccess, onError, "IndexAppContent", "clearItemsForIdentifiers", [aIdentifiers]); +}; + +IndexAppContent.prototype.setIndexingInterval = function (iMinutes, onSuccess, onError) { + if (!onError) { + onError = function() {}; + } + if (!Number.isInteger(iMinutes)) { + onError("Not a number"); + return; + } + if (iMinutes < 0) { + onError("Interval must => 0"); + return; + } + exec(onSuccess, onError, "IndexAppContent", "setIndexingInterval", [iMinutes]); }; if (!window.plugins) {