From 5955c00ce4e92e53a51b5a7f650143101754f8cf Mon Sep 17 00:00:00 2001 From: Mark Ha Date: Wed, 28 May 2014 14:26:39 -0400 Subject: [PATCH 01/13] MS-615 Inject JS to listen to Pitbull data in SDK Buffer data from JS Intercept mobile web on-click callback Save compressed screenshots of creative Moved mediatedAd validation to ANMediationAdViewController MS-690 Parse for auction_info for mediated ads New ANPBBuffer class --- ANSDK.xcodeproj/project.pbxproj | 8 +- .../AppNexusSDKApp.xcodeproj/project.pbxproj | 6 + sdk/Resources/sdkjs.js | 43 +++- sdk/internal/ANAdFetcher.h | 4 +- sdk/internal/ANAdFetcher.m | 98 +-------- sdk/internal/ANAdResponse.h | 1 + sdk/internal/ANAdResponse.m | 4 + sdk/internal/ANAdWebViewController.m | 10 + sdk/internal/ANBannerAdView.m | 4 + sdk/internal/ANGlobal.h | 3 + sdk/internal/ANInterstitialAd.m | 23 +- sdk/internal/ANMediatedAd.h | 5 +- sdk/internal/ANMediatedAd.m | 7 - sdk/internal/ANMediationAdViewController.h | 12 +- sdk/internal/ANMediationAdViewController.m | 178 +++++++++++++-- sdk/internal/ANPBBuffer.h | 29 +++ sdk/internal/ANPBBuffer.m | 205 ++++++++++++++++++ tests/TestClasses/MediationCallbacksTests.m | 4 +- tests/TestClasses/MediationTests.m | 5 +- tests/Tests.xcodeproj/project.pbxproj | 16 +- 20 files changed, 524 insertions(+), 141 deletions(-) create mode 100644 sdk/internal/ANPBBuffer.h create mode 100644 sdk/internal/ANPBBuffer.m diff --git a/ANSDK.xcodeproj/project.pbxproj b/ANSDK.xcodeproj/project.pbxproj index 78af7c6c9..dedf0568f 100644 --- a/ANSDK.xcodeproj/project.pbxproj +++ b/ANSDK.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ EC7AB086188880C300C27B1E /* ANLogManager.h in Copy Files */ = {isa = PBXBuildFile; fileRef = EC7AB08118887E4600C27B1E /* ANLogManager.h */; }; EC9B33CC187DA9F300013F79 /* ANWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = EC9B33CB187DA9F300013F79 /* ANWebView.m */; }; EC9B344A187DEB6900013F79 /* ANAdRequestUrl.m in Sources */ = {isa = PBXBuildFile; fileRef = EC9B3448187DEB6900013F79 /* ANAdRequestUrl.m */; }; + ECD6B3CC19366176006681FF /* ANPBBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = ECD6B3CB19366176006681FF /* ANPBBuffer.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -91,9 +92,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 8A629C1D1912A68000A295CE /* ANBasicConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ANBasicConfig.h; sourceTree = ""; }; EC2E96D71911A147001A42EF /* ANANJAMImplementation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ANANJAMImplementation.h; sourceTree = ""; }; EC2E96D81911A147001A42EF /* ANANJAMImplementation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANANJAMImplementation.m; sourceTree = ""; }; - 8A629C1D1912A68000A295CE /* ANBasicConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ANBasicConfig.h; sourceTree = ""; }; EC3905F8186693AA0017959C /* ANTargetingParameters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ANTargetingParameters.h; sourceTree = ""; }; EC3905F9186693B50017959C /* ANTargetingParameters.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANTargetingParameters.m; sourceTree = ""; }; EC3E5CDC1843C6D50070315E /* libANSDK.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libANSDK.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -164,6 +165,8 @@ EC9B33CB187DA9F300013F79 /* ANWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANWebView.m; sourceTree = ""; }; EC9B3448187DEB6900013F79 /* ANAdRequestUrl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANAdRequestUrl.m; sourceTree = ""; }; EC9B3449187DEB6900013F79 /* ANAdRequestUrl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ANAdRequestUrl.h; sourceTree = ""; }; + ECD6B3CA19366176006681FF /* ANPBBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ANPBBuffer.h; sourceTree = ""; }; + ECD6B3CB19366176006681FF /* ANPBBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANPBBuffer.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -282,6 +285,8 @@ EC6AAEC61884B62300CD2FDC /* ANMRAIDProperties.h */, EC3E5D331843C7560070315E /* ANMRAIDViewController.h */, EC3E5D341843C7560070315E /* ANMRAIDViewController.m */, + ECD6B3CA19366176006681FF /* ANPBBuffer.h */, + ECD6B3CB19366176006681FF /* ANPBBuffer.m */, EC3E5D351843C7560070315E /* ANReachability.h */, EC3E5D361843C7560070315E /* ANReachability.m */, EC3905F9186693B50017959C /* ANTargetingParameters.m */, @@ -432,6 +437,7 @@ EC9B33CC187DA9F300013F79 /* ANWebView.m in Sources */, EC3E5D521843C7560070315E /* ANInterstitialAdViewController.m in Sources */, EC3E5D561843C7560070315E /* ANMediationAdViewController.m in Sources */, + ECD6B3CC19366176006681FF /* ANPBBuffer.m in Sources */, EC3E5D471843C7560070315E /* UIView+ANCategory.m in Sources */, EC3E5D4A1843C7560070315E /* ANAdResponse.m in Sources */, EC9B344A187DEB6900013F79 /* ANAdRequestUrl.m in Sources */, diff --git a/examples/AppNexusSDKApp/AppNexusSDKApp.xcodeproj/project.pbxproj b/examples/AppNexusSDKApp/AppNexusSDKApp.xcodeproj/project.pbxproj index 680d857e3..a624183ff 100644 --- a/examples/AppNexusSDKApp/AppNexusSDKApp.xcodeproj/project.pbxproj +++ b/examples/AppNexusSDKApp/AppNexusSDKApp.xcodeproj/project.pbxproj @@ -114,6 +114,7 @@ ECB124D11821A25D00CB4BE8 /* ANMediationAdViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ECB124AA1821A25D00CB4BE8 /* ANMediationAdViewController.m */; }; ECB124D21821A25D00CB4BE8 /* ANReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = ECB124AC1821A25D00CB4BE8 /* ANReachability.m */; }; ECB5846518ABF9180059FD2D /* CreativePreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ECB5846418ABF9180059FD2D /* CreativePreviewViewController.m */; }; + ECCA1106192D463800472379 /* ANPBBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = ECCA1105192D463800472379 /* ANPBBuffer.m */; }; ECCF548418451FE40000E37C /* ANBrowserViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = ECCF547718451FE40000E37C /* ANBrowserViewController.xib */; }; ECCF548518451FE40000E37C /* ANInterstitialAdViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = ECCF547818451FE40000E37C /* ANInterstitialAdViewController.xib */; }; ECCF548618451FE40000E37C /* errors.strings in Resources */ = {isa = PBXBuildFile; fileRef = ECCF547918451FE40000E37C /* errors.strings */; }; @@ -317,6 +318,8 @@ ECB124AC1821A25D00CB4BE8 /* ANReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANReachability.m; sourceTree = ""; }; ECB5846318ABF9180059FD2D /* CreativePreviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CreativePreviewViewController.h; sourceTree = ""; }; ECB5846418ABF9180059FD2D /* CreativePreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CreativePreviewViewController.m; sourceTree = ""; }; + ECCA1104192D463800472379 /* ANPBBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ANPBBuffer.h; sourceTree = ""; }; + ECCA1105192D463800472379 /* ANPBBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANPBBuffer.m; sourceTree = ""; }; ECCF547718451FE40000E37C /* ANBrowserViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ANBrowserViewController.xib; sourceTree = ""; }; ECCF547818451FE40000E37C /* ANInterstitialAdViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ANInterstitialAdViewController.xib; sourceTree = ""; }; ECCF547918451FE40000E37C /* errors.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = errors.strings; sourceTree = ""; }; @@ -643,6 +646,8 @@ EC6AAEC5188479C700CD2FDC /* ANMRAIDProperties.h */, ECF9AA931858E5540079BCD0 /* ANMRAIDViewController.h */, EC70ED8C18316B2000BDF92F /* ANMRAIDViewController.m */, + ECCA1104192D463800472379 /* ANPBBuffer.h */, + ECCA1105192D463800472379 /* ANPBBuffer.m */, ECB124AB1821A25D00CB4BE8 /* ANReachability.h */, ECB124AC1821A25D00CB4BE8 /* ANReachability.m */, ECAFB02C1864EE25007F1973 /* ANTargetingParameters.m */, @@ -853,6 +858,7 @@ EC3FD72C1822213C00B25418 /* AdPreviewTVC.m in Sources */, ECB124CB1821A25D00CB4BE8 /* ANInterstitialAd.m in Sources */, ECB124BB1821A25D00CB4BE8 /* NSString+ANCategory.m in Sources */, + ECCA1106192D463800472379 /* ANPBBuffer.m in Sources */, ECB5846518ABF9180059FD2D /* CreativePreviewViewController.m in Sources */, EC3FD73C1822213C00B25418 /* CoreDataExample.xcdatamodeld in Sources */, 8ADA000118A992E00016B1BC /* AddCustomKeywordTVC.m in Sources */, diff --git a/sdk/Resources/sdkjs.js b/sdk/Resources/sdkjs.js index 8a1e26f23..1356e65fb 100644 --- a/sdk/Resources/sdkjs.js +++ b/sdk/Resources/sdkjs.js @@ -54,6 +54,43 @@ sdkjs.listener = function (event) { // accept all event.origin values, filter based on protocol + // filter for pitbull based on the presence of 'auction_id' in the raw data + if (event.data) { + if (event.data.indexOf("auction_id") !== -1) { + // tell the pitbull JS to stop refreshing for 'auction_id' + var pitbullData = JSON.parse(event.data); + var auction_id = pitbullData.auction_id; + if (event.source) { + event.source.postMessage("pitbull_stop_refresh:" + + auction_id + ":urlnotset", "*"); + } + + sdkjs.makeNativeCallWithPrefix("appnexuspb://app?", + "auction_info=" + encodeURIComponent(event.data)); + + // capture screenshot when webview becomes visible + if (document.hidden) { + var handleVisibilityChange = function () { + if (!document.hidden) { + sdkjs.makeNativeCallWithPrefix( + "appnexuspb://capture?", + "auction_id=" + auction_id); + document.removeEventListener( + handleVisibilityChange); + } + }; + document.addEventListener("visibilitychange", + handleVisibilityChange, false); + } else { + sdkjs.makeNativeCallWithPrefix("appnexuspb://capture?", + "auction_id=" + auction_id); + } + + // send the auction info to native + return; + } + } + // use anchor element for convenient parsing var a = document.createElement("a"); a.href = event.data; @@ -133,7 +170,11 @@ } sdkjs.makeNativeCall = function (uri) { - nativeCallQueue.push(NATIVE_CALL_PREFIX + uri); + sdkjs.makeNativeCallWithPrefix(NATIVE_CALL_PREFIX, uri); + } + + sdkjs.makeNativeCallWithPrefix = function (prefix, uri) { + nativeCallQueue.push(prefix + uri); if (nativeCallQueue.length == 1) { setTimeout(sdkjs.dequeue, 0); } diff --git a/sdk/internal/ANAdFetcher.h b/sdk/internal/ANAdFetcher.h index 647470bd6..113d442cb 100644 --- a/sdk/internal/ANAdFetcher.h +++ b/sdk/internal/ANAdFetcher.h @@ -45,7 +45,8 @@ extern NSString *const kANAdFetcherMediatedClassKey; - (void)setupAutoRefreshTimerIfNecessary; - (void)fireResultCB:(NSString *)resultCBString reason:(ANADRESPONSECODE)reason - adObject:(id)adObject; + adObject:(id)adObject + auctionID:(NSString *)auctionID; - (void)processAdResponse:(ANAdResponse *)response; - (void)processFinalResponse:(ANAdResponse *)response; @end @@ -57,6 +58,7 @@ extern NSString *const kANAdFetcherMediatedClassKey; - (CGSize)requestedSizeForAdFetcher:(ANAdFetcher *)fetcher; - (NSTimeInterval)autoRefreshIntervalForAdFetcher:(ANAdFetcher *)fetcher; - (void)adFetcher:(ANAdFetcher *)fetcher adShouldOpenInBrowserWithURL:(NSURL *)URL; +- (UIView *)containerView; // Delegate method for ANAdView subclasses to provide parameters that are specific to them. Should return an array of NSString - (NSArray *)extraParameters; diff --git a/sdk/internal/ANAdFetcher.m b/sdk/internal/ANAdFetcher.m index a8a9cb6ba..818d4dfcb 100644 --- a/sdk/internal/ANAdFetcher.m +++ b/sdk/internal/ANAdFetcher.m @@ -305,93 +305,12 @@ - (NSString *)prependSDKJS:(NSString *)content { - (void)handleMediatedAds:(NSMutableArray *)mediatedAds { // pop the front ad - ANMediatedAd *currentAd = mediatedAds.firstObject; - [mediatedAds removeObject:currentAd]; - - // variables to pass into the failure handler if necessary - NSString *className = nil; - NSString *resultCB = nil; - NSString *errorInfo = nil; - ANADRESPONSECODE errorCode = (ANADRESPONSECODE)ANDefaultCode; - - if (!currentAd) { - errorInfo = @"null mediated ad object"; - errorCode = (ANADRESPONSECODE)ANAdResponseUnableToFill; - } else { - className = currentAd.className; - resultCB = currentAd.resultCB; - - ANPostNotifications(kANAdFetcherWillInstantiateMediatedClassNotification, self, - @{kANAdFetcherMediatedClassKey: className}); - - ANLogDebug([NSString stringWithFormat:ANErrorString(@"instantiating_class"), className]); - - Class adClass = NSClassFromString(className); - - // Check to see if an instance of this class exists - if (!adClass) { - errorInfo = @"ClassNotFoundError"; - errorCode = (ANADRESPONSECODE)ANAdResponseMediatedSDKUnavailable; - } else { - id adInstance = [[adClass alloc] init]; - - if (!adInstance || ![adInstance respondsToSelector:@selector(setDelegate:)]) - { - errorInfo = @"InstantiationError"; - errorCode = (ANADRESPONSECODE)ANAdResponseMediatedSDKUnavailable; - } else { - [self initMediationController:adInstance resultCB:resultCB]; - - // Grab the size of the ad - interstitials will ignore this value - CGSize sizeOfCreative = CGSizeMake([currentAd.width floatValue], [currentAd.height floatValue]); - BOOL requestedSuccessfully = [self.mediationController - requestAd:sizeOfCreative - serverParameter:currentAd.param - adUnitId:currentAd.adId - adView:self.delegate]; - - if (!requestedSuccessfully) { - // don't add class to invalid networks list for this failure - className = nil; - errorInfo = @"ClassCastError"; - errorCode = (ANADRESPONSECODE)ANAdResponseMediatedSDKUnavailable; - } - } - } - } - - if (errorCode != (ANADRESPONSECODE)ANDefaultCode) { - [self handleInstantiationFailure:className resultCB:resultCB - errorCode:errorCode errorInfo:errorInfo]; - } - - // otherwise, no error yet - // wait for a mediation adapter to hit one of our callbacks. -} - -- (void)handleInstantiationFailure:(NSString *)className - resultCB:(NSString *)resultCB - errorCode:(ANADRESPONSECODE)errorCode - errorInfo:(NSString *)errorInfo { - if ([errorInfo length] > 0) { - ANLogError(ANErrorString(@"mediation_instantiation_failure"), errorInfo); - } - if ([className length] > 0) { - ANLogWarn(ANErrorString(@"mediation_adding_invalid"), className); - ANAddInvalidNetwork(className); - } - - [self clearMediationController]; - [self fireResultCB:resultCB reason:errorCode adObject:nil]; -} + ANMediatedAd *adToParse = mediatedAds.firstObject; + [mediatedAds removeObject:adToParse]; -- (void)initMediationController:(id)adInstance - resultCB:(NSString *)resultCB { - // create new mediation controller - self.mediationController = [ANMediationAdViewController initWithFetcher:self adViewDelegate:self.delegate]; - adInstance.delegate = self.mediationController; - [self.mediationController setAdapter:adInstance]; - [self.mediationController setResultCBString:resultCB]; + self.mediationController = [ANMediationAdViewController initMediatedAd:adToParse + withFetcher:self + adViewDelegate:self.delegate]; } - (void)clearMediationController { @@ -513,7 +432,8 @@ - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)err - (void)fireResultCB:(NSString *)resultCBString reason:(ANADRESPONSECODE)reason - adObject:(id)adObject { + adObject:(id)adObject + auctionID:(NSString *)auctionID { self.loading = NO; if (reason == ANAdResponseSuccessful) { @@ -526,8 +446,12 @@ - (void)fireResultCB:(NSString *)resultCBString } ANAdResponse *response = [ANAdResponse adResponseSuccessfulWithAdObject:adObject]; + response.auctionID = auctionID; [self processFinalResponse:response]; } else { + // clear all failed mediation controllers + [self clearMediationController]; + if (self.delegate) { // if there is still a delegate to send a successful ad response to // fire the resultCB if there is one if ([resultCBString length] > 0) { diff --git a/sdk/internal/ANAdResponse.h b/sdk/internal/ANAdResponse.h index 9f9bed644..2bcbbe324 100644 --- a/sdk/internal/ANAdResponse.h +++ b/sdk/internal/ANAdResponse.h @@ -27,6 +27,7 @@ @property (nonatomic, readwrite, strong) NSString *type; @property (nonatomic, readwrite, strong) NSMutableArray *mediatedAds; +@property (nonatomic, readwrite, strong) NSString *auctionID; @property (nonatomic, readwrite, getter = containsAds) BOOL containsAds; diff --git a/sdk/internal/ANAdResponse.m b/sdk/internal/ANAdResponse.m index 9e3eeed65..65ae872f6 100644 --- a/sdk/internal/ANAdResponse.m +++ b/sdk/internal/ANAdResponse.m @@ -34,6 +34,7 @@ NSString *const kResponseKeyId = @"id"; NSString *const kResponseKeyParam = @"param"; NSString *const kResponseKeyResultCB = @"result_cb"; +NSString *const kResponseKeyAuctionInfo = @"auction_info"; NSString *const kResponseValueError = @"error"; NSString *const kResponseValueIOS = @"ios"; @@ -185,6 +186,8 @@ - (BOOL)handleMediatedAds:(NSDictionary *)jsonResponse NSString *resultCB = [mediatedElement objectForKey:kResponseKeyResultCB]; + NSString *auctionInfo = [mediatedElement objectForKey:kResponseKeyAuctionInfo]; + ANMediatedAd *mediatedAd = [ANMediatedAd new]; mediatedAd.className = className; mediatedAd.param = param; @@ -192,6 +195,7 @@ - (BOOL)handleMediatedAds:(NSDictionary *)jsonResponse mediatedAd.height = height; mediatedAd.adId = adId; mediatedAd.resultCB = resultCB; + mediatedAd.auctionInfo = auctionInfo; if ([mediatedAd.className length] > 0) [_mediatedAds addObject:mediatedAd]; diff --git a/sdk/internal/ANAdWebViewController.m b/sdk/internal/ANAdWebViewController.m index b8da74fed..dab61c645 100644 --- a/sdk/internal/ANAdWebViewController.m +++ b/sdk/internal/ANAdWebViewController.m @@ -20,6 +20,7 @@ #import "ANBrowserViewController.h" #import "ANGlobal.h" #import "ANLogging.h" +#import "ANPBBuffer.h" #import "ANWebView.h" #import "NSString+ANCategory.h" #import "UIWebView+ANCategory.h" @@ -218,6 +219,15 @@ - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *) ANLogDebug(@"Loading URL: %@", [[URL absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]); + if ([scheme isEqualToString:@"appnexuspb"]) { + UIView *view = self.webView; + if ([self.adFetcher.delegate respondsToSelector:@selector(containerView)]) { + view = [self.adFetcher.delegate containerView]; + } + [ANPBBuffer handleUrl:URL forView:view]; + return NO; + } + if (self.completedFirstLoad) { if (hasHttpPrefix(scheme)) { if (self.isMRAID) { diff --git a/sdk/internal/ANBannerAdView.m b/sdk/internal/ANBannerAdView.m index 746d280c6..cc889e09d 100644 --- a/sdk/internal/ANBannerAdView.m +++ b/sdk/internal/ANBannerAdView.m @@ -301,6 +301,10 @@ - (CGSize)requestedSizeForAdFetcher:(ANAdFetcher *)fetcher { return self.adSize; } +- (UIView *)containerView { + return self; +} + #pragma mark ANMRAIDAdViewDelegate - (NSString *)adType { diff --git a/sdk/internal/ANGlobal.h b/sdk/internal/ANGlobal.h index 3cf940de4..883953e3f 100644 --- a/sdk/internal/ANGlobal.h +++ b/sdk/internal/ANGlobal.h @@ -57,6 +57,9 @@ #define kANInterstitialDefaultCloseButtonDelay 10.0 #define kANInterstitialMaximumCloseButtonDelay 10.0 +// Buffer Limit +#define kANPBBufferLimit 10 + NSString *ANUserAgent(void); NSString *ANDeviceModel(void); BOOL ANAdvertisingTrackingEnabled(void); diff --git a/sdk/internal/ANInterstitialAd.m b/sdk/internal/ANInterstitialAd.m index 036310e49..e1713c8e8 100644 --- a/sdk/internal/ANInterstitialAd.m +++ b/sdk/internal/ANInterstitialAd.m @@ -22,6 +22,7 @@ #import "ANInterstitialAdViewController.h" #import "ANLogging.h" #import "ANMRAIDViewController.h" +#import "ANPBBuffer.h" #define AN_INTERSTITIAL_AD_TIMEOUT 60.0 @@ -35,6 +36,7 @@ NSString *const kANInterstitialAdViewKey = @"kANInterstitialAdViewKey"; NSString *const kANInterstitialAdViewDateLoadedKey = @"kANInterstitialAdViewDateLoadedKey"; +NSString *const kANInterstitialAdViewAuctionInfoKey = @"kANInterstitialAdViewAuctionInfoKey"; @interface ANADVIEW (ANINTERSTITIALAD) - (void)initialize; @@ -112,6 +114,7 @@ - (void)loadAd { - (void)displayAdFromViewController:(UIViewController *)controller { self.controller.contentView = nil; id adToShow = nil; + NSString *auctionID = nil; NSString *errorString = nil; while ([self.precachedAdObjects count] > 0 @@ -125,6 +128,7 @@ - (void)displayAdFromViewController:(UIViewController *)controller { if (([dateLoaded timeIntervalSinceNow] * -1) < AN_INTERSTITIAL_AD_TIMEOUT) { // If ad is still valid, save a reference to it. We'll use it later adToShow = [adDict objectForKey:kANInterstitialAdViewKey]; + auctionID = [adDict objectForKey:kANInterstitialAdViewAuctionInfoKey]; } // This ad is now stale, so remove it from our cached ads. @@ -150,6 +154,9 @@ - (void)displayAdFromViewController:(UIViewController *)controller { } else if ([adToShow conformsToProtocol:@protocol(ANCUSTOMADAPTERINTERSTITIAL)]) { [adToShow presentFromViewController:controller]; + + // capture mediated interstitials after show event + [ANPBBuffer captureDelayedImage:controller.presentedViewController.view forAuctionID:auctionID]; } else { errorString = @"Got a non-presentable object %@. Cannot display interstitial."; @@ -287,10 +294,14 @@ - (NSArray *)extraParameters { - (void)adFetcher:(ANAdFetcher *)fetcher didFinishRequestWithResponse:(ANAdResponse *)response { if ([response isSuccessful]) { - NSDictionary *adViewWithDateLoaded = [NSDictionary dictionaryWithObjectsAndKeys: - response.adObject, kANInterstitialAdViewKey, - [NSDate date], kANInterstitialAdViewDateLoadedKey, - nil]; + NSMutableDictionary *adViewWithDateLoaded = [NSMutableDictionary dictionaryWithObjectsAndKeys: + response.adObject, kANInterstitialAdViewKey, + [NSDate date], kANInterstitialAdViewDateLoadedKey, + nil]; + // cannot insert nil objects + if (response.auctionID) { + [adViewWithDateLoaded setObject:response.auctionID forKey:kANInterstitialAdViewAuctionInfoKey]; + } [self.precachedAdObjects addObject:adViewWithDateLoaded]; ANLogDebug(@"Stored ad %@ in precached ad views", adViewWithDateLoaded); @@ -305,6 +316,10 @@ - (CGSize)requestedSizeForAdFetcher:(ANAdFetcher *)fetcher { return self.frame.size; } +- (UIView *)containerView { + return self.controller.view; +} + #pragma mark ANMRAIDAdViewDelegate - (NSString *)adType { diff --git a/sdk/internal/ANMediatedAd.h b/sdk/internal/ANMediatedAd.h index 633fbf0ad..5cb174cbc 100644 --- a/sdk/internal/ANMediatedAd.h +++ b/sdk/internal/ANMediatedAd.h @@ -23,9 +23,6 @@ @property (nonatomic, readwrite, strong) NSString *height; @property (nonatomic, readwrite, strong) NSString *adId; @property (nonatomic, readwrite, strong) NSString *resultCB; +@property (nonatomic, readwrite, strong) NSString *auctionInfo; @end - -@protocol ANMediatedAd - -@end \ No newline at end of file diff --git a/sdk/internal/ANMediatedAd.m b/sdk/internal/ANMediatedAd.m index 0da2d8eec..0a053509c 100644 --- a/sdk/internal/ANMediatedAd.m +++ b/sdk/internal/ANMediatedAd.m @@ -17,11 +17,4 @@ @implementation ANMediatedAd -@synthesize className; -@synthesize param; -@synthesize width; -@synthesize height; -@synthesize adId; -@synthesize resultCB; - @end diff --git a/sdk/internal/ANMediationAdViewController.h b/sdk/internal/ANMediationAdViewController.h index 84bc77ab9..4365f2c17 100644 --- a/sdk/internal/ANMediationAdViewController.h +++ b/sdk/internal/ANMediationAdViewController.h @@ -14,22 +14,18 @@ */ #import "ANAdFetcher.h" +#import "ANMediatedAd.h" #import @interface ANMediationAdViewController : NSObject - (void)startTimeout; -- (void)setAdapter:(id)adapter; - (void)clearAdapter; -- (void)setResultCBString:(NSString *)resultCBString; -- (BOOL)requestAd:(CGSize)size - serverParameter:(NSString *)parameterString - adUnitId:(NSString *)idString - adView:(id)adView; -+ (ANMediationAdViewController *)initWithFetcher:(ANAdFetcher *)fetcher - adViewDelegate:(id)adViewDelegate; ++ (ANMediationAdViewController *)initMediatedAd:(ANMediatedAd *)mediatedAd + withFetcher:(ANAdFetcher *)fetcher + adViewDelegate:(id)adViewDelegate; @end diff --git a/sdk/internal/ANMediationAdViewController.m b/sdk/internal/ANMediationAdViewController.m index 7f6fef5ae..788f41aff 100644 --- a/sdk/internal/ANMediationAdViewController.m +++ b/sdk/internal/ANMediationAdViewController.m @@ -20,6 +20,8 @@ #import "ANGlobal.h" #import ANINTERSTITIALADHEADER #import "ANLogging.h" +#import "ANMediatedAd.h" +#import "ANPBBuffer.h" @interface ANMediationAdViewController () @@ -28,17 +30,112 @@ @interface ANMediationAdViewController () adViewDelegate; -@property (nonatomic, readwrite, strong) NSString *resultCBString; +@property (nonatomic, readwrite, weak) id adViewDelegate; +@property (nonatomic, readwrite, strong) ANMediatedAd *mediatedAd; @end @implementation ANMediationAdViewController -+ (ANMediationAdViewController *)initWithFetcher:fetcher adViewDelegate:(id)adViewDelegate { ++ (ANMediationAdViewController *)initMediatedAd:(ANMediatedAd *)mediatedAd + withFetcher:(ANAdFetcher *)fetcher + adViewDelegate:(id)adViewDelegate { ANMediationAdViewController *controller = [[ANMediationAdViewController alloc] init]; controller.fetcher = fetcher; controller.adViewDelegate = adViewDelegate; - return controller; + + if ([controller requestForAd:mediatedAd]) { + return controller; + } else { + return nil; + } +} + +- (BOOL)requestForAd:(ANMediatedAd *)ad { + // variables to pass into the failure handler if necessary + NSString *className = nil; + NSString *errorInfo = nil; + ANADRESPONSECODE errorCode = (ANADRESPONSECODE)ANDefaultCode; + + do { + // check that the ad is non-nil + if (!ad) { + errorInfo = @"null mediated ad object"; + errorCode = (ANADRESPONSECODE)ANAdResponseUnableToFill; + break; + } + + self.mediatedAd = ad; + className = ad.className; + + // notify that a mediated class name was received + ANPostNotifications(kANAdFetcherWillInstantiateMediatedClassNotification, self, + @{kANAdFetcherMediatedClassKey: className}); + + ANLogDebug([NSString stringWithFormat:ANErrorString(@"instantiating_class"), className]); + + // check to see if an instance of this class exists + Class adClass = NSClassFromString(className); + if (!adClass) { + errorInfo = @"ClassNotFoundError"; + errorCode = (ANADRESPONSECODE)ANAdResponseMediatedSDKUnavailable; + break; + } + + id adInstance = [[adClass alloc] init]; + if (!adInstance + || ![adInstance respondsToSelector:@selector(setDelegate:)] + || ![adInstance conformsToProtocol:@protocol(ANCUSTOMADAPTER)]) { + errorInfo = @"InstantiationError"; + errorCode = (ANADRESPONSECODE)ANAdResponseMediatedSDKUnavailable; + break; + } + + // instance valid - request a mediated ad + id adapter = (id)adInstance; + adapter.delegate = self; + self.currentAdapter = adapter; + + // Grab the size of the ad - interstitials will ignore this value + CGSize sizeOfCreative = CGSizeMake([ad.width floatValue], [ad.height floatValue]); + BOOL requestedSuccessfully = [self requestAd:sizeOfCreative + serverParameter:ad.param + adUnitId:ad.adId + adView:self.adViewDelegate]; + + if (!requestedSuccessfully) { + // don't add class to invalid networks list for this failure + className = nil; + errorInfo = @"ClassCastError"; + errorCode = (ANADRESPONSECODE)ANAdResponseMediatedSDKUnavailable; + break; + } + + } while (false); + + + if (errorCode != (ANADRESPONSECODE)ANDefaultCode) { + [self handleInstantiationFailure:className + errorCode:errorCode errorInfo:errorInfo]; + return NO; + } + + // otherwise, no error yet + // wait for a mediation adapter to hit one of our callbacks. + return YES; +} + +- (void)handleInstantiationFailure:(NSString *)className + errorCode:(ANADRESPONSECODE)errorCode + errorInfo:(NSString *)errorInfo { + if ([errorInfo length] > 0) { + ANLogError(ANErrorString(@"mediation_instantiation_failure"), errorInfo); + } + if ([className length] > 0) { + ANLogWarn(ANErrorString(@"mediation_adding_invalid"), className); + ANAddInvalidNetwork(className); + } + + [self didFailToReceiveAd:errorCode]; } - (void)setAdapter:adapter { @@ -53,7 +150,7 @@ - (void)clearAdapter { self.hasFailed = YES; self.fetcher = nil; self.adViewDelegate = nil; - self.resultCBString = nil; + self.mediatedAd = nil; [self cancelTimeout]; ANLogInfo(ANErrorString(@"mediation_finish")); } @@ -74,9 +171,9 @@ - (BOOL)requestAd:(CGSize)size // make sure the container and protocol match if ([[self.currentAdapter class] conformsToProtocol:@protocol(ANCUSTOMADAPTERBANNER)] && [self.currentAdapter respondsToSelector:@selector(requestBannerAdWithSize:rootViewController:serverParameter:adUnitId:targetingParameters:)]) { + [self startTimeout]; ANBANNERADVIEW *banner = (ANBANNERADVIEW *)adView; - id bannerAdapter = (id) self.currentAdapter; [bannerAdapter requestBannerAdWithSize:size rootViewController:banner.rootViewController @@ -91,6 +188,7 @@ - (BOOL)requestAd:(CGSize)size // make sure the container and protocol match if ([[self.currentAdapter class] conformsToProtocol:@protocol(ANCUSTOMADAPTERINTERSTITIAL)] && [self.currentAdapter respondsToSelector:@selector(requestInterstitialAdWithParameter:adUnitId:targetingParameters:)]) { + [self startTimeout]; id interstitialAdapter = (id) self.currentAdapter; [interstitialAdapter requestInterstitialAdWithParameter:parameterString @@ -101,7 +199,7 @@ - (BOOL)requestAd:(CGSize)size ANLogError([NSString stringWithFormat:ANErrorString(@"instance_exception"), @"CustomAdapterInterstitial"]); } } - + // executes iff request was unsuccessful [self clearAdapter]; return NO; @@ -127,37 +225,51 @@ - (void)didFailToLoadAd:(ANADRESPONSECODE)errorCode { - (void)adWasClicked { if (self.hasFailed) return; - [self.adViewDelegate adWasClicked]; + [self runInBlock:^(void) { + [self.adViewDelegate adWasClicked]; + }]; } - (void)willPresentAd { if (self.hasFailed) return; - [self.adViewDelegate adWillPresent]; + [self runInBlock:^(void) { + [self.adViewDelegate adWillPresent]; + }]; } - (void)didPresentAd { if (self.hasFailed) return; - [self.adViewDelegate adDidPresent]; + [self runInBlock:^(void) { + [self.adViewDelegate adDidPresent]; + }]; } - (void)willCloseAd { if (self.hasFailed) return; - [self.adViewDelegate adWillClose]; + [self runInBlock:^(void) { + [self.adViewDelegate adWillClose]; + }]; } - (void)didCloseAd { if (self.hasFailed) return; - [self.adViewDelegate adDidClose]; + [self runInBlock:^(void) { + [self.adViewDelegate adDidClose]; + }]; } - (void)willLeaveApplication { if (self.hasFailed) return; - [self.adViewDelegate adWillLeaveApplication]; + [self runInBlock:^(void) { + [self.adViewDelegate adWillLeaveApplication]; + }]; } - (void)failedToDisplayAd { if (self.hasFailed) return; - [self.adViewDelegate adFailedToDisplay]; + [self runInBlock:^(void) { + [self.adViewDelegate adFailedToDisplay]; + }]; } #pragma mark helper methods @@ -178,16 +290,44 @@ - (void)didReceiveAd:(id)adObject { self.hasSucceeded = YES; ANLogDebug(@"received an ad from the adapter"); + + // save auctionInfo for the winning ad + NSString *auctionID = [ANPBBuffer saveAuctionInfo:self.mediatedAd.auctionInfo]; - [self.fetcher fireResultCB:self.resultCBString reason:(ANADRESPONSECODE)ANAdResponseSuccessful adObject:adObject]; + [self finish:(ANADRESPONSECODE)ANAdResponseSuccessful withAdObject:adObject auctionID:auctionID]; + + // if auctionInfo was present and had an auctionID, + // screenshot the view. For banners, do it here + if (auctionID && [adObject isKindOfClass:[UIView class]]) { + [ANPBBuffer captureDelayedImage:adObject forAuctionID:auctionID]; + } } - (void)didFailToReceiveAd:(ANADRESPONSECODE)errorCode { if ([self checkIfHasResponded]) return; - ANAdFetcher *fetcher = self.fetcher; - NSString *resultCBString = self.resultCBString; - [self clearAdapter]; - [fetcher fireResultCB:resultCBString reason:errorCode adObject:nil]; + + [self finish:errorCode withAdObject:nil auctionID:nil]; +} + +- (void)finish:(ANADRESPONSECODE)errorCode withAdObject:(id)adObject + auctionID:(NSString *)auctionID { + // use queue to force return + [self runInBlock:^(void) { + ANAdFetcher *fetcher = self.fetcher; + NSString *resultCBString = self.mediatedAd.resultCB; + // fireResulCB will clear the adapter if fetcher exists + if (!fetcher) { + [self clearAdapter]; + } + [fetcher fireResultCB:resultCBString reason:errorCode adObject:adObject auctionID:auctionID]; + }]; +} + +- (void)runInBlock:(void (^)())block { + // nothing keeps 'block' alive, so we don't have a retain cycle + dispatch_async(dispatch_get_main_queue(), ^{ + block(); + }); } #pragma mark Timeout handler diff --git a/sdk/internal/ANPBBuffer.h b/sdk/internal/ANPBBuffer.h new file mode 100644 index 000000000..12dae7a42 --- /dev/null +++ b/sdk/internal/ANPBBuffer.h @@ -0,0 +1,29 @@ +/* Copyright 2013 APPNEXUS INC + + Licensed 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 "ANAdFetcher.h" + +#import + +@interface ANPBBuffer : NSObject + ++ (void)handleUrl:(NSURL *)URL forView:(UIView *)view; + +// returns auction_id field for convenience ++ (NSString *)saveAuctionInfo:(NSString *)auctionInfo; + ++ (void)captureImage:(UIView *)view forAuctionID:(NSString *)auctionID; ++ (void)captureDelayedImage:(UIView *)view forAuctionID:(NSString *)auctionID; +@end diff --git a/sdk/internal/ANPBBuffer.m b/sdk/internal/ANPBBuffer.m new file mode 100644 index 000000000..59f0f8704 --- /dev/null +++ b/sdk/internal/ANPBBuffer.m @@ -0,0 +1,205 @@ +/* Copyright 2013 APPNEXUS INC + + Licensed 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 "ANPBBuffer.h" + +#import "ANGlobal.h" +#import "NSString+ANCategory.h" + +#import + +@implementation ANPBBuffer + +static NSMutableDictionary *pbBuffer; +static NSMutableArray *pbKeys; +NSString *const kImageKey = @"kUTTypeImage"; +NSString *const kTextKey = @"kUTTypeUTF8PlainText"; +NSString *const kAuctionIDKey = @"auction_id"; +NSString *const kAuctionInfoKey = @"auction_info"; +NSString *const kPBAppUrl = @"appnexuspb://app?"; +int64_t const kPBCaptureDelay = 1; // delay in seconds + +# pragma mark static initializer, called before any methods are used + ++ (void) initialize { + // make sure pitbullBuffer is initialized + if (!pbBuffer || !pbKeys) { + [ANPBBuffer resetBuffer]; + } +} + +#pragma mark Public Interface to Modify Buffer + ++ (void)handleUrl:(NSURL *)URL forView:(UIView *)view { + + NSString *host = [URL host]; + + if ([host isEqualToString:@"web"]) { + // intercept call to populate pasteboard before launching app + [ANPBBuffer setPasteboardAndLaunch]; + + } else if ([host isEqualToString:@"app"]) { + // record auction_info into buffer + NSDictionary *queryComponents = [[URL query] queryComponents]; + NSString *auctionInfo = [queryComponents objectForKey:kAuctionInfoKey]; + [ANPBBuffer saveAuctionInfo:auctionInfo]; + + } else if ([host isEqualToString:@"capture"]) { + // take a screenshot and attach it to the info for this auction ID + NSDictionary *queryComponents = [[URL query] queryComponents]; + NSString *auctionID = [queryComponents objectForKey:kAuctionIDKey]; + [ANPBBuffer captureImage:view forAuctionID:auctionID]; + + } +} + +// returns the auctionID that was parsed ++ (NSString *)saveAuctionInfo:(NSString *)auctionInfo { + // record auction_info into buffer + if (auctionInfo) { + NSData *data = [auctionInfo dataUsingEncoding:NSUTF8StringEncoding]; + NSError *jsonParsingError = nil; + NSDictionary *jsonDict = [NSJSONSerialization + JSONObjectWithData:data + options:0 + error:&jsonParsingError]; + + if (!jsonParsingError) { + NSString *auctionID = [jsonDict objectForKey:kAuctionIDKey]; + if (auctionID && ![ANPBBuffer containsAuctionInfoForID:auctionID]) { + [ANPBBuffer trimBuffer]; + [ANPBBuffer saveAuctionInfo:auctionInfo forAuctionID:auctionID]; + return auctionID; + } + } + } + return nil; +} + +// capture image with delay ++ (void)captureDelayedImage:(UIView *)view + forAuctionID:(NSString *)auctionID { + [ANPBBuffer captureImage:view forAuctionID:auctionID afterDelay:kPBCaptureDelay]; +} + +// capture image with immediately ++ (void)captureImage:(UIView *)view + forAuctionID:(NSString *)auctionID { + [ANPBBuffer captureImage:view forAuctionID:auctionID afterDelay:0]; +} + +#pragma mark Buffer Convenience Interface (private) + +/* Buffer methods */ + +// clears and initializes a new buffer ++ (void)resetBuffer { + pbBuffer = [NSMutableDictionary new]; + pbKeys = [NSMutableArray new]; +} + +// trim the buffer if necessary ++ (void)trimBuffer { + if ([pbBuffer count] >= kANPBBufferLimit) { + id key = [pbKeys objectAtIndex:0]; + // remove first object and shift forward + [pbKeys removeObjectAtIndex:0]; + [pbBuffer removeObjectForKey:key]; + } +} + ++ (void)saveAuctionInfo:(NSString *)auctionInfo + forAuctionID:(NSString *)auctionID { + if (auctionID && auctionInfo) { + [pbBuffer setValue:@{kTextKey:auctionInfo} + forKey:auctionID]; + [pbKeys addObject:auctionID]; + } +} + +// if pbKeys contains an auctionID key, then auctionInfo must be present ++ (BOOL)containsAuctionInfoForID:(NSString *)auctionID { + return [pbKeys containsObject:auctionID]; // uses isEqual validation +} + +// check if the pbBuffer contains an image for auctionID ++ (BOOL)containsImageForID:(NSString *)auctionID { + NSDictionary *item = [pbBuffer objectForKey:auctionID]; + return item && [item valueForKey:kImageKey]; +} + +/* Pasteboard methods */ + ++ (NSArray *)getPasteboardArray { + NSMutableArray *array = [NSMutableArray new]; + for (id key in pbKeys) { + id value = [pbBuffer objectForKey:key]; + [array addObject:value]; + } + return array; +} + ++ (void)setPasteboardAndLaunch { + NSURL *appURL = [NSURL URLWithString:kPBAppUrl]; + + if ([[UIApplication sharedApplication] canOpenURL:appURL]) { + // copy buffer to pasteboard for the app + [[UIPasteboard generalPasteboard] setItems:[ANPBBuffer getPasteboardArray]]; + + // clear buffer + [ANPBBuffer resetBuffer]; + + [[UIApplication sharedApplication] openURL:appURL]; + } +} + +/* Capture Image methods */ + ++ (void)saveImage:(UIImage *)image forAuctionID:(NSString *)auctionID { + NSMutableDictionary *item = [[pbBuffer objectForKey:auctionID] mutableCopy]; + if (item) { + [item setValue:[ANPBBuffer compressImage:image] forKeyPath:kImageKey]; + [pbBuffer setObject:item forKey:auctionID]; + } +} + ++ (UIImage *)captureView:(UIView *)view { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, 0); + [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO]; + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + ++ (NSData *)compressImage:(UIImage *)image { + return UIImageJPEGRepresentation(image, 1.0f); +} + ++ (void)captureImage:(__weak UIView *)view + forAuctionID:(NSString *)auctionID + afterDelay:(int64_t)delay { + if (view && auctionID && ![ANPBBuffer containsImageForID:auctionID]) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), + dispatch_get_main_queue(), ^{ + UIView *strongView = view; + if (strongView) { + UIImage *image = [ANPBBuffer captureView:strongView]; + [ANPBBuffer saveImage:image forAuctionID:auctionID]; + } + }); + } +} + +@end diff --git a/tests/TestClasses/MediationCallbacksTests.m b/tests/TestClasses/MediationCallbacksTests.m index d104af344..308b77503 100644 --- a/tests/TestClasses/MediationCallbacksTests.m +++ b/tests/TestClasses/MediationCallbacksTests.m @@ -73,7 +73,7 @@ - (void)checkCallbacks:(BOOL)called { - (void)test17 { - [ANTimeout setTimeout:CALLBACKS_TIMEOUT - 1]; + [ANTimeout setTimeout:CALLBACKS_TIMEOUT - 2]; [self stubWithBody:[ANTestResponses mediationWaterfallBanners:kClassDoesNotExist firstResult:@"" secondClass:kANTimeout secondResult:@""]]; [self stubResultCBResponses:@""]; @@ -91,7 +91,7 @@ - (void)test18LoadedMultiple - (void)test19Timeout { - [ANTimeout setTimeout:kAppNexusMediationNetworkTimeoutInterval + 1]; + [ANTimeout setTimeout:kAppNexusMediationNetworkTimeoutInterval + 2]; [self stubWithBody:[ANTestResponses createMediatedBanner:kANTimeout]]; [self stubResultCBResponses:@""]; [self runBasicTest:NO waitTime:kAppNexusMediationNetworkTimeoutInterval + CALLBACKS_TIMEOUT]; diff --git a/tests/TestClasses/MediationTests.m b/tests/TestClasses/MediationTests.m index 738210ee1..ede15f4d4 100644 --- a/tests/TestClasses/MediationTests.m +++ b/tests/TestClasses/MediationTests.m @@ -16,6 +16,7 @@ #import "ANBaseTestCase.h" #import "ANAdFetcher.h" #import "ANAdWebViewController.h" +#import "ANMediatedAd.h" #import "ANMediationAdViewController.h" #import "ANSuccessfulBannerNeverCalled.h" @@ -56,7 +57,7 @@ - (NSMutableURLRequest *)request; @interface ANMediationAdViewController () - (id)currentAdapter; -- (NSString *)resultCBString; +- (ANMediatedAd *)mediatedAd; @end #pragma mark MediationTests @@ -349,7 +350,7 @@ - (void)adFetcher:(ANAdFetcher *)fetcher didFinishRequestWithResponse:(ANAdRespo { self.adapter = [[fetcher mediationController] currentAdapter]; [fetcher requestAdWithURL: - [NSURL URLWithString:[[fetcher mediationController] resultCBString]]]; + [NSURL URLWithString:[[[fetcher mediationController] mediatedAd] resultCB]]]; } break; case 70: diff --git a/tests/Tests.xcodeproj/project.pbxproj b/tests/Tests.xcodeproj/project.pbxproj index 5c4363b40..5432b5068 100755 --- a/tests/Tests.xcodeproj/project.pbxproj +++ b/tests/Tests.xcodeproj/project.pbxproj @@ -108,6 +108,7 @@ ECABE5A71821E4C80037E6C5 /* UIButtonBarArrowRight.png in Resources */ = {isa = PBXBuildFile; fileRef = ECABE5841821E4C80037E6C5 /* UIButtonBarArrowRight.png */; }; ECABE5A81821E4C80037E6C5 /* UIButtonBarArrowRight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = ECABE5851821E4C80037E6C5 /* UIButtonBarArrowRight@2x.png */; }; ECABE5A91821E4C80037E6C5 /* MRAID.bundle in Resources */ = {isa = PBXBuildFile; fileRef = ECABE5861821E4C80037E6C5 /* MRAID.bundle */; }; + ECABF11B192E5C58006FFFB2 /* ANPBBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = ECABF119192E5C58006FFFB2 /* ANPBBuffer.m */; }; ECB57A551822C43700FA7441 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEF6F6F1732F8BC0054A86F /* SystemConfiguration.framework */; }; ECBA78B6187F6D4200F1D73F /* ANTargetingParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = ECBA78B5187F6D4200F1D73F /* ANTargetingParameters.m */; }; ECD929A71822CAD300420840 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = ECD9298B1822CAD300420840 /* InfoPlist.strings */; }; @@ -298,6 +299,8 @@ ECABE5841821E4C80037E6C5 /* UIButtonBarArrowRight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = UIButtonBarArrowRight.png; sourceTree = ""; }; ECABE5851821E4C80037E6C5 /* UIButtonBarArrowRight@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "UIButtonBarArrowRight@2x.png"; sourceTree = ""; }; ECABE5861821E4C80037E6C5 /* MRAID.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = MRAID.bundle; sourceTree = ""; }; + ECABF119192E5C58006FFFB2 /* ANPBBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANPBBuffer.m; sourceTree = ""; }; + ECABF11A192E5C58006FFFB2 /* ANPBBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ANPBBuffer.h; sourceTree = ""; }; ECBA78B5187F6D4200F1D73F /* ANTargetingParameters.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ANTargetingParameters.m; sourceTree = ""; }; ECBA78B7187F6D4800F1D73F /* ANTargetingParameters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ANTargetingParameters.h; sourceTree = ""; }; ECD9298C1822CAD300420840 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -658,11 +661,6 @@ ECABE5581821E4C80037E6C5 /* internal */ = { isa = PBXGroup; children = ( - EC2E96BF19103A00001A42EF /* ANANJAMImplementation.h */, - EC2E96C019103A00001A42EF /* ANANJAMImplementation.m */, - EC2E96C119103A00001A42EF /* ANMRAIDProperties.h */, - EC4055F9183D43FB006A1D09 /* ANMRAIDViewController.h */, - EC4055FA183D43FB006A1D09 /* ANMRAIDViewController.m */, ECABE5591821E4C80037E6C5 /* ANAdFetcher.h */, ECABE55A1821E4C80037E6C5 /* ANAdFetcher.m */, ECABE55B1821E4C80037E6C5 /* ANAdResponse.h */, @@ -673,6 +671,8 @@ ECABE55F1821E4C80037E6C5 /* ANAdViewDelegate.h */, ECABE5601821E4C80037E6C5 /* ANAdWebViewController.h */, ECABE5611821E4C80037E6C5 /* ANAdWebViewController.m */, + EC2E96BF19103A00001A42EF /* ANANJAMImplementation.h */, + EC2E96C019103A00001A42EF /* ANANJAMImplementation.m */, ECABE5621821E4C80037E6C5 /* ANBannerAdView.m */, ECABE5631821E4C80037E6C5 /* ANBrowserViewController.h */, ECABE5641821E4C80037E6C5 /* ANBrowserViewController.m */, @@ -691,6 +691,11 @@ ECABE5741821E4C80037E6C5 /* ANMediatedAd.m */, ECABE5751821E4C80037E6C5 /* ANMediationAdViewController.h */, ECABE5761821E4C80037E6C5 /* ANMediationAdViewController.m */, + EC2E96C119103A00001A42EF /* ANMRAIDProperties.h */, + EC4055F9183D43FB006A1D09 /* ANMRAIDViewController.h */, + EC4055FA183D43FB006A1D09 /* ANMRAIDViewController.m */, + ECABF11A192E5C58006FFFB2 /* ANPBBuffer.h */, + ECABF119192E5C58006FFFB2 /* ANPBBuffer.m */, ECABE5771821E4C80037E6C5 /* ANReachability.h */, ECABE5781821E4C80037E6C5 /* ANReachability.m */, ECBA78B5187F6D4200F1D73F /* ANTargetingParameters.m */, @@ -905,6 +910,7 @@ ECBA78B6187F6D4200F1D73F /* ANTargetingParameters.m in Sources */, 1D3623260D0F684500981E51 /* AppDelegate.m in Sources */, 5C3209001730664500DAB5CF /* RootViewController.m in Sources */, + ECABF11B192E5C58006FFFB2 /* ANPBBuffer.m in Sources */, ECABE58E1821E4C80037E6C5 /* ANAdFetcher.m in Sources */, EC7AB08E188883DB00C27B1E /* ANAdRequestUrl.m in Sources */, EC7AB08B188883C300C27B1E /* ANLogManager.m in Sources */, From aea938563c8facd17fa774250c5b644f4976e23a Mon Sep 17 00:00:00 2001 From: Jose Cabal-Ugaz Date: Thu, 29 May 2014 13:10:28 -0400 Subject: [PATCH 02/13] Added non public-facing way to toggle mobile environment in ad fetcher --- .../Classes/Controllers/AdPreviewTVC.m | 6 +++++ .../Classes/Controllers/AdSettingsTVC.m | 26 +++++++++++++++++++ sdk/internal/ANAdFetcher.h | 2 ++ sdk/internal/ANAdFetcher.m | 26 +++++++++++++++++-- sdk/internal/ANGlobal.h | 19 ++++++++++++++ sdk/internal/ANInstallTrackerPixel.h | 3 +++ sdk/internal/ANInstallTrackerPixel.m | 18 ++++++++++++- 7 files changed, 97 insertions(+), 3 deletions(-) diff --git a/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Controllers/AdPreviewTVC.m b/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Controllers/AdPreviewTVC.m index 20f348448..9c1269446 100644 --- a/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Controllers/AdPreviewTVC.m +++ b/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Controllers/AdPreviewTVC.m @@ -20,6 +20,7 @@ #import "ANLogging.h" #import "ANAdProtocol.h" #import "AppNexusSDKAppGlobal.h" +#import "ANAdFetcher.h" #define SV_BACKGROUND_COLOR_RED 249.0 #define SV_BACKGROUND_COLOR_BLUE 249.0 @@ -136,6 +137,11 @@ - (void)loadAdvancedSettingsOnAdView:(ANAdView *)adView { if ([self.settings.zipcode length]) { [adView.customKeywords setValue:self.settings.zipcode forKey:@"pcode"]; } + + ANAdFetcher *fetcher = [adView performSelector:@selector(adFetcher)]; + if (fetcher) { + fetcher.endpoint = self.settings.environment; + } } - (void)loadBannerAd { diff --git a/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Controllers/AdSettingsTVC.m b/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Controllers/AdSettingsTVC.m index 414ba3cdb..043f43e87 100644 --- a/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Controllers/AdSettingsTVC.m +++ b/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Controllers/AdSettingsTVC.m @@ -24,6 +24,7 @@ #import "CustomKeywordsTVC.h" #import "BackgroundColorView.h" #import "AppNexusSDKAppGlobal.h" +#import "ANGlobal.h" #define CLASS_NAME @"AdSettingsTVC" @@ -67,6 +68,8 @@ @interface AdSettingsTVC () delegate; @property (nonatomic, readonly, getter = isLoading) BOOL loading; +@property (nonatomic, readwrite, assign) ANMobileEndpoint endpoint; - (void)stopAd; - (void)requestAd; diff --git a/sdk/internal/ANAdFetcher.m b/sdk/internal/ANAdFetcher.m index a8a9cb6ba..a83881fa1 100644 --- a/sdk/internal/ANAdFetcher.m +++ b/sdk/internal/ANAdFetcher.m @@ -44,6 +44,8 @@ @interface ANAdFetcher () @property (nonatomic, readwrite, strong) NSMutableArray *mediatedAds; @property (nonatomic, readwrite, strong) ANMediationAdViewController *mediationController; @property (nonatomic, readwrite, assign) BOOL requestShouldBePosted; +@property (nonatomic, readwrite, strong) NSString *ANMobileHostname; +@property (nonatomic, readwrite, strong) NSString *ANBaseURL; @end @implementation ANAdFetcher @@ -55,12 +57,32 @@ - (id)init self.data = [NSMutableData data]; self.request = [ANAdFetcher initBasicRequest]; self.successResultRequest = [ANAdFetcher initBasicRequest]; + self.ANMobileHostname = AN_MOBILE_HOSTNAME; + self.ANBaseURL = AN_BASE_URL; [NSHTTPCookieStorage sharedHTTPCookieStorage].cookieAcceptPolicy = NSHTTPCookieAcceptPolicyAlways; } return self; } +- (void)setEndpoint:(ANMobileEndpoint)endpoint { + _endpoint = endpoint; + switch (endpoint) { + case ANMobileEndpointClientTesting: + self.ANMobileHostname = AN_MOBILE_HOSTNAME_CTEST; + self.ANBaseURL = AN_BASE_URL_CTEST; + break; + case ANMobileEndpointSandbox: + self.ANMobileHostname = AN_MOBILE_HOSTNAME_SAND; + self.ANBaseURL = AN_BASE_URL_SAND; + break; + default: + self.ANMobileHostname = AN_MOBILE_HOSTNAME; + self.ANBaseURL = AN_BASE_URL; + break; + } +} + + (NSMutableURLRequest *)initBasicRequest { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:nil @@ -92,7 +114,7 @@ - (void)requestAdWithURL:(NSURL *)URL ANLogDebug(ANErrorString(([self getAutoRefreshFromDelegate] > 0.0) ? @"fetcher_start_auto" : @"fetcher_start_single")); - NSString *baseUrlString = [NSString stringWithFormat:@"http://%@?", AN_MOBILE_HOSTNAME]; + NSString *baseUrlString = [NSString stringWithFormat:@"http://%@?", self.ANMobileHostname]; self.URL = URL ? URL : [ANAdRequestUrl buildRequestUrlWithAdFetcherDelegate:self.delegate baseUrlString:baseUrlString]; @@ -271,7 +293,7 @@ - (void)handleStandardAd:(ANAdResponse *)response { contentToLoad = [self prependMRAIDJS:contentToLoad]; contentToLoad = [self prependSDKJS:contentToLoad]; - [webView loadHTMLString:contentToLoad baseURL:[NSURL URLWithString:AN_BASE_URL]]; + [webView loadHTMLString:contentToLoad baseURL:[NSURL URLWithString:self.ANBaseURL]]; } - (NSString *)prependMRAIDJS:(NSString *)content { diff --git a/sdk/internal/ANGlobal.h b/sdk/internal/ANGlobal.h index 3cf940de4..558d32626 100644 --- a/sdk/internal/ANGlobal.h +++ b/sdk/internal/ANGlobal.h @@ -16,9 +16,21 @@ #import #import +// Production #define AN_BASE_URL @"http://mediation.adnxs.com/" #define AN_MOBILE_HOSTNAME @"mediation.adnxs.com/mob" #define AN_MOBILE_HOSTNAME_INSTALL @"mediation.adnxs.com/install" + +// Client Testing +#define AN_BASE_URL_CTEST @"http://ib.client-testing.adnxs.net/" +#define AN_MOBILE_HOSTNAME_CTEST @"ib.client-testing.adnxs.net/mob" +#define AN_MOBILE_HOSTNAME_INSTALL_CTEST @"ib.client-testing.adnxs.net/install" + +//Sandbox +#define AN_BASE_URL_SAND @"http://ib.sand-08.adnxs.net/" +#define AN_MOBILE_HOSTNAME_SAND @"ib.sand-08.adnxs.net/mob" +#define AN_MOBILE_HOSTNAME_INSTALL_SAND @"ib.sand-08.adnxs.net/install" + #define AN_ERROR_DOMAIN @"com.appnexus.sdk" #define AN_ERROR_TABLE @"errors" @@ -57,6 +69,12 @@ #define kANInterstitialDefaultCloseButtonDelay 10.0 #define kANInterstitialMaximumCloseButtonDelay 10.0 +typedef NS_ENUM(NSUInteger, ANMobileEndpoint) { + ANMobileEndpointProduction = 0, + ANMobileEndpointClientTesting, + ANMobileEndpointSandbox +}; + NSString *ANUserAgent(void); NSString *ANDeviceModel(void); BOOL ANAdvertisingTrackingEnabled(void); @@ -72,3 +90,4 @@ NSMutableSet *ANInvalidNetworks(); void ANAddInvalidNetwork(NSString *network); void ANSetNotificationsEnabled(BOOL enabled); void ANPostNotifications(NSString *name, id object, NSDictionary *userInfo); + diff --git a/sdk/internal/ANInstallTrackerPixel.h b/sdk/internal/ANInstallTrackerPixel.h index b141f877e..e90446f05 100644 --- a/sdk/internal/ANInstallTrackerPixel.h +++ b/sdk/internal/ANInstallTrackerPixel.h @@ -14,11 +14,14 @@ */ #import +#import "ANGlobal.h" @protocol ANInstallTrackerPixelDelegate; @interface ANInstallTrackerPixel : NSObject +@property (nonatomic, readwrite, assign) ANMobileEndpoint endpoint; + - (id)initWithTrackingID:(NSString *)trackingID; - (void)fireInstallTrackerPixel; diff --git a/sdk/internal/ANInstallTrackerPixel.m b/sdk/internal/ANInstallTrackerPixel.m index 913f75aab..2015f82f5 100644 --- a/sdk/internal/ANInstallTrackerPixel.m +++ b/sdk/internal/ANInstallTrackerPixel.m @@ -15,7 +15,6 @@ #import "ANInstallTrackerPixel.h" -#import "ANGlobal.h" #import "ANLogging.h" #import "NSString+ANCategory.h" @@ -32,6 +31,7 @@ @interface ANInstallTrackerPixel () @property (nonatomic, readwrite, strong) NSURLConnection *connection; @property (nonatomic, readwrite, strong) NSMutableData *data; @property (nonatomic, readwrite, strong) NSString *trackingID; +@property (nonatomic, readwrite, strong) NSString *ANHostnameInstallURL; @property (nonatomic, readwrite, assign, getter = isLoading) BOOL loading; @end @@ -50,6 +50,7 @@ - (id)initWithTrackingID:(NSString *)trackingID { self.trackingID = trackingID; self.data = [NSMutableData data]; + self.ANHostnameInstallURL = AN_MOBILE_HOSTNAME_INSTALL; __lastAttemptInterval = 0; self.request = [[NSMutableURLRequest alloc] initWithURL:nil @@ -61,6 +62,21 @@ - (id)initWithTrackingID:(NSString *)trackingID return self; } +- (void)setEndpoint:(ANMobileEndpoint)endpoint { + _endpoint = endpoint; + switch (endpoint) { + case ANMobileEndpointClientTesting: + self.ANHostnameInstallURL = AN_MOBILE_HOSTNAME_INSTALL_CTEST; + break; + case ANMobileEndpointSandbox: + self.ANHostnameInstallURL = AN_MOBILE_HOSTNAME_INSTALL_SAND; + break; + default: + self.ANHostnameInstallURL = AN_MOBILE_HOSTNAME_INSTALL; + break; + } +} + - (NSString *)trackingIDParameter { NSString *trackingID = self.trackingID ? self.trackingID : @""; From 72c0b81be2dce70147048057d77450b6a0a92c8d Mon Sep 17 00:00:00 2001 From: Jose Cabal-Ugaz Date: Thu, 29 May 2014 13:14:30 -0400 Subject: [PATCH 03/13] Save environment in persistent settings. --- .../AppNexusSDKApp/Classes/Model/AdSettings.h | 4 ++++ .../AppNexusSDKApp/Classes/Model/AdSettings.m | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Model/AdSettings.h b/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Model/AdSettings.h index 4f8d5c526..41f45a1e8 100644 --- a/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Model/AdSettings.h +++ b/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Model/AdSettings.h @@ -30,6 +30,7 @@ #define DEFAULT_RESERVE 0.0 #define DEFAULT_CUSTOM_KEYWORDS [[NSDictionary alloc] init] #define DEFAULT_ZIPCODE @"" +#define DEFAULT_ENVIRONMENT 0 @interface AdSettings : NSObject @@ -60,6 +61,8 @@ typedef NS_ENUM(int, BrowserType) { @property (nonatomic) NSDictionary *customKeywords; +@property (nonatomic) NSUInteger environment; + /* Banner Properties */ @@ -84,4 +87,5 @@ typedef NS_ENUM(int, BrowserType) { @property (nonatomic) int memberID; @property (strong, nonatomic) NSString *dongle; + @end diff --git a/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Model/AdSettings.m b/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Model/AdSettings.m index c0e7b371d..c758475a7 100644 --- a/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Model/AdSettings.m +++ b/examples/AppNexusSDKApp/AppNexusSDKApp/Classes/Model/AdSettings.m @@ -37,6 +37,7 @@ @implementation AdSettings #define RESERVE_KEY @"Reserve" #define CUSTOM_KEYWORDS_KEY @"CustomKeywords" #define ZIPCODE_KEY @"Zipcode" +#define ENVIRONMENT_KEY @"Environment" - (id)init { NSDictionary *settingsFromUserDefaults = [[NSUserDefaults standardUserDefaults] dictionaryForKey:ALL_SETTINGS_KEY]; @@ -68,7 +69,8 @@ - (id)asPropertyList { GENDER_KEY:@(self.gender), RESERVE_KEY:@(self.reserve), CUSTOM_KEYWORDS_KEY:self.customKeywords, - ZIPCODE_KEY:self.zipcode}; + ZIPCODE_KEY:self.zipcode, + ENVIRONMENT_KEY:@(self.environment)}; } - (id)initFromPropertyList:(id)plist { @@ -89,7 +91,8 @@ - (id)initFromPropertyList:(id)plist { _reserve = settingsDict[RESERVE_KEY] ? [settingsDict[RESERVE_KEY] doubleValue] : DEFAULT_RESERVE; _customKeywords = settingsDict[CUSTOM_KEYWORDS_KEY] ? settingsDict[CUSTOM_KEYWORDS_KEY] : DEFAULT_CUSTOM_KEYWORDS; _zipcode = settingsDict[ZIPCODE_KEY] ? settingsDict[ZIPCODE_KEY] : DEFAULT_ZIPCODE; - + _environment = settingsDict[ENVIRONMENT_KEY] ? [settingsDict[ENVIRONMENT_KEY] unsignedIntegerValue] : DEFAULT_ENVIRONMENT; + /* Banner Properties */ @@ -127,6 +130,7 @@ - (id)initWithDefaultSettings { _reserve = DEFAULT_RESERVE; _customKeywords = DEFAULT_CUSTOM_KEYWORDS; _zipcode = DEFAULT_ZIPCODE; + _environment = DEFAULT_ENVIRONMENT; /* Banner Properties @@ -237,4 +241,9 @@ + (BOOL)backgroundColorIsValid:(NSString *)backgroundColor { return isValid; } +- (void)setEnvironment:(NSUInteger)environment { + _environment = environment; + [self synchronize]; +} + @end From 70f24cd5aa8a4747484639b7514154f9f1d541dc Mon Sep 17 00:00:00 2001 From: Jose Cabal-Ugaz Date: Thu, 29 May 2014 13:14:59 -0400 Subject: [PATCH 04/13] Display persistent settings in UI --- .../en.lproj/MainStoryboard.storyboard | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/examples/AppNexusSDKApp/AppNexusSDKApp/en.lproj/MainStoryboard.storyboard b/examples/AppNexusSDKApp/AppNexusSDKApp/en.lproj/MainStoryboard.storyboard index 45b8dae01..aea01c176 100644 --- a/examples/AppNexusSDKApp/AppNexusSDKApp/en.lproj/MainStoryboard.storyboard +++ b/examples/AppNexusSDKApp/AppNexusSDKApp/en.lproj/MainStoryboard.storyboard @@ -1088,12 +1088,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1134,7 +1169,7 @@ - + @@ -1192,6 +1227,7 @@ + @@ -1678,15 +1714,15 @@ Cg - + - + - +