diff --git a/HackerNews.xcodeproj/project.pbxproj b/HackerNews.xcodeproj/project.pbxproj index e7ccf67..80863cf 100644 --- a/HackerNews.xcodeproj/project.pbxproj +++ b/HackerNews.xcodeproj/project.pbxproj @@ -7,6 +7,19 @@ objects = { /* Begin PBXBuildFile section */ + 3C43E84E182B097500A7ED14 /* PocketAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C43E846182B097500A7ED14 /* PocketAPI.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3C43E84F182B097500A7ED14 /* PocketAPILogin.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C43E848182B097500A7ED14 /* PocketAPILogin.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3C43E850182B097500A7ED14 /* PocketAPIOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C43E84A182B097500A7ED14 /* PocketAPIOperation.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3C43E851182B097500A7ED14 /* SFHFKeychainUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C43E84D182B097500A7ED14 /* SFHFKeychainUtils.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3C43E853182B09BC00A7ED14 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C43E852182B09BC00A7ED14 /* Security.framework */; }; + 3C5501F9182DD3E0002C7B97 /* SVProgressHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C5501F8182DD3E0002C7B97 /* SVProgressHUD.m */; }; + 3C909304182ECB8F009ECB9A /* HNPostURL.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C909303182ECB8F009ECB9A /* HNPostURL.m */; }; + 3CFD566C182ED903002CF3AD /* DRPocketActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFD5665182ED903002CF3AD /* DRPocketActivity.m */; }; + 3CFD566D182ED903002CF3AD /* LoadingHUDViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFD5667182ED903002CF3AD /* LoadingHUDViewController.m */; }; + 3CFD566E182ED903002CF3AD /* PocketActivity.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CFD5668182ED903002CF3AD /* PocketActivity.png */; }; + 3CFD566F182ED903002CF3AD /* PocketActivity@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CFD5669182ED903002CF3AD /* PocketActivity@2x.png */; }; + 3CFD5670182ED903002CF3AD /* PocketActivity@2x~ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CFD566A182ED903002CF3AD /* PocketActivity@2x~ipad.png */; }; + 3CFD5671182ED903002CF3AD /* PocketActivity~ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CFD566B182ED903002CF3AD /* PocketActivity~ipad.png */; }; 6C2DAAD3174C977B0069BAE1 /* ARChromeActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 6C2DAACE174C977B0069BAE1 /* ARChromeActivity.m */; }; 6C2DAAD4174C977B0069BAE1 /* ARChromeActivity.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2DAACF174C977B0069BAE1 /* ARChromeActivity.png */; }; 6C2DAAD5174C977B0069BAE1 /* ARChromeActivity@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2DAAD0174C977B0069BAE1 /* ARChromeActivity@2x.png */; }; @@ -144,6 +157,29 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 3C43E844182B097500A7ED14 /* PocketAPI+NSOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PocketAPI+NSOperation.h"; sourceTree = ""; }; + 3C43E845182B097500A7ED14 /* PocketAPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PocketAPI.h; sourceTree = ""; }; + 3C43E846182B097500A7ED14 /* PocketAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PocketAPI.m; sourceTree = ""; }; + 3C43E847182B097500A7ED14 /* PocketAPILogin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PocketAPILogin.h; sourceTree = ""; }; + 3C43E848182B097500A7ED14 /* PocketAPILogin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PocketAPILogin.m; sourceTree = ""; }; + 3C43E849182B097500A7ED14 /* PocketAPIOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PocketAPIOperation.h; sourceTree = ""; }; + 3C43E84A182B097500A7ED14 /* PocketAPIOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PocketAPIOperation.m; sourceTree = ""; }; + 3C43E84B182B097500A7ED14 /* PocketAPITypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PocketAPITypes.h; sourceTree = ""; }; + 3C43E84C182B097500A7ED14 /* SFHFKeychainUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SFHFKeychainUtils.h; sourceTree = ""; }; + 3C43E84D182B097500A7ED14 /* SFHFKeychainUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFHFKeychainUtils.m; sourceTree = ""; }; + 3C43E852182B09BC00A7ED14 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 3C5501F7182DD3E0002C7B97 /* SVProgressHUD.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SVProgressHUD.h; sourceTree = ""; }; + 3C5501F8182DD3E0002C7B97 /* SVProgressHUD.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SVProgressHUD.m; sourceTree = ""; }; + 3C909302182ECB8F009ECB9A /* HNPostURL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HNPostURL.h; sourceTree = ""; }; + 3C909303182ECB8F009ECB9A /* HNPostURL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HNPostURL.m; sourceTree = ""; }; + 3CFD5664182ED903002CF3AD /* DRPocketActivity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DRPocketActivity.h; sourceTree = ""; }; + 3CFD5665182ED903002CF3AD /* DRPocketActivity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DRPocketActivity.m; sourceTree = ""; }; + 3CFD5666182ED903002CF3AD /* LoadingHUDViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LoadingHUDViewController.h; path = DRPocketActivity/LoadingHUDViewController.h; sourceTree = ""; }; + 3CFD5667182ED903002CF3AD /* LoadingHUDViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LoadingHUDViewController.m; path = DRPocketActivity/LoadingHUDViewController.m; sourceTree = ""; }; + 3CFD5668182ED903002CF3AD /* PocketActivity.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = PocketActivity.png; sourceTree = ""; }; + 3CFD5669182ED903002CF3AD /* PocketActivity@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "PocketActivity@2x.png"; sourceTree = ""; }; + 3CFD566A182ED903002CF3AD /* PocketActivity@2x~ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "PocketActivity@2x~ipad.png"; sourceTree = ""; }; + 3CFD566B182ED903002CF3AD /* PocketActivity~ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "PocketActivity~ipad.png"; sourceTree = ""; }; 6C2DAACD174C977B0069BAE1 /* ARChromeActivity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARChromeActivity.h; path = ARChromeActivity/ARChromeActivity.h; sourceTree = ""; }; 6C2DAACE174C977B0069BAE1 /* ARChromeActivity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ARChromeActivity.m; path = ARChromeActivity/ARChromeActivity.m; sourceTree = ""; }; 6C2DAACF174C977B0069BAE1 /* ARChromeActivity.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = ARChromeActivity.png; path = ARChromeActivity/ARChromeActivity.png; sourceTree = ""; }; @@ -326,6 +362,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3C43E853182B09BC00A7ED14 /* Security.framework in Frameworks */, C0CC4EAB1819ECB100D39DC5 /* StoreKit.framework in Frameworks */, C08142181797975E00D1BB17 /* CoreText.framework in Frameworks */, C01EDEB91733698D00AE97B8 /* MessageUI.framework in Frameworks */, @@ -340,9 +377,79 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C43E843182B097500A7ED14 /* Pocket-ObjC-SDK */ = { + isa = PBXGroup; + children = ( + 3C43E844182B097500A7ED14 /* PocketAPI+NSOperation.h */, + 3C43E845182B097500A7ED14 /* PocketAPI.h */, + 3C43E846182B097500A7ED14 /* PocketAPI.m */, + 3C43E847182B097500A7ED14 /* PocketAPILogin.h */, + 3C43E848182B097500A7ED14 /* PocketAPILogin.m */, + 3C43E849182B097500A7ED14 /* PocketAPIOperation.h */, + 3C43E84A182B097500A7ED14 /* PocketAPIOperation.m */, + 3C43E84B182B097500A7ED14 /* PocketAPITypes.h */, + 3C43E84C182B097500A7ED14 /* SFHFKeychainUtils.h */, + 3C43E84D182B097500A7ED14 /* SFHFKeychainUtils.m */, + ); + name = "Pocket-ObjC-SDK"; + path = ../../SDK; + sourceTree = ""; + }; + 3C5501F6182DD3AA002C7B97 /* SVProgressHUD */ = { + isa = PBXGroup; + children = ( + 3C5501F7182DD3E0002C7B97 /* SVProgressHUD.h */, + 3C5501F8182DD3E0002C7B97 /* SVProgressHUD.m */, + ); + name = SVProgressHUD; + sourceTree = ""; + }; + 3C909301182ECB27009ECB9A /* HNPostURL */ = { + isa = PBXGroup; + children = ( + 3C909302182ECB8F009ECB9A /* HNPostURL.h */, + 3C909303182ECB8F009ECB9A /* HNPostURL.m */, + ); + name = HNPostURL; + sourceTree = ""; + }; + 3CFD5663182ED903002CF3AD /* DRPocketActivity */ = { + isa = PBXGroup; + children = ( + 3C43E843182B097500A7ED14 /* Pocket-ObjC-SDK */, + 3CFD5673182ED93E002CF3AD /* images */, + 3CFD5664182ED903002CF3AD /* DRPocketActivity.h */, + 3CFD5665182ED903002CF3AD /* DRPocketActivity.m */, + ); + path = DRPocketActivity; + sourceTree = ""; + }; + 3CFD5672182ED91F002CF3AD /* LoadingHUDViewController */ = { + isa = PBXGroup; + children = ( + 3CFD5666182ED903002CF3AD /* LoadingHUDViewController.h */, + 3CFD5667182ED903002CF3AD /* LoadingHUDViewController.m */, + ); + name = LoadingHUDViewController; + sourceTree = ""; + }; + 3CFD5673182ED93E002CF3AD /* images */ = { + isa = PBXGroup; + children = ( + 3CFD5668182ED903002CF3AD /* PocketActivity.png */, + 3CFD5669182ED903002CF3AD /* PocketActivity@2x.png */, + 3CFD566A182ED903002CF3AD /* PocketActivity@2x~ipad.png */, + 3CFD566B182ED903002CF3AD /* PocketActivity~ipad.png */, + ); + name = images; + sourceTree = ""; + }; 6C2DAACC174C971E0069BAE1 /* ShareActivities */ = { isa = PBXGroup; children = ( + 3CFD5672182ED91F002CF3AD /* LoadingHUDViewController */, + 3CFD5663182ED903002CF3AD /* DRPocketActivity */, + 3C909301182ECB27009ECB9A /* HNPostURL */, 6C2DAAD9174C9C050069BAE1 /* TUSafariActivity */, 6C2DAAD8174C9BF80069BAE1 /* ARChromeActivity */, ); @@ -424,6 +531,7 @@ C01EDDE51731553B00AE97B8 /* Frameworks */ = { isa = PBXGroup; children = ( + 3C43E852182B09BC00A7ED14 /* Security.framework */, C0CC4EAA1819ECB000D39DC5 /* StoreKit.framework */, C01EDDE61731553B00AE97B8 /* UIKit.framework */, C01EDDE81731553B00AE97B8 /* Foundation.framework */, @@ -435,6 +543,7 @@ C01EDDEC1731553B00AE97B8 /* HackerNews */ = { isa = PBXGroup; children = ( + 3C5501F6182DD3AA002C7B97 /* SVProgressHUD */, C0CC4E9018176C7300D39DC5 /* Singleton */, C0CC4E7C1816D71E00D39DC5 /* Controllers */, C0FB762C1803B14800BDA142 /* libHN */, @@ -814,6 +923,7 @@ C02032BC18270E680038950B /* topicon.png in Resources */, C0CC4E891816E18F00D39DC5 /* LinksViewController.xib in Resources */, C01EDE6C1731A84800AE97B8 /* nav_markasread_on-01.png in Resources */, + 3CFD566F182ED903002CF3AD /* PocketActivity@2x.png in Resources */, C01EDE6D1731A84800AE97B8 /* nav_readability_off-01.png in Resources */, C01EDE6E1731A84800AE97B8 /* nav_readability_on-01.png in Resources */, C0CC4E9B1818752600D39DC5 /* upvote-01.png in Resources */, @@ -824,6 +934,7 @@ C0CC4EA01818AC9300D39DC5 /* CommentAuxiliaryView.xib in Resources */, C01EDE721731A84800AE97B8 /* twitter-01.png in Resources */, C01EDE751731A84800AE97B8 /* minus-01.png in Resources */, + 3CFD5671182ED903002CF3AD /* PocketActivity~ipad.png in Resources */, C01EDE761731A84800AE97B8 /* commentbubble-01.png in Resources */, C01EDEB51733683200AE97B8 /* NavigationDeckViewController.xib in Resources */, C078EE1B181D6157008DC47C /* nav_background_ios6@2x.png in Resources */, @@ -842,6 +953,7 @@ C0D074E017EE07EB000CAAC3 /* bigCheck-01.png in Resources */, 6C2DAAD5174C977B0069BAE1 /* ARChromeActivity@2x.png in Resources */, 6C2DAAD6174C977B0069BAE1 /* ARChromeActivity@2x~ipad.png in Resources */, + 3CFD566E182ED903002CF3AD /* PocketActivity.png in Resources */, 6C2DAAD7174C977B0069BAE1 /* ARChromeActivity~ipad.png in Resources */, 6C2DAAF3174C9C2E0069BAE1 /* cs.lproj in Resources */, 6C2DAAF4174C9C2E0069BAE1 /* de.lproj in Resources */, @@ -862,6 +974,7 @@ 6C2DAB00174C9C2E0069BAE1 /* pt.lproj in Resources */, 6C2DAB01174C9C2E0069BAE1 /* ru.lproj in Resources */, 6C2DAB02174C9C2E0069BAE1 /* Safari.png in Resources */, + 3CFD5670182ED903002CF3AD /* PocketActivity@2x~ipad.png in Resources */, 6C2DAB03174C9C2E0069BAE1 /* Safari@2x.png in Resources */, 6C2DAB04174C9C2E0069BAE1 /* Safari@2x~ipad.png in Resources */, 6C2DAB05174C9C2E0069BAE1 /* Safari~ipad.png in Resources */, @@ -884,6 +997,7 @@ buildActionMask = 2147483647; files = ( C01EDDF31731553B00AE97B8 /* main.m in Sources */, + 3C43E850182B097500A7ED14 /* PocketAPIOperation.m in Sources */, C01EDDF71731553B00AE97B8 /* AppDelegate.m in Sources */, C0CC4E831816D76100D39DC5 /* CommentsViewController.m in Sources */, C01EDE001731553B00AE97B8 /* PostsViewController.m in Sources */, @@ -892,7 +1006,9 @@ C01EDE191731A3CA00AE97B8 /* IIViewDeckController.m in Sources */, C01EDE1A1731A3CA00AE97B8 /* IIWrapController.m in Sources */, C0CC4E9618176CBC00D39DC5 /* SubmitHNViewController.m in Sources */, + 3CFD566C182ED903002CF3AD /* DRPocketActivity.m in Sources */, C01EDE1E1731A3E500AE97B8 /* triangleView.m in Sources */, + 3C43E84E182B097500A7ED14 /* PocketAPI.m in Sources */, C0CC4EAE181A9DDB00D39DC5 /* NavPurchaseProCell.m in Sources */, C0FB76391803B18200BDA142 /* HNPost.m in Sources */, C01EDE2F1731A44200AE97B8 /* FilterCell.m in Sources */, @@ -901,6 +1017,7 @@ C01EDE351731A44200AE97B8 /* CreditsCell.m in Sources */, C01EDE3B1731A4BB00AE97B8 /* frontPageCell.m in Sources */, C01EDE401731A4C900AE97B8 /* CommentsCell.m in Sources */, + 3CFD566D182ED903002CF3AD /* LoadingHUDViewController.m in Sources */, C0CC4E7B1816B30E00D39DC5 /* HNWebService.m in Sources */, C01EDE441731A61700AE97B8 /* HNSingleton.m in Sources */, C0CC4EA31819EC0000D39DC5 /* SatelliteStore.m in Sources */, @@ -911,6 +1028,7 @@ C0CC4E9E1818AC7F00D39DC5 /* CommentAuxiliaryView.m in Sources */, C01EDEB41733683200AE97B8 /* NavigationDeckViewController.m in Sources */, C0ABB6221735C777005EA39C /* FailedLoadingView.m in Sources */, + 3C5501F9182DD3E0002C7B97 /* SVProgressHUD.m in Sources */, C0F93FE51739CE1800E47B5E /* ProfileNotLoggedInCell.m in Sources */, C0F93FEB1739D4D100E47B5E /* ProfileLoggedInCell.m in Sources */, C0FB76421803B19B00BDA142 /* HNManager.m in Sources */, @@ -918,7 +1036,10 @@ C0F93FF51739DE0000E47B5E /* KGStatusBar.m in Sources */, C0FB763F1803B18F00BDA142 /* HNUtilities.m in Sources */, 6C2DAAD3174C977B0069BAE1 /* ARChromeActivity.m in Sources */, + 3C43E84F182B097500A7ED14 /* PocketAPILogin.m in Sources */, + 3C43E851182B097500A7ED14 /* SFHFKeychainUtils.m in Sources */, C0CC4EA81819EC4300D39DC5 /* GetProViewController.m in Sources */, + 3C909304182ECB8F009ECB9A /* HNPostURL.m in Sources */, 6C2DAB08174C9C2E0069BAE1 /* TUSafariActivity.m in Sources */, C0EE6F8317AEDF8E00C97F0B /* AddCommentView.m in Sources */, ); diff --git a/HackerNews/ARChromeActivity/ARChromeActivity.m b/HackerNews/ARChromeActivity/ARChromeActivity.m index 15f5b01..81fa2c5 100755 --- a/HackerNews/ARChromeActivity/ARChromeActivity.m +++ b/HackerNews/ARChromeActivity/ARChromeActivity.m @@ -12,6 +12,7 @@ #import "ARChromeActivity.h" +#import "HNPostURL.h" @implementation ARChromeActivity { NSURL *_activityURL; @@ -23,7 +24,7 @@ @implementation ARChromeActivity { - (void)commonInit { _callbackSource = [[NSBundle mainBundle]objectForInfoDictionaryKey:@"CFBundleName"]; - _activityTitle = @"Chrome"; + _activityTitle = @"Open in Chrome"; } - (id)init { @@ -52,11 +53,12 @@ - (NSString *)activityType { } - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { - return [[activityItems lastObject] isKindOfClass:[NSURL class]] && [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"googlechrome-x-callback://"]]; + return [[activityItems lastObject] isKindOfClass:[HNPostURL class]] && [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"googlechrome-x-callback://"]]; } - (void)prepareWithActivityItems:(NSArray *)activityItems { - _activityURL = [activityItems lastObject]; + // Note: Shouldn't we use the original URL here? Not the readibility one. + _activityURL = ((HNPostURL*)[activityItems lastObject]).mobileFriendlyAbsoluteURL; } - (void)performActivity { diff --git a/HackerNews/AppDelegate.m b/HackerNews/AppDelegate.m index 25c9f3e..9b9e92d 100644 --- a/HackerNews/AppDelegate.m +++ b/HackerNews/AppDelegate.m @@ -11,11 +11,15 @@ #import "IIViewDeckController.h" #import "NavigationDeckViewController.h" #import "HNManager.h" +#import "PocketAPI.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + //Register in the Pocket API. + [[PocketAPI sharedAPI] setConsumerKey:@"20118-9a164e727c7246cdc440dcab"]; + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Set Pro & Start HNManager Session @@ -84,4 +88,14 @@ - (void)applicationWillTerminate:(UIApplication *)application // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation +{ + if ([[PocketAPI sharedAPI] handleOpenURL:url]) { + return YES; + } else { + // Handle other url-schemes here. + return NO; + } +} + @end diff --git a/HackerNews/DRPocketActivity/DRPocketActivity.h b/HackerNews/DRPocketActivity/DRPocketActivity.h new file mode 100644 index 0000000..0296af6 --- /dev/null +++ b/HackerNews/DRPocketActivity/DRPocketActivity.h @@ -0,0 +1,13 @@ +// +// DRPocketActivity.h +// HackerNews +// +// Created by Daniel Rosado on 09/11/13. +// Copyright (c) 2013 Benjamin Gordon. All rights reserved. +// + +#import + +@interface DRPocketActivity : UIActivity + +@end \ No newline at end of file diff --git a/HackerNews/DRPocketActivity/DRPocketActivity.m b/HackerNews/DRPocketActivity/DRPocketActivity.m new file mode 100644 index 0000000..e249663 --- /dev/null +++ b/HackerNews/DRPocketActivity/DRPocketActivity.m @@ -0,0 +1,66 @@ +// +// DRPocketActivity.m +// HackerNews +// +// Created by Daniel Rosado on 09/11/13. +// Copyright (c) 2013 Benjamin Gordon. All rights reserved. +// + +#import "PocketAPI.h" +#import "DRPocketActivity.h" +#import "SVProgressHUD.h" +#import "LoadingHUDViewController.h" + +@implementation DRPocketActivity { + NSURL *_URL; +} + +- (NSString *)activityType { + return NSStringFromClass([self class]); +} + +- (NSString *)activityTitle { + return NSLocalizedStringFromTable(@"Save to Pocket", @"DRPocketActivity", nil); +} + +- (UIImage *)activityImage { + return [UIImage imageNamed:@"PocketActivity.png"]; +} + +- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { + for (id activityItem in activityItems) { + if ([activityItem isKindOfClass:[NSURL class]] && [[UIApplication sharedApplication] canOpenURL:activityItem]) { + return YES; + } + } + + return NO; +} + +- (void)prepareWithActivityItems:(NSArray *)activityItems { + for (id activityItem in activityItems) { + if ([activityItem isKindOfClass:[NSURL class]]) { + _URL = ((NSURL*)activityItem).absoluteURL; + } + } +} + +- (UIViewController *)activityViewController +{ + [self performSelector:@selector(saveToPocket) withObject:nil afterDelay:0]; + return [[LoadingHUDViewController alloc] initWithLoadingStatus:@"Saving..."]; +} + +- (void)saveToPocket { + [[PocketAPI sharedAPI] saveURL:_URL handler: ^(PocketAPI *API, NSURL *URL, NSError *error) { + BOOL activityCompletedSuccessfully = error ? NO : YES; + + double delayInSeconds = 0.5; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ + [self activityDidFinish:activityCompletedSuccessfully]; + }); + }]; +} + +@end \ No newline at end of file diff --git a/HackerNews/DRPocketActivity/LoadingHUDViewController.h b/HackerNews/DRPocketActivity/LoadingHUDViewController.h new file mode 100644 index 0000000..2dbe4ee --- /dev/null +++ b/HackerNews/DRPocketActivity/LoadingHUDViewController.h @@ -0,0 +1,15 @@ +// +// LoadingHUDViewController.h +// HackerNews +// +// Created by Daniel Rosado on 09/11/13. +// Copyright (c) 2013 Benjamin Gordon. All rights reserved. +// + +#import + +@interface LoadingHUDViewController : UIViewController + +- (instancetype)initWithLoadingStatus:(NSString*)string; + +@end diff --git a/HackerNews/DRPocketActivity/LoadingHUDViewController.m b/HackerNews/DRPocketActivity/LoadingHUDViewController.m new file mode 100644 index 0000000..5bfed5e --- /dev/null +++ b/HackerNews/DRPocketActivity/LoadingHUDViewController.m @@ -0,0 +1,36 @@ +// +// LoadingHUDViewController.m +// HackerNews +// +// Created by Daniel Rosado on 09/11/13. +// Copyright (c) 2013 Benjamin Gordon. All rights reserved. +// + +#import "LoadingHUDViewController.h" +#import "SVProgressHUD.h" + +@implementation LoadingHUDViewController +{ + NSString *_loadingString; +} + +- (instancetype)initWithLoadingStatus:(NSString*)string +{ + self = [super initWithNibName:nil bundle:nil]; + if (self) { + _loadingString = string; + } + + return self; +} + +- (void)loadView +{ + UIView *vw = [[UIView alloc] initWithFrame:[UIApplication sharedApplication].keyWindow.bounds]; + vw.backgroundColor = [UIColor clearColor]; + self.view = vw; + + [SVProgressHUD showWithStatus:_loadingString maskType:SVProgressHUDMaskTypeGradient]; +} + +@end diff --git a/HackerNews/DRPocketActivity/PocketActivity.png b/HackerNews/DRPocketActivity/PocketActivity.png new file mode 100644 index 0000000..e7c1cf1 Binary files /dev/null and b/HackerNews/DRPocketActivity/PocketActivity.png differ diff --git a/HackerNews/DRPocketActivity/PocketActivity@2x.png b/HackerNews/DRPocketActivity/PocketActivity@2x.png new file mode 100644 index 0000000..e908700 Binary files /dev/null and b/HackerNews/DRPocketActivity/PocketActivity@2x.png differ diff --git a/HackerNews/DRPocketActivity/PocketActivity@2x~ipad.png b/HackerNews/DRPocketActivity/PocketActivity@2x~ipad.png new file mode 100644 index 0000000..6c452fe Binary files /dev/null and b/HackerNews/DRPocketActivity/PocketActivity@2x~ipad.png differ diff --git a/HackerNews/DRPocketActivity/PocketActivity~ipad.png b/HackerNews/DRPocketActivity/PocketActivity~ipad.png new file mode 100644 index 0000000..67e49e2 Binary files /dev/null and b/HackerNews/DRPocketActivity/PocketActivity~ipad.png differ diff --git a/HackerNews/HNPostURL.h b/HackerNews/HNPostURL.h new file mode 100644 index 0000000..3b70595 --- /dev/null +++ b/HackerNews/HNPostURL.h @@ -0,0 +1,18 @@ +// +// HNPostURL.h +// HackerNews +// +// Created by Daniel Rosado on 09/11/13. +// Copyright (c) 2013 Benjamin Gordon. All rights reserved. +// + +#import + +@interface HNPostURL : NSURL + +- (id)initWithString:(NSString *)URLString andMobileFriendlyString:(NSString*)mFriendlyURLString; + +@property (nonatomic, readonly, strong) NSString *mobileFriendlyAbsoluteString; +@property (nonatomic, readonly, strong) NSURL *mobileFriendlyAbsoluteURL; + +@end diff --git a/HackerNews/HNPostURL.m b/HackerNews/HNPostURL.m new file mode 100644 index 0000000..e209cb4 --- /dev/null +++ b/HackerNews/HNPostURL.m @@ -0,0 +1,44 @@ +// +// HNPostURL.m +// HackerNews +// +// Created by Daniel Rosado on 09/11/13. +// Copyright (c) 2013 Benjamin Gordon. All rights reserved. +// + +#import "HNPostURL.h" + +@interface HNPostURL () + +@property (nonatomic, readwrite, strong) NSString *mobileFriendlyAbsoluteString; +@property (nonatomic, readwrite, strong) NSURL *mobileFriendlyAbsoluteURL; + +@end + + +@implementation HNPostURL + +#pragma mark - Designated initializer +- (id)initWithString:(NSString *)URLString andMobileFriendlyString:(NSString*)mFriendlyURLString +{ + self = [super initWithString:URLString]; + if (self) { + _mobileFriendlyAbsoluteString = mFriendlyURLString; + _mobileFriendlyAbsoluteURL = [[NSURL alloc] initWithString:mFriendlyURLString]; + } + + return self; +} + +#pragma mark - Mobile friendly getters +- (NSString*)mobileFriendlyAbsoluteString +{ + return _mobileFriendlyAbsoluteString; +} + +- (NSURL*)mobileFriendlyAbsoluteURL +{ + return _mobileFriendlyAbsoluteURL; +} + +@end diff --git a/HackerNews/HackerNews-Info.plist b/HackerNews/HackerNews-Info.plist index ad474ed..6eb8c3a 100644 --- a/HackerNews/HackerNews-Info.plist +++ b/HackerNews/HackerNews-Info.plist @@ -2,6 +2,17 @@ + CFBundleURLTypes + + + CFBundleURLName + com.getpocket.sdk + CFBundleURLSchemes + + pocketapp20118 + + + CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/HackerNews/LinksViewController.m b/HackerNews/LinksViewController.m index 6f590ad..d9b080e 100644 --- a/HackerNews/LinksViewController.m +++ b/HackerNews/LinksViewController.m @@ -11,7 +11,10 @@ #import "Helpers.h" #import "ARChromeActivity.h" #import "TUSafariActivity.h" +#import "DRPocketActivity/DRPocketActivity.h" #import "KGStatusBar.h" +#import "SVProgressHUD.h" +#import "HNPostURL.h" @interface LinksViewController () @property (weak, nonatomic) IBOutlet UIWebView *LinkWebView; @@ -109,14 +112,22 @@ - (void)upvoteCurrentPost { #pragma mark - Share - (void)didClickShare { - NSURL *urlToShare = self.LinkWebView.request.URL; - NSArray *activityItems = @[ urlToShare ]; - + + NSArray *activityItems = @[[self buildPostURL]]; + ARChromeActivity *chromeActivity = [[ARChromeActivity alloc] init]; TUSafariActivity *safariActivity = [[TUSafariActivity alloc] init]; - NSArray *applicationActivities = @[ safariActivity, chromeActivity ]; + DRPocketActivity *pocketActivity = [[DRPocketActivity alloc] init]; + NSArray *applicationActivities = @[ pocketActivity, safariActivity, chromeActivity ]; UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:applicationActivities]; + activityController.completionHandler = ^(NSString *activityType, BOOL completed) { + if ([activityType isEqualToString:pocketActivity.activityType]) { + completed + ? [SVProgressHUD showSuccessWithStatus:@"Saved to pocket"] + : [SVProgressHUD showErrorWithStatus:@"Unable to save to pocket"]; + } + }; [self presentViewController:activityController animated:YES completion:nil]; } @@ -127,6 +138,14 @@ - (void)didClickComment { } } +- (HNPostURL *)buildPostURL +{ + NSString *readabilityURLString = self.LinkWebView.request.URL.absoluteString; + NSString *originalPostUrlString = self.Post.UrlString; + return [[HNPostURL alloc] initWithString:originalPostUrlString andMobileFriendlyString:readabilityURLString]; +} + + #pragma mark - Change Readability - (void)didChangeReadability:(NSNotification *)notification { self.Readability = [[NSUserDefaults standardUserDefaults] boolForKey:@"Readability"]; @@ -155,4 +174,19 @@ - (void)webViewDidFinishLoad:(UIWebView *)webView { [self.indicator removeFromSuperview]; } + +#pragma mark - Activity result HUD +- (void)showActivityResultHUDWithText:(NSString*)text { + [self showActivityResultHUDWithText:text andDismissAfter:0]; +} +- (void)showActivityResultHUDWithText:(NSString*)text andDismissAfter:(NSTimeInterval)seconds { + + [SVProgressHUD showWithStatus:text]; + double delayInSeconds = seconds; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ + [SVProgressHUD dismiss]; + }); +} + @end diff --git a/HackerNews/SVProgressHUD.h b/HackerNews/SVProgressHUD.h new file mode 100644 index 0000000..cdf925f --- /dev/null +++ b/HackerNews/SVProgressHUD.h @@ -0,0 +1,67 @@ +// +// SVProgressHUD.h +// +// Created by Sam Vermette on 27.03.11. +// Copyright 2011 Sam Vermette. All rights reserved. +// +// https://github.com/samvermette/SVProgressHUD +// + +#import +#import + +extern NSString * const SVProgressHUDDidReceiveTouchEventNotification; +extern NSString * const SVProgressHUDWillDisappearNotification; +extern NSString * const SVProgressHUDDidDisappearNotification; +extern NSString * const SVProgressHUDWillAppearNotification; +extern NSString * const SVProgressHUDDidAppearNotification; + +extern NSString * const SVProgressHUDStatusUserInfoKey; + +enum { + SVProgressHUDMaskTypeNone = 1, // allow user interactions while HUD is displayed + SVProgressHUDMaskTypeClear, // don't allow + SVProgressHUDMaskTypeBlack, // don't allow and dim the UI in the back of the HUD + SVProgressHUDMaskTypeGradient // don't allow and dim the UI with a a-la-alert-view bg gradient +}; + +typedef NSUInteger SVProgressHUDMaskType; + +@interface SVProgressHUD : UIView + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 +@property (readwrite, nonatomic, retain) UIColor *hudBackgroundColor NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; +@property (readwrite, nonatomic, retain) UIColor *hudForegroundColor NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; +@property (readwrite, nonatomic, retain) UIColor *hudStatusShadowColor NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; +@property (readwrite, nonatomic, retain) UIColor *hudRingBackgroundColor NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; +@property (readwrite, nonatomic, retain) UIColor *hudRingForegroundColor NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; +@property (readwrite, nonatomic, retain) UIFont *hudFont NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; +@property (readwrite, nonatomic, retain) UIImage *hudSuccessImage NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; +@property (readwrite, nonatomic, retain) UIImage *hudErrorImage NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; +#endif + ++ (void)setOffsetFromCenter:(UIOffset)offset; ++ (void)resetOffsetFromCenter; + ++ (void)show; ++ (void)showWithMaskType:(SVProgressHUDMaskType)maskType; ++ (void)showWithStatus:(NSString*)status; ++ (void)showWithStatus:(NSString*)status maskType:(SVProgressHUDMaskType)maskType; + ++ (void)showProgress:(float)progress; ++ (void)showProgress:(float)progress status:(NSString*)status; ++ (void)showProgress:(float)progress status:(NSString*)status maskType:(SVProgressHUDMaskType)maskType; + ++ (void)setStatus:(NSString*)string; // change the HUD loading status while it's showing + +// stops the activity indicator, shows a glyph + status, and dismisses HUD 1s later ++ (void)showSuccessWithStatus:(NSString*)string; ++ (void)showErrorWithStatus:(NSString *)string; ++ (void)showImage:(UIImage*)image status:(NSString*)status; // use 28x28 white pngs + ++ (void)popActivity; ++ (void)dismiss; + ++ (BOOL)isVisible; + +@end diff --git a/HackerNews/SVProgressHUD.m b/HackerNews/SVProgressHUD.m new file mode 100644 index 0000000..fe6cc8e --- /dev/null +++ b/HackerNews/SVProgressHUD.m @@ -0,0 +1,974 @@ +// +// SVProgressHUD.m +// +// Created by Sam Vermette on 27.03.11. +// Copyright 2011 Sam Vermette. All rights reserved. +// +// https://github.com/samvermette/SVProgressHUD +// + +#if !__has_feature(objc_arc) +#error SVProgressHUD is ARC only. Either turn on ARC for the project or use -fobjc-arc flag +#endif + +#import "SVProgressHUD.h" +#import + +NSString * const SVProgressHUDDidReceiveTouchEventNotification = @"SVProgressHUDDidReceiveTouchEventNotification"; +NSString * const SVProgressHUDWillDisappearNotification = @"SVProgressHUDWillDisappearNotification"; +NSString * const SVProgressHUDDidDisappearNotification = @"SVProgressHUDDidDisappearNotification"; +NSString * const SVProgressHUDWillAppearNotification = @"SVProgressHUDWillAppearNotification"; +NSString * const SVProgressHUDDidAppearNotification = @"SVProgressHUDDidAppearNotification"; + +NSString * const SVProgressHUDStatusUserInfoKey = @"SVProgressHUDStatusUserInfoKey"; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 +CGFloat SVProgressHUDRingRadius = 14; +CGFloat SVProgressHUDRingThickness = 1; +#else +CGFloat SVProgressHUDRingRadius = 14; +CGFloat SVProgressHUDRingThickness = 6; +#endif + +@interface SVProgressHUD () + +@property (nonatomic, readwrite) SVProgressHUDMaskType maskType; +@property (nonatomic, strong, readonly) NSTimer *fadeOutTimer; +@property (nonatomic, readonly, getter = isClear) BOOL clear; + +@property (nonatomic, strong, readonly) UIControl *overlayView; +@property (nonatomic, strong, readonly) UIView *hudView; +@property (nonatomic, strong, readonly) UILabel *stringLabel; +@property (nonatomic, strong, readonly) UIImageView *imageView; +@property (nonatomic, strong, readonly) UIActivityIndicatorView *spinnerView; + +@property (nonatomic, readwrite) CGFloat progress; +@property (nonatomic, readwrite) NSUInteger activityCount; +@property (nonatomic, strong) CAShapeLayer *backgroundRingLayer; +@property (nonatomic, strong) CAShapeLayer *ringLayer; + +@property (nonatomic, readonly) CGFloat visibleKeyboardHeight; +@property (nonatomic, assign) UIOffset offsetFromCenter; + +- (void)showProgress:(float)progress + status:(NSString*)string + maskType:(SVProgressHUDMaskType)hudMaskType; + +- (void)showImage:(UIImage*)image + status:(NSString*)status + duration:(NSTimeInterval)duration; + +- (void)dismiss; + +- (void)setStatus:(NSString*)string; +- (void)registerNotifications; +- (NSDictionary *)notificationUserInfo; +- (void)moveToPoint:(CGPoint)newCenter rotateAngle:(CGFloat)angle; +- (void)positionHUD:(NSNotification*)notification; +- (NSTimeInterval)displayDurationForString:(NSString*)string; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 50000 +- (UIColor *)hudBackgroundColor; +- (UIColor *)hudForegroundColor; +- (UIColor *)hudStatusShadowColor; +- (UIColor *)hudRingBackgroundColor; +- (UIColor *)hudRingForegroundColor; +- (UIFont *)hudFont; +- (UIImage *)hudSuccessImage; +- (UIImage *)hudErrorImage; +#endif + +@end + + +@implementation SVProgressHUD + +@synthesize overlayView, hudView, maskType, fadeOutTimer, stringLabel, imageView, spinnerView, visibleKeyboardHeight; +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 +@synthesize hudBackgroundColor = _uiHudBgColor; +@synthesize hudForegroundColor = _uiHudFgColor; +@synthesize hudStatusShadowColor = _uiHudStatusShColor; +@synthesize hudRingBackgroundColor = _uiHudRingBgColor; +@synthesize hudRingForegroundColor = _uiHudRingFgColor; +@synthesize hudFont = _uiHudFont; +@synthesize hudSuccessImage = _uiHudSuccessImage; +@synthesize hudErrorImage = _uiHudErrorImage; +#endif + + ++ (SVProgressHUD*)sharedView { + static dispatch_once_t once; + static SVProgressHUD *sharedView; + dispatch_once(&once, ^ { sharedView = [[self alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; }); + return sharedView; +} + + ++ (void)setStatus:(NSString *)string { + [[self sharedView] setStatus:string]; +} + +#pragma mark - Show Methods + ++ (void)show { + [[self sharedView] showProgress:-1 status:nil maskType:SVProgressHUDMaskTypeNone]; +} + ++ (void)showWithStatus:(NSString *)status { + [[self sharedView] showProgress:-1 status:status maskType:SVProgressHUDMaskTypeNone]; +} + ++ (void)showWithMaskType:(SVProgressHUDMaskType)maskType { + [[self sharedView] showProgress:-1 status:nil maskType:maskType]; +} + ++ (void)showWithStatus:(NSString*)status maskType:(SVProgressHUDMaskType)maskType { + [[self sharedView] showProgress:-1 status:status maskType:maskType]; +} + ++ (void)showProgress:(float)progress { + [[self sharedView] showProgress:progress status:nil maskType:SVProgressHUDMaskTypeNone]; +} + ++ (void)showProgress:(float)progress status:(NSString *)status { + [[self sharedView] showProgress:progress status:status maskType:SVProgressHUDMaskTypeNone]; +} + ++ (void)showProgress:(float)progress status:(NSString *)status maskType:(SVProgressHUDMaskType)maskType { + [[self sharedView] showProgress:progress status:status maskType:maskType]; +} + +#pragma mark - Show then dismiss methods + ++ (void)showSuccessWithStatus:(NSString *)string { + [self showImage:[[self sharedView] hudSuccessImage] status:string]; +} + ++ (void)showErrorWithStatus:(NSString *)string { + [self showImage:[[self sharedView] hudErrorImage] status:string]; +} + ++ (void)showImage:(UIImage *)image status:(NSString *)string { + NSTimeInterval displayInterval = [[SVProgressHUD sharedView] displayDurationForString:string]; + [[self sharedView] showImage:image status:string duration:displayInterval]; +} + + +#pragma mark - Dismiss Methods + ++ (void)popActivity { + [self sharedView].activityCount--; + if([self sharedView].activityCount == 0) + [[self sharedView] dismiss]; +} + ++ (void)dismiss { + if ([self isVisible]) { + [[self sharedView] dismiss]; + } +} + + +#pragma mark - Offset + ++ (void)setOffsetFromCenter:(UIOffset)offset { + [self sharedView].offsetFromCenter = offset; +} + ++ (void)resetOffsetFromCenter { + [self setOffsetFromCenter:UIOffsetZero]; +} + +#pragma mark - Instance Methods + +- (id)initWithFrame:(CGRect)frame { + + if ((self = [super initWithFrame:frame])) { + self.userInteractionEnabled = NO; + self.backgroundColor = [UIColor clearColor]; + self.alpha = 0; + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.activityCount = 0; + } + + return self; +} + +- (void)drawRect:(CGRect)rect { + + CGContextRef context = UIGraphicsGetCurrentContext(); + + switch (self.maskType) { + + case SVProgressHUDMaskTypeBlack: { + [[UIColor colorWithWhite:0 alpha:0.5] set]; + CGContextFillRect(context, self.bounds); + break; + } + + case SVProgressHUDMaskTypeGradient: { + + size_t locationsCount = 2; + CGFloat locations[2] = {0.0f, 1.0f}; + CGFloat colors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.75f}; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, colors, locations, locationsCount); + CGColorSpaceRelease(colorSpace); + + CGFloat freeHeight = self.bounds.size.height - self.visibleKeyboardHeight; + + CGPoint center = CGPointMake(self.bounds.size.width/2, freeHeight/2); + float radius = MIN(self.bounds.size.width , self.bounds.size.height) ; + CGContextDrawRadialGradient (context, gradient, center, 0, center, radius, kCGGradientDrawsAfterEndLocation); + CGGradientRelease(gradient); + + break; + } + } +} + +- (void)updatePosition { + + CGFloat hudWidth = 100; + CGFloat hudHeight = 100; + CGFloat stringHeightBuffer = 20; + CGFloat stringAndImageHeightBuffer = 80; + + CGFloat stringWidth = 0; + CGFloat stringHeight = 0; + CGRect labelRect = CGRectZero; + + NSString *string = self.stringLabel.text; + // False if it's text-only + BOOL imageUsed = (self.imageView.image) || (self.imageView.hidden); + + if(string) { + CGSize constraintSize = CGSizeMake(200, 300); +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + CGRect stringRect = [string boundingRectWithSize:constraintSize options:(NSStringDrawingUsesFontLeading|NSStringDrawingTruncatesLastVisibleLine|NSStringDrawingUsesLineFragmentOrigin) attributes:@{NSFontAttributeName: self.stringLabel.font} context:NULL]; + stringWidth = stringRect.size.width; + stringHeight = stringRect.size.height; +#else + CGSize stringSize = [string sizeWithFont:self.stringLabel.font constrainedToSize:constraintSize]; + stringWidth = stringSize.width; + stringHeight = stringSize.height; +#endif + + if (imageUsed) + hudHeight = stringAndImageHeightBuffer + stringHeight; + else + hudHeight = stringHeightBuffer + stringHeight; + + if(stringWidth > hudWidth) + hudWidth = ceil(stringWidth/2)*2; + + CGFloat labelRectY = imageUsed ? 66 : 9; + + if(hudHeight > 100) { + labelRect = CGRectMake(12, labelRectY, hudWidth, stringHeight); + hudWidth+=24; + } else { + hudWidth+=24; + labelRect = CGRectMake(0, labelRectY, hudWidth, stringHeight); + } + } + + self.hudView.bounds = CGRectMake(0, 0, hudWidth, hudHeight); + + if(string) + self.imageView.center = CGPointMake(CGRectGetWidth(self.hudView.bounds)/2, 36); + else + self.imageView.center = CGPointMake(CGRectGetWidth(self.hudView.bounds)/2, CGRectGetHeight(self.hudView.bounds)/2); + + self.stringLabel.hidden = NO; + self.stringLabel.frame = labelRect; + + if(string) { + self.spinnerView.center = CGPointMake(ceil(CGRectGetWidth(self.hudView.bounds)/2)+0.5, 40.5); + + if(self.progress != -1) + self.backgroundRingLayer.position = self.ringLayer.position = CGPointMake((CGRectGetWidth(self.hudView.bounds)/2), 36); + } + else { + self.spinnerView.center = CGPointMake(ceil(CGRectGetWidth(self.hudView.bounds)/2)+0.5, ceil(self.hudView.bounds.size.height/2)+0.5); + + if(self.progress != -1) + self.backgroundRingLayer.position = self.ringLayer.position = CGPointMake((CGRectGetWidth(self.hudView.bounds)/2), CGRectGetHeight(self.hudView.bounds)/2); + } + +} + +- (void)setStatus:(NSString *)string { + + self.stringLabel.text = string; + [self updatePosition]; + +} + +- (void)setFadeOutTimer:(NSTimer *)newTimer { + + if(fadeOutTimer) + [fadeOutTimer invalidate], fadeOutTimer = nil; + + if(newTimer) + fadeOutTimer = newTimer; +} + + +- (void)registerNotifications { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIApplicationDidChangeStatusBarOrientationNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIKeyboardWillHideNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIKeyboardDidHideNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIKeyboardWillShowNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIKeyboardDidShowNotification + object:nil]; +} + + +- (NSDictionary *)notificationUserInfo +{ + return (self.stringLabel.text ? @{SVProgressHUDStatusUserInfoKey : self.stringLabel.text} : nil); +} + + +- (void)positionHUD:(NSNotification*)notification { + + CGFloat keyboardHeight; + double animationDuration; + + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + + if(notification) { + NSDictionary* keyboardInfo = [notification userInfo]; + CGRect keyboardFrame = [[keyboardInfo valueForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]; + animationDuration = [[keyboardInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + if(notification.name == UIKeyboardWillShowNotification || notification.name == UIKeyboardDidShowNotification) { + if(UIInterfaceOrientationIsPortrait(orientation)) + keyboardHeight = keyboardFrame.size.height; + else + keyboardHeight = keyboardFrame.size.width; + } else + keyboardHeight = 0; + } else { + keyboardHeight = self.visibleKeyboardHeight; + } + + CGRect orientationFrame = [UIScreen mainScreen].bounds; + CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame; + + if(UIInterfaceOrientationIsLandscape(orientation)) { + float temp = orientationFrame.size.width; + orientationFrame.size.width = orientationFrame.size.height; + orientationFrame.size.height = temp; + + temp = statusBarFrame.size.width; + statusBarFrame.size.width = statusBarFrame.size.height; + statusBarFrame.size.height = temp; + } + + CGFloat activeHeight = orientationFrame.size.height; + + if(keyboardHeight > 0) + activeHeight += statusBarFrame.size.height*2; + + activeHeight -= keyboardHeight; + CGFloat posY = floor(activeHeight*0.45); + CGFloat posX = orientationFrame.size.width/2; + + CGPoint newCenter; + CGFloat rotateAngle; + + switch (orientation) { + case UIInterfaceOrientationPortraitUpsideDown: + rotateAngle = M_PI; + newCenter = CGPointMake(posX, orientationFrame.size.height-posY); + break; + case UIInterfaceOrientationLandscapeLeft: + rotateAngle = -M_PI/2.0f; + newCenter = CGPointMake(posY, posX); + break; + case UIInterfaceOrientationLandscapeRight: + rotateAngle = M_PI/2.0f; + newCenter = CGPointMake(orientationFrame.size.height-posY, posX); + break; + default: // as UIInterfaceOrientationPortrait + rotateAngle = 0.0; + newCenter = CGPointMake(posX, posY); + break; + } + + if(notification) { + [UIView animateWithDuration:animationDuration + delay:0 + options:UIViewAnimationOptionAllowUserInteraction + animations:^{ + [self moveToPoint:newCenter rotateAngle:rotateAngle]; + } completion:NULL]; + } + + else { + [self moveToPoint:newCenter rotateAngle:rotateAngle]; + } + +} + +- (void)moveToPoint:(CGPoint)newCenter rotateAngle:(CGFloat)angle { + self.hudView.transform = CGAffineTransformMakeRotation(angle); + self.hudView.center = CGPointMake(newCenter.x + self.offsetFromCenter.horizontal, newCenter.y + self.offsetFromCenter.vertical); +} + +- (void)overlayViewDidReceiveTouchEvent:(id)sender forEvent:(UIEvent *)event { + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidReceiveTouchEventNotification object:event]; +} + +#pragma mark - Master show/dismiss methods + +- (void)showProgress:(float)progress status:(NSString*)string maskType:(SVProgressHUDMaskType)hudMaskType { + + if(!self.overlayView.superview){ + NSEnumerator *frontToBackWindows = [[[UIApplication sharedApplication]windows]reverseObjectEnumerator]; + + for (UIWindow *window in frontToBackWindows) + if (window.windowLevel == UIWindowLevelNormal) { + [window addSubview:self.overlayView]; + break; + } + } + + if(!self.superview) + [self.overlayView addSubview:self]; + + self.fadeOutTimer = nil; + self.imageView.hidden = YES; + self.maskType = hudMaskType; + self.progress = progress; + + self.stringLabel.text = string; + [self updatePosition]; + + if(progress >= 0) { + self.imageView.image = nil; + self.imageView.hidden = NO; + [self.spinnerView stopAnimating]; + self.ringLayer.strokeEnd = progress; + + if(progress == 0) + self.activityCount++; + } + else { + self.activityCount++; + [self cancelRingLayerAnimation]; + [self.spinnerView startAnimating]; + } + + if(self.maskType != SVProgressHUDMaskTypeNone) { + self.overlayView.userInteractionEnabled = YES; + self.accessibilityLabel = string; + self.isAccessibilityElement = YES; + } + else { + self.overlayView.userInteractionEnabled = NO; + self.hudView.accessibilityLabel = string; + self.hudView.isAccessibilityElement = YES; + } + + [self.overlayView setHidden:NO]; + [self positionHUD:nil]; + + if(self.alpha != 1) { + NSDictionary *userInfo = [self notificationUserInfo]; + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDWillAppearNotification + object:nil + userInfo:userInfo]; + + [self registerNotifications]; + self.hudView.transform = CGAffineTransformScale(self.hudView.transform, 1.3, 1.3); + + if(self.isClear) { + self.alpha = 1; + self.hudView.alpha = 0; + } + + [UIView animateWithDuration:0.15 + delay:0 + options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState + animations:^{ + self.hudView.transform = CGAffineTransformScale(self.hudView.transform, 1/1.3, 1/1.3); + + if(self.isClear) // handle iOS 7 UIToolbar not answer well to hierarchy opacity change + self.hudView.alpha = 1; + else + self.alpha = 1; + } + completion:^(BOOL finished){ + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidAppearNotification + object:nil + userInfo:userInfo]; + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, string); + }]; + + [self setNeedsDisplay]; + } +} + + +- (void)showImage:(UIImage *)image status:(NSString *)string duration:(NSTimeInterval)duration { + self.progress = -1; + [self cancelRingLayerAnimation]; + + if(![self.class isVisible]) + [self.class show]; + + self.imageView.image = image; + self.imageView.hidden = NO; + + self.stringLabel.text = string; + [self updatePosition]; + [self.spinnerView stopAnimating]; + + if(self.maskType != SVProgressHUDMaskTypeNone) { + self.accessibilityLabel = string; + self.isAccessibilityElement = YES; + } else { + self.hudView.accessibilityLabel = string; + self.hudView.isAccessibilityElement = YES; + } + + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, string); + + self.fadeOutTimer = [NSTimer timerWithTimeInterval:duration target:self selector:@selector(dismiss) userInfo:nil repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:self.fadeOutTimer forMode:NSRunLoopCommonModes]; +} + +- (void)dismiss { + NSDictionary *userInfo = [self notificationUserInfo]; + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDWillDisappearNotification + object:nil + userInfo:userInfo]; + + self.activityCount = 0; + [UIView animateWithDuration:0.15 + delay:0 + options:UIViewAnimationCurveEaseIn | UIViewAnimationOptionAllowUserInteraction + animations:^{ + self.hudView.transform = CGAffineTransformScale(self.hudView.transform, 0.8, 0.8); + if(self.isClear) // handle iOS 7 UIToolbar not answer well to hierarchy opacity change + self.hudView.alpha = 0; + else + self.alpha = 0; + } + completion:^(BOOL finished){ + if(self.alpha == 0 || self.hudView.alpha == 0) { + self.alpha = 0; + self.hudView.alpha = 0; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self cancelRingLayerAnimation]; + [hudView removeFromSuperview]; + hudView = nil; + + [overlayView removeFromSuperview]; + overlayView = nil; + + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidDisappearNotification + object:nil + userInfo:userInfo]; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + // Tell the rootViewController to update the StatusBar appearance + UIViewController *rootController = [[UIApplication sharedApplication] keyWindow].rootViewController; + if ([rootController respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)]) { + [rootController setNeedsStatusBarAppearanceUpdate]; + } +#endif + // uncomment to make sure UIWindow is gone from app.windows + //NSLog(@"%@", [UIApplication sharedApplication].windows); + //NSLog(@"keyWindow = %@", [UIApplication sharedApplication].keyWindow); + } + }]; +} + + +#pragma mark - +#pragma mark Ring progress animation + +- (CAShapeLayer *)ringLayer { + if(!_ringLayer) { + CGPoint center = CGPointMake(CGRectGetWidth(hudView.frame)/2, CGRectGetHeight(hudView.frame)/2); + _ringLayer = [self createRingLayerWithCenter:center radius:SVProgressHUDRingRadius lineWidth:SVProgressHUDRingThickness color:self.hudRingForegroundColor]; + [self.hudView.layer addSublayer:_ringLayer]; + } + return _ringLayer; +} + +- (CAShapeLayer *)backgroundRingLayer { + if(!_backgroundRingLayer) { + CGPoint center = CGPointMake(CGRectGetWidth(hudView.frame)/2, CGRectGetHeight(hudView.frame)/2); + _backgroundRingLayer = [self createRingLayerWithCenter:center radius:SVProgressHUDRingRadius lineWidth:SVProgressHUDRingThickness color:self.hudRingBackgroundColor]; + _backgroundRingLayer.strokeEnd = 1; + [self.hudView.layer addSublayer:_backgroundRingLayer]; + } + return _backgroundRingLayer; +} + +- (void)cancelRingLayerAnimation { + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [hudView.layer removeAllAnimations]; + + _ringLayer.strokeEnd = 0.0f; + if (_ringLayer.superlayer) { + [_ringLayer removeFromSuperlayer]; + } + _ringLayer = nil; + + if (_backgroundRingLayer.superlayer) { + [_backgroundRingLayer removeFromSuperlayer]; + } + _backgroundRingLayer = nil; + + [CATransaction commit]; +} + +- (CGPoint)pointOnCircleWithCenter:(CGPoint)center radius:(double)radius angleInDegrees:(double)angleInDegrees { + float x = (float)(radius * cos(angleInDegrees * M_PI / 180)) + radius; + float y = (float)(radius * sin(angleInDegrees * M_PI / 180)) + radius; + return CGPointMake(x, y); +} + + +- (UIBezierPath *)createCirclePathWithCenter:(CGPoint)center radius:(CGFloat)radius sampleCount:(NSInteger)sampleCount { + + UIBezierPath *smoothedPath = [UIBezierPath bezierPath]; + CGPoint startPoint = [self pointOnCircleWithCenter:center radius:radius angleInDegrees:-90]; + + [smoothedPath moveToPoint:startPoint]; + + CGFloat delta = 360.0f/sampleCount; + CGFloat angleInDegrees = -90; + for (NSInteger i=1; i= 70000 + hudView = [[UIToolbar alloc] initWithFrame:CGRectZero]; + ((UIToolbar *)hudView).translucent = YES; + ((UIToolbar *)hudView).barTintColor = self.hudBackgroundColor; +#else + hudView = [[UIView alloc] initWithFrame:CGRectZero]; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + + // UIAppearance is used when iOS >= 5.0 + hudView.backgroundColor = self.hudBackgroundColor; +#endif +#endif + + hudView.layer.cornerRadius = 10; + hudView.layer.masksToBounds = YES; + + hudView.autoresizingMask = (UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin | + UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin); + + [self addSubview:hudView]; + } + return hudView; +} + +- (UILabel *)stringLabel { + if (stringLabel == nil) { + stringLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + stringLabel.backgroundColor = [UIColor clearColor]; + stringLabel.adjustsFontSizeToFitWidth = YES; +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 + stringLabel.textAlignment = UITextAlignmentCenter; +#else + stringLabel.textAlignment = NSTextAlignmentCenter; +#endif + + stringLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + + // UIAppearance is used when iOS >= 5.0 + stringLabel.textColor = self.hudForegroundColor; + stringLabel.font = self.hudFont; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 70000 + stringLabel.shadowColor = self.hudStatusShadowColor; + stringLabel.shadowOffset = CGSizeMake(0, -1); +#endif + stringLabel.numberOfLines = 0; + } + + if(!stringLabel.superview) + [self.hudView addSubview:stringLabel]; + + return stringLabel; +} + +- (UIImageView *)imageView { + if (imageView == nil) + imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 28, 28)]; + + if(!imageView.superview) + [self.hudView addSubview:imageView]; + + return imageView; +} + +- (UIActivityIndicatorView *)spinnerView { + if (spinnerView == nil) { + spinnerView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + spinnerView.hidesWhenStopped = YES; + spinnerView.bounds = CGRectMake(0, 0, 37, 37); + + if([spinnerView respondsToSelector:@selector(setColor:)]) // setColor is iOS 5+ + spinnerView.color = self.hudForegroundColor; + } + + if(!spinnerView.superview) + [self.hudView addSubview:spinnerView]; + + return spinnerView; +} + +- (CGFloat)visibleKeyboardHeight { + + UIWindow *keyboardWindow = nil; + for (UIWindow *testWindow in [[UIApplication sharedApplication] windows]) { + if(![[testWindow class] isEqual:[UIWindow class]]) { + keyboardWindow = testWindow; + break; + } + } + + for (__strong UIView *possibleKeyboard in [keyboardWindow subviews]) { + if([possibleKeyboard isKindOfClass:NSClassFromString(@"UIPeripheralHostView")] || [possibleKeyboard isKindOfClass:NSClassFromString(@"UIKeyboard")]) + return possibleKeyboard.bounds.size.height; + } + + return 0; +} + +#pragma mark - UIAppearance getters + +- (UIColor *)hudBackgroundColor { + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + if(_uiHudBgColor == nil) { + _uiHudBgColor = [[[self class] appearance] hudBackgroundColor]; + } + + if(_uiHudBgColor != nil) { + return _uiHudBgColor; + } +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + return [UIColor whiteColor]; +#else + return [UIColor colorWithWhite:0 alpha:0.8]; +#endif +} + +- (UIColor *)hudForegroundColor { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + if(_uiHudFgColor == nil) { + _uiHudFgColor = [[[self class] appearance] hudForegroundColor]; + } + + if(_uiHudFgColor != nil) { + return _uiHudFgColor; + } +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + return [UIColor colorWithWhite:0 alpha:0.8]; +#else + return [UIColor whiteColor]; +#endif +} + +- (UIColor *)hudRingBackgroundColor { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + if(_uiHudRingBgColor == nil) { + _uiHudRingBgColor = [[[self class] appearance] hudRingBackgroundColor]; + } + + if(_uiHudRingBgColor != nil) { + return _uiHudRingBgColor; + } +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + return [UIColor whiteColor]; +#else + return [UIColor colorWithWhite:0 alpha:0.8]; +#endif +} + +- (UIColor *)hudRingForegroundColor { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + if(_uiHudRingFgColor == nil) { + _uiHudRingFgColor = [[[self class] appearance] hudRingForegroundColor]; + } + + if(_uiHudRingFgColor != nil) { + return _uiHudRingFgColor; + } +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + return self.tintColor; +#else + return [UIColor whiteColor]; +#endif +} + +- (UIColor *)hudStatusShadowColor { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + if(_uiHudStatusShColor == nil) { + _uiHudStatusShColor = [[[self class] appearance] hudStatusShadowColor]; + } + + if(_uiHudStatusShColor != nil) { + return _uiHudStatusShColor; + } +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + return [UIColor colorWithWhite:200.0f/255.0f alpha:0.8]; +#else + return [UIColor blackColor]; +#endif +} + +- (UIFont *)hudFont { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + if(_uiHudFont == nil) { + _uiHudFont = [[[self class] appearance] hudFont]; + } + + if(_uiHudFont != nil) { + return _uiHudFont; + } +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + return [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; +#else + return [UIFont boldSystemFontOfSize:16]; +#endif +} + +- (UIImage *)hudSuccessImage { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + if(_uiHudSuccessImage == nil) { + _uiHudSuccessImage = [[[self class] appearance] hudSuccessImage]; + } + + if(_uiHudSuccessImage != nil) { + return _uiHudSuccessImage; + } +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + return [UIImage imageNamed:@"SVProgressHUD.bundle/success-black"]; +#else + return [UIImage imageNamed:@"SVProgressHUD.bundle/success.png"]; +#endif +} + +- (UIImage *)hudErrorImage { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 + if(_uiHudErrorImage == nil) { + _uiHudErrorImage = [[[self class] appearance] hudErrorImage]; + } + + if(_uiHudErrorImage != nil) { + return _uiHudErrorImage; + } +#endif + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + return [UIImage imageNamed:@"SVProgressHUD.bundle/error-black"]; +#else + return [UIImage imageNamed:@"SVProgressHUD.bundle/error.png"]; +#endif +} + +@end diff --git a/HackerNews/TUSafariActivity/TUSafariActivity.m b/HackerNews/TUSafariActivity/TUSafariActivity.m index a6b7891..bfbd051 100755 --- a/HackerNews/TUSafariActivity/TUSafariActivity.m +++ b/HackerNews/TUSafariActivity/TUSafariActivity.m @@ -28,6 +28,7 @@ // #import "TUSafariActivity.h" +#import "HNPostURL.h" @implementation TUSafariActivity { @@ -52,7 +53,7 @@ - (UIImage *)activityImage - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { for (id activityItem in activityItems) { - if ([activityItem isKindOfClass:[NSURL class]] && [[UIApplication sharedApplication] canOpenURL:activityItem]) { + if ([activityItem isKindOfClass:[HNPostURL class]] && [[UIApplication sharedApplication] canOpenURL:activityItem]) { return YES; } } @@ -62,11 +63,8 @@ - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems - (void)prepareWithActivityItems:(NSArray *)activityItems { - for (id activityItem in activityItems) { - if ([activityItem isKindOfClass:[NSURL class]]) { - _URL = activityItem; - } - } + // Note: Shouldn't we use the original URL here? Not the readibility one. + _URL = ((HNPostURL*)[activityItems lastObject]).mobileFriendlyAbsoluteURL; } - (void)performActivity diff --git a/SDK/PocketAPI+NSOperation.h b/SDK/PocketAPI+NSOperation.h new file mode 100644 index 0000000..a4f7766 --- /dev/null +++ b/SDK/PocketAPI+NSOperation.h @@ -0,0 +1,67 @@ +// +// PocketAPI.h +// PocketSDK +// +// Created by James Yopp on 2012/08/21. +// Copyright (c) 2012 Read It Later, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +// Advanced use if you use your own NSOperationQueues for handling network traffic. +// If you don't need tight control over network requests, just use the simple API. +// Note: May not behave predictably if recoverable errors are encountered. + +@interface PocketAPI (NSOperations) + +-(NSOperation *)saveOperationWithURL:(NSURL *)url + delegate:(id)delegate; + +-(NSOperation *)saveOperationWithURL:(NSURL *)url + title:(NSString *)title + delegate:(id)delegate; + +-(NSOperation *)saveOperationWithURL:(NSURL *)url + title:(NSString *)title + tweetID:(NSString *)tweetID + delegate:(id)delegate; + +-(NSOperation *)methodOperationWithAPIMethod:(NSString *)APIMethod + forHTTPMethod:(PocketAPIHTTPMethod)HTTPMethod + arguments:(NSDictionary *)arguments + delegate:(id)delegate; + +#if NS_BLOCKS_AVAILABLE +-(NSOperation *)saveOperationWithURL:(NSURL *)url + handler:(PocketAPISaveHandler)handler; + +-(NSOperation *)saveOperationWithURL:(NSURL *)url + title:(NSString *)title + handler:(PocketAPISaveHandler)handler; + +-(NSOperation *)saveOperationWithURL:(NSURL *)url + title:(NSString *)title + tweetID:(NSString *)tweetID + handler:(PocketAPISaveHandler)handler; + +-(NSOperation *)methodOperationWithAPIMethod:(NSString *)APIMethod + forHTTPMethod:(PocketAPIHTTPMethod)HTTPMethod + arguments:(NSDictionary *)arguments + handler:(PocketAPIResponseHandler)handler; +#endif + +@end \ No newline at end of file diff --git a/SDK/PocketAPI.h b/SDK/PocketAPI.h new file mode 100644 index 0000000..81dde03 --- /dev/null +++ b/SDK/PocketAPI.h @@ -0,0 +1,119 @@ +// +// PocketAPI.h +// PocketSDK +// +// Created by Steve Streza on 5/29/12. +// Copyright (c) 2012 Read It Later, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +/** + * The PocketAPI class represents a singleton for saving stuff to a user's Pocket list. + * To begin, you will need to obtain an API token from https://getpocket.com/api/ and set it + * on the PocketAPI singleton at some point at the beginning of your application's lifecycle. + * + * APIs are presented in one of four ways, but all behave fundamentally the same. Their differences + * are presented for flexibility for your app. You can use: + * + * - a delegate-based API + * - a block-based API + * - an NSOperation based on a delegate (for advanced uses) + * - an NSOperation based on a block (for advanced uses) + * + * All delegates and blocks are called on the main thread, so you can safely update UI from there. + * + * You can find more information on these in PocketAPITypes.h + * + * These classes are not implemented as ARC, but will interoperate with ARC. You will need to add the + * -fno-objc-arc compiler flag to each of the files in the SDK. + */ + +#import +#import "PocketAPITypes.h" + +@class PocketAPILogin; + +@interface PocketAPI : NSObject +{ + NSString *consumerKey; + NSString *URLScheme; + NSOperationQueue *operationQueue; + + PocketAPILogin *currentLogin; + NSString *userAgent; +} + +@property (nonatomic, retain) NSString *consumerKey; +@property (nonatomic, retain) NSString *URLScheme; // if you do not set this, it is derived from your consumer key + +@property (nonatomic, copy, readonly) NSString *username; +@property (nonatomic, assign, readonly, getter=isLoggedIn) BOOL loggedIn; + +@property (nonatomic, retain) NSOperationQueue *operationQueue; + ++(PocketAPI *)sharedAPI; ++(BOOL)hasPocketAppInstalled; ++(NSString *)pocketAppURLScheme; + +-(void)setConsumerKey:(NSString *)consumerKey; + +-(NSUInteger)appID; + +// Simple API +-(void)loginWithDelegate:(id)delegate; + +-(void)saveURL:(NSURL *)url + delegate:(id)delegate; +-(void)saveURL:(NSURL *)url + withTitle:(NSString *)title + delegate:(id)delegate; +-(void)saveURL:(NSURL *)url + withTitle:(NSString *)title + tweetID:(NSString *)tweetID + delegate:(id)delegate; + +-(void)callAPIMethod:(NSString *)apiMethod + withHTTPMethod:(PocketAPIHTTPMethod)HTTPMethod + arguments:(NSDictionary *)arguments + delegate:(id)delegate; + +#if NS_BLOCKS_AVAILABLE +-(void)loginWithHandler:(PocketAPILoginHandler)handler; + +-(void)saveURL:(NSURL *)url + handler:(PocketAPISaveHandler)handler; +-(void)saveURL:(NSURL *)url + withTitle:(NSString *)title + handler:(PocketAPISaveHandler)handler; +-(void)saveURL:(NSURL *)url + withTitle:(NSString *)title + tweetID:(NSString *)tweetID + handler:(PocketAPISaveHandler)handler; + +-(void)callAPIMethod:(NSString *)apiMethod + withHTTPMethod:(PocketAPIHTTPMethod)HTTPMethod + arguments:(NSDictionary *)arguments + handler:(PocketAPIResponseHandler)handler; +#endif + +-(void)logout; + +-(BOOL)handleOpenURL:(NSURL *)url; + +@end + +extern NSString *PocketAPITweetID(unsigned long long tweetID); diff --git a/SDK/PocketAPI.m b/SDK/PocketAPI.m new file mode 100644 index 0000000..08471bb --- /dev/null +++ b/SDK/PocketAPI.m @@ -0,0 +1,794 @@ +// +// PocketAPI.m +// PocketSDK +// +// Created by Steve Streza on 5/29/12. +// Copyright (c) 2012 Read It Later, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import "PocketAPI.h" +#import "PocketAPI+NSOperation.h" +#import "PocketAPILogin.h" +#import "PocketAPIOperation.h" +#import +#import +#import + +#define POCKET_SDK_VERSION @"1.0.2" + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#define PocketGlobalKeychainServiceName @"PocketAPI" +#else +#define PocketGlobalKeychainServiceName [NSString stringWithFormat:@"%@.PocketAPI", [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleIdentifierKey]] +#endif + +static NSString *kPocketAPICurrentLoginKey = @"PocketAPICurrentLogin"; + +#pragma mark Private APIs (please do not call these directly) + +@interface PocketAPI () + ++(NSString *)pkt_hashForConsumerKey:(NSString *)consumerKey accessToken:(NSString *)accessToken; + +-(NSString *)pkt_getToken; + +-(PocketAPILogin *)pkt_loadCurrentLoginFromDefaults; +-(void)pkt_saveCurrentLoginToDefaults; + +-(NSDictionary *)pkt_actionDictionaryWithName:(NSString *)name parameters:(NSDictionary *)params; + +@end + +@interface PocketAPI (Credentials) + +-(void)pkt_setKeychainValue:(id)value forKey:(NSString *)key; +-(id)pkt_getKeychainValueForKey:(NSString *)key; + +-(void)pkt_setKeychainValue:(id)value forKey:(NSString *)key serviceName:(NSString *)serviceName; +-(id)pkt_getKeychainValueForKey:(NSString *)key serviceName:(NSString *)serviceName; + +@end + +@interface PocketAPILogin (Private) +-(void)_setRequestToken:(NSString *)requestToken; +-(void)_setReverseAuth:(BOOL)isReverseAuth; +@end + +#if NS_BLOCKS_AVAILABLE +@interface PocketAPIBlockDelegate : NSObject { + PocketAPILoginHandler loginHandler; + PocketAPISaveHandler saveHandler; + PocketAPIResponseHandler responseHandler; +} + ++(id)delegateWithLoginHandler:(PocketAPILoginHandler)handler; ++(id)delegateWithSaveHandler: (PocketAPISaveHandler )handler; ++(id)delegateWithResponseHandler: (PocketAPIResponseHandler )handler; + +@property (nonatomic, copy) PocketAPILoginHandler loginHandler; +@property (nonatomic, copy) PocketAPISaveHandler saveHandler; +@property (nonatomic, copy) PocketAPIResponseHandler responseHandler; +@end +#endif + +#pragma mark Implementation + +@implementation PocketAPI + +@synthesize consumerKey, URLScheme, operationQueue; + +#pragma mark Public API + +static PocketAPI *sSharedAPI = nil; + ++(PocketAPI *)sharedAPI{ + @synchronized(self) + { + if (sSharedAPI == NULL){ + sSharedAPI = [self alloc]; + [sSharedAPI init]; + } + } + + return(sSharedAPI); +} + ++(NSString *)pocketAppURLScheme{ + return @"pocket-oauth-v1"; +} + ++(BOOL)hasPocketAppInstalled{ +#if TARGET_OS_IPHONE + return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:[[self pocketAppURLScheme] stringByAppendingString:@":"]]]; +#else + return NO; +#endif +} + ++(NSString *)pkt_hashForConsumerKey:(NSString *)consumerKey accessToken:(NSString *)accessToken{ + NSString *string = [NSString stringWithFormat:@"%@-%@",consumerKey, accessToken]; + NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding]; + + uint8_t digest[CC_SHA1_DIGEST_LENGTH]; + CC_SHA1(stringData.bytes, (unsigned int)(stringData.length), digest); + + NSMutableString *hashString = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH]; + for(int i=0; i < CC_SHA1_DIGEST_LENGTH; i++){ + [hashString appendFormat:@"%02x",digest[i]]; + } + return hashString; +} + +-(id)init{ + if(self = [super init]){ + operationQueue = [[NSOperationQueue alloc] init]; + + // set the initial API key to the one from the singleton + if(sSharedAPI != self){ + self.consumerKey = [sSharedAPI consumerKey]; + } + +#if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self + andSelector:@selector(receivedURL:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID:kAEGetURL]; +#endif + + // register for lifecycle notifications +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; +#endif + } + return self; +} + +#if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR +- (void) receivedURL: (NSAppleEventDescriptor*)event withReplyEvent: (NSAppleEventDescriptor*)replyEvent +{ + NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + if(urlString){ + [self handleOpenURL:[NSURL URLWithString:urlString]]; + } +} +#endif + +-(void)setConsumerKey:(NSString *)aConsumerKey{ + [aConsumerKey retain]; + [consumerKey release]; + consumerKey = aConsumerKey; + + if(!URLScheme && consumerKey){ + [self setURLScheme:[self URLScheme]]; + } + + // if on a Mac, and this user was logged in with a pre-1.0.2 SDK, attempt to migrate the keychain values if the token/digest pair matches +#if !DEBUG && TARGET_OS_MAC && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + if(![self pkt_getToken]){ // if we don't already have a token + NSString *existingHash = [self pkt_getKeychainValueForKey:@"tokenDigest" serviceName:@"PocketAPI"]; + if(existingHash){ // ...but we do have an unmigrated token + NSString *token = [self pkt_getKeychainValueForKey:@"token" serviceName:@"PocketAPI"]; + NSString *currentHash = [[self class] pkt_hashForConsumerKey:self.consumerKey accessToken:token]; + if([existingHash isEqualToString:currentHash]){ // ...and the hash matches our consumer key + // migrate the token to the new location in the keychain + NSString *username = [self pkt_getKeychainValueForKey:@"username" serviceName:@"PocketAPI"]; + + [self pkt_setKeychainValue:username forKey:@"username"]; + [self pkt_setKeychainValue:nil forKey:@"username" serviceName:@"PocketAPI"]; + + [self pkt_setKeychainValue:token forKey:@"token"]; + [self pkt_setKeychainValue:nil forKey:@"token" serviceName:@"PocketAPI"]; + + [self pkt_setKeychainValue:currentHash forKey:@"tokenDigest"]; + [self pkt_setKeychainValue:nil forKey:@"tokenDigest" serviceName:@"PocketAPI"]; + } + } + } +#endif + + // ensure the access token stored matches the consumer key that generated it + if(self.isLoggedIn){ + NSString *existingHash = [self pkt_getKeychainValueForKey:@"tokenDigest"]; + NSString *currentHash = [[self class] pkt_hashForConsumerKey:self.consumerKey accessToken:[self pkt_getToken]]; + + if(![existingHash isEqualToString:currentHash]){ + NSLog(@"*** ERROR: The access token that exists does not match the consumer key. The user has been logged out."); + [self logout]; + } + } +} + +-(NSString *)URLScheme{ + if(!URLScheme){ + return [NSString stringWithFormat:@"pocketapp%lu", (unsigned long)[self appID]]; + }else{ + return URLScheme; + } +} + +-(void)setURLScheme:(NSString *)aURLScheme{ + [aURLScheme retain]; + [URLScheme release]; + URLScheme = aURLScheme; + +#if DEBUG + // check to make sure + BOOL foundURLScheme = NO; + NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; + NSArray *urlSchemeLists = [infoDict objectForKey:@"CFBundleURLTypes"]; + for(NSDictionary *urlSchemeList in urlSchemeLists){ + NSArray *urlSchemes = [urlSchemeList objectForKey:@"CFBundleURLSchemes"]; + if([urlSchemes containsObject:URLScheme]){ + foundURLScheme = YES; + break; + } + } + + if(!foundURLScheme){ + NSLog(@"** WARNING: You haven't added a URL scheme for the Pocket SDK. This will prevent login from working. See the SDK readme."); + NSLog(@"** The URL scheme you need to register is: %@",URLScheme); + } +#endif +} + +- (void) setOperationQueue:(NSOperationQueue *)anOperationQueue { + if (consumerKey) { + NSLog(@"ERROR: PocketAPI operationQueue is being set after the consumer key was obtained.\n\tThis is probably a sever error."); + } + [operationQueue release]; + operationQueue = [anOperationQueue retain]; +} + +-(void)applicationDidEnterBackground:(NSNotification *)notification{ + [self pkt_saveCurrentLoginToDefaults]; +} + +-(void)dealloc{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [operationQueue waitUntilAllOperationsAreFinished]; + [operationQueue release], operationQueue = nil; + + [consumerKey release], consumerKey = nil; + [URLScheme release], URLScheme = nil; + [userAgent release], userAgent = nil; + + [super dealloc]; +} + +-(BOOL)handleOpenURL:(NSURL *)url{ + if([[url scheme] isEqualToString:self.URLScheme]){ + NSDictionary *urlQuery = [NSDictionary pkt_dictionaryByParsingURLEncodedFormString:[url query]]; + + PocketAPILogin *login = currentLogin; + if([[url path] isEqualToString:@"/reverse"] && [urlQuery objectForKey:@"code"]){ + BOOL allowReverseLogin = YES; +#if TARGET_OS_IPHONE + id appDelegate = (id)[[UIApplication sharedApplication] delegate]; +#else + id appDelegate = (id)[[NSApplication sharedApplication] delegate]; +#endif + + if(appDelegate && [appDelegate respondsToSelector:@selector(shouldAllowPocketReverseAuth)]){ + if(![appDelegate shouldAllowPocketReverseAuth]){ + allowReverseLogin = NO; + } + } + + if(allowReverseLogin){ + NSString *requestToken = [urlQuery objectForKey:@"code"]; + login = [[[PocketAPILogin alloc] initWithAPI:self delegate:nil] autorelease]; + [login _setRequestToken:requestToken]; + [login _setReverseAuth:YES]; + } + } + + if(!login){ + login = [self pkt_loadCurrentLoginFromDefaults]; + } + + currentLogin = [login retain]; + + [currentLogin convertRequestTokenToAccessToken]; + return YES; + } + + return NO; +} + +-(NSUInteger)appID{ + NSUInteger appID = NSNotFound; + if(self.consumerKey){ + NSArray *keyPieces = [self.consumerKey componentsSeparatedByString:@"-"]; + if(keyPieces && keyPieces.count > 0){ + NSString *appIDPiece = [keyPieces objectAtIndex:0]; + if(appIDPiece && appIDPiece.length > 0){ + appID = [appIDPiece integerValue]; + } + } + } + return appID; +} + +-(BOOL)isLoggedIn{ + NSString *username = [self username]; + NSString *token = [self pkt_getToken]; + return (username && token && username.length > 0 && token.length > 0); +} + +-(void)loginWithDelegate:(id)delegate{ + [currentLogin autorelease]; + currentLogin = [[PocketAPILogin alloc] initWithAPI:self delegate:delegate]; + [currentLogin fetchRequestToken]; +} + +-(void)saveURL:(NSURL *)url delegate:(id)delegate{ + [operationQueue addOperation:[self saveOperationWithURL:url delegate:delegate]]; +} + +-(void)saveURL:(NSURL *)url withTitle:(NSString *)title delegate:(id)delegate{ + [operationQueue addOperation:[self saveOperationWithURL:url title:title delegate:delegate]]; +} + +-(void)saveURL:(NSURL *)url withTitle:(NSString *)title tweetID:(NSString *)tweetID delegate:(id)delegate{ + [operationQueue addOperation:[self saveOperationWithURL:url title:title tweetID:tweetID delegate:delegate]]; +} + +-(void)callAPIMethod:(NSString *)APIMethod withHTTPMethod:(PocketAPIHTTPMethod)HTTPMethod arguments:(NSDictionary *)arguments delegate:(id)delegate{ + [operationQueue addOperation:[self methodOperationWithAPIMethod:APIMethod forHTTPMethod:HTTPMethod arguments:arguments delegate:delegate]]; +} + +-(NSOperation *)saveOperationWithURL:(NSURL *)url title:(NSString *)title tweetID:(NSString *)tweetID delegate:(id)delegate{ + if(!url || !url.absoluteString) return nil; + + NSNumber *timestamp = [NSNumber numberWithInteger:(NSInteger)([[NSDate date] timeIntervalSince1970])]; + + NSMutableDictionary *arguments = [NSMutableDictionary dictionary]; + [arguments setObject:timestamp forKey:@"time"]; + [arguments setObject:url.absoluteString forKey:@"url"]; + + if(title){ + [arguments setObject:title forKey:@"title"]; + } + + if(tweetID && ![tweetID isEqualToString:@""] && ![tweetID isEqualToString:@"0"]){ + [arguments setObject:tweetID forKey:@"ref_id"]; + } + + return [self methodOperationWithAPIMethod:@"add" + forHTTPMethod:PocketAPIHTTPMethodPOST + arguments:[[arguments copy] autorelease] + delegate:delegate]; +} + +-(NSOperation *)saveOperationWithURL:(NSURL *)url title:(NSString *)title delegate:(id)delegate{ + return [self saveOperationWithURL:url title:title tweetID:nil delegate:delegate]; +} + +-(NSOperation *)saveOperationWithURL:(NSURL *)url delegate:(id)delegate{ + return [self saveOperationWithURL:url title:nil tweetID:nil delegate:delegate]; +} + +-(NSOperation *)methodOperationWithAPIMethod:(NSString *)APIMethod forHTTPMethod:(PocketAPIHTTPMethod)HTTPMethod arguments:(NSDictionary *)arguments delegate:(id)delegate{ + PocketAPIOperation *operation = [[[PocketAPIOperation alloc] init] autorelease]; + operation.API = self; + operation.delegate = delegate; + operation.APIMethod = APIMethod; + operation.HTTPMethod = HTTPMethod; + operation.arguments = arguments; + return operation; +} + +#if NS_BLOCKS_AVAILABLE + +-(void)loginWithHandler:(PocketAPILoginHandler)handler{ + [self loginWithDelegate:[PocketAPIBlockDelegate delegateWithLoginHandler:handler]]; +} + +-(void)saveURL:(NSURL *)url handler:(PocketAPISaveHandler)handler{ + [self saveURL:url delegate:[PocketAPIBlockDelegate delegateWithSaveHandler:handler]]; +} + +-(void)saveURL:(NSURL *)url withTitle:(NSString *)title handler:(PocketAPISaveHandler)handler{ + [self saveURL:url withTitle:title delegate:[PocketAPIBlockDelegate delegateWithSaveHandler:handler]]; +} + +-(void)saveURL:(NSURL *)url withTitle:(NSString *)title tweetID:(NSString *)tweetID handler:(PocketAPISaveHandler)handler{ + [self saveURL:url withTitle:title tweetID:tweetID delegate:[PocketAPIBlockDelegate delegateWithSaveHandler:handler]]; +} + +-(void)callAPIMethod:(NSString *)APIMethod withHTTPMethod:(PocketAPIHTTPMethod)HTTPMethod arguments:(NSDictionary *)arguments handler:(PocketAPIResponseHandler)handler{ + [self callAPIMethod:APIMethod withHTTPMethod:HTTPMethod arguments:arguments delegate:[PocketAPIBlockDelegate delegateWithResponseHandler:handler]]; +} + +// operation API + +-(NSOperation *)saveOperationWithURL:(NSURL *)url handler:(PocketAPISaveHandler)handler{ + return [self saveOperationWithURL:url delegate:[PocketAPIBlockDelegate delegateWithSaveHandler:handler]]; +} + +-(NSOperation *)saveOperationWithURL:(NSURL *)url title:(NSString *)title handler:(PocketAPISaveHandler)handler{ + return [self saveOperationWithURL:url title:title delegate:[PocketAPIBlockDelegate delegateWithSaveHandler:handler]]; +} + +-(NSOperation *)saveOperationWithURL:(NSURL *)url title:(NSString *)title tweetID:(NSString *)tweetID handler:(PocketAPISaveHandler)handler{ + return [self saveOperationWithURL:url title:title tweetID:tweetID delegate:[PocketAPIBlockDelegate delegateWithSaveHandler:handler]]; +} + +-(NSOperation *)methodOperationWithAPIMethod:(NSString *)APIMethod forHTTPMethod:(PocketAPIHTTPMethod)httpMethod arguments:(NSDictionary *)arguments handler:(PocketAPIResponseHandler)handler{ + return [self methodOperationWithAPIMethod:APIMethod forHTTPMethod:httpMethod arguments:arguments delegate:[PocketAPIBlockDelegate delegateWithResponseHandler:handler]]; +} + +#endif + +#pragma mark Account Info + +-(NSString *)username{ + return [self pkt_getKeychainValueForKey:@"username"]; +} + +-(NSString *)pkt_getToken{ + return [self pkt_getKeychainValueForKey:@"token"]; +} + +-(void)pkt_loggedInWithUsername:(NSString *)username token:(NSString *)token{ + [self willChangeValueForKey:@"username"]; + [self willChangeValueForKey:@"isLoggedIn"]; + + [self pkt_setKeychainValue:username forKey:@"username"]; + [self pkt_setKeychainValue:token forKey:@"token"]; + [self pkt_setKeychainValue:[[self class] pkt_hashForConsumerKey:self.consumerKey accessToken:token] forKey:@"tokenDigest"]; + + [self didChangeValueForKey:@"isLoggedIn"]; + [self didChangeValueForKey:@"username"]; +} + +-(void)logout{ + [self willChangeValueForKey:@"username"]; + [self willChangeValueForKey:@"isLoggedIn"]; + + [self pkt_setKeychainValue:nil forKey:@"username"]; + [self pkt_setKeychainValue:nil forKey:@"token"]; + [self pkt_setKeychainValue:nil forKey:@"tokenDigest"]; + + [self didChangeValueForKey:@"isLoggedIn"]; + [self didChangeValueForKey:@"username"]; +} + +-(PocketAPILogin *)pkt_loadCurrentLoginFromDefaults{ + NSUserDefaults *defaults = [[[NSUserDefaults alloc] init] autorelease]; + + PocketAPILogin *login = nil; + if(!login){ + NSData *data = [defaults dataForKey:kPocketAPICurrentLoginKey]; + if (data) { + @try { + login = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + } + @catch (NSException *exception) { + NSLog(@"Encountered an exception reading Pocket login: %@", [exception description]); + } + } + } + + if(login){ + [defaults removeObjectForKey:kPocketAPICurrentLoginKey]; + [defaults synchronize]; + } + + return login; +} + +-(void)pkt_saveCurrentLoginToDefaults{ + if(currentLogin){ + NSData *loginData = [NSKeyedArchiver archivedDataWithRootObject:currentLogin]; + + NSUserDefaults *defaults = [[NSUserDefaults alloc] init]; + [defaults setObject:loginData forKey:kPocketAPICurrentLoginKey]; + [defaults synchronize]; + [defaults release]; + } +} + +// NOTE: This API will FAIL for you by default. It is only enabled for certain consumer keys, +// and it will only be available for a short time. If you use it, do NOT store the user's +// password permanently. If you require access to this API, contact us at api@getpocket.com. +// +// Be prepared for this API to return errors at any time, even after you are in production. + +-(void)pkt_migrateAccountToAccessTokenWithUsername:(NSString *)username password:(NSString *)password delegate:(id)delegate{ + PocketAPIOperation *operation = [[PocketAPIOperation alloc] init]; + operation.API = self; + operation.delegate = delegate; + operation.domain = PocketAPIDomainAuth; + operation.HTTPMethod = PocketAPIHTTPMethodPOST; + operation.APIMethod = @"authorize"; + + NSString *locale = [[NSLocale preferredLanguages] objectAtIndex:0]; + NSString *country = [[NSLocale currentLocale] objectForKey: NSLocaleCountryCode]; + int timeZone = round([[NSTimeZone systemTimeZone] secondsFromGMT] / 60); + + operation.arguments = [NSDictionary dictionaryWithObjectsAndKeys: + username, @"username", + password, @"password", + @"credentials", @"grant_type", + locale, @"locale", + country, @"country", + [NSString stringWithFormat:@"%i", timeZone], @"timezone", + nil]; + + [operationQueue addOperation:operation]; + [operation release]; +} + +#if NS_BLOCKS_AVAILABLE +-(void)pkt_migrateAccountToAccessTokenWithUsername:(NSString *)username password:(NSString *)password handler:(PocketAPILoginHandler)handler{ + [self pkt_migrateAccountToAccessTokenWithUsername:username password:password delegate:[PocketAPIBlockDelegate delegateWithLoginHandler:handler]]; +} +#endif + +-(NSDictionary *)pkt_actionDictionaryWithName:(NSString *)name parameters:(NSDictionary *)params{ + if(!name) return nil; + + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:params]; + [dict setObject:name forKey:@"action"]; + [dict setObject:[NSNumber numberWithInteger:(NSInteger)([[NSDate date] timeIntervalSince1970])] forKey:@"time"]; + + return dict; +} + +#pragma mark - +#pragma mark User Agent (uses UIDevice+Hardware from https://github.com/erica/uidevice-extension) + +-(NSString *)pkt_userAgent{ + if(!userAgent){ + NSDictionary *bundleInfo = [[NSBundle mainBundle] infoDictionary]; + + NSString *productName = @"PocketSDK:" POCKET_SDK_VERSION; + NSString *appName = [bundleInfo objectForKey:@"CFBundleDisplayName"]; + if(!appName){ + appName = [bundleInfo objectForKey:(NSString *)kCFBundleNameKey]; + } + NSString *appVersion = [bundleInfo objectForKey:@"CFBundleVersion"]; + NSString *deviceMfg = @"Apple"; + NSString *storeName = @"App Store"; + NSString *deviceName = [self pkt_deviceName]; + NSString *osVersion = [self pkt_deviceOSVersion]; + + NSString *osType = nil; + NSString *deviceType = nil; + +#if TARGET_OS_IPHONE + osType = [[UIDevice currentDevice] systemName]; + deviceType = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? @"Tablet" : @"Mobile"; +#else + osType = @"OS X"; + deviceType = @"Computer"; + + NSString *receiptPath = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/_MASReceipt/receipt"] stringByStandardizingPath]; + if(![[NSFileManager defaultManager] fileExistsAtPath:receiptPath]){ + storeName = @"Vendor"; + } +#endif + +#define PKTAtLeastEmptyString(__str) ((__str) == nil ? @"" : (__str)) + userAgent = [[[NSArray arrayWithObjects: + PKTAtLeastEmptyString(productName), + PKTAtLeastEmptyString(appName), + PKTAtLeastEmptyString(appVersion), + PKTAtLeastEmptyString(osType), + PKTAtLeastEmptyString(osVersion), + PKTAtLeastEmptyString(deviceMfg), + PKTAtLeastEmptyString(deviceName), + PKTAtLeastEmptyString(deviceType), + PKTAtLeastEmptyString(storeName), + nil] componentsJoinedByString:@";"] retain]; +#undef PKTAtLeastEmptyString + } + return userAgent; +} + +-(NSString *)pkt_deviceName{ +#if TARGET_OS_IPHONE + size_t size; + const char *typeSpecifier = "hw.machine"; + sysctlbyname(typeSpecifier, NULL, &size, NULL, 0); + + char *answer = malloc(size); + sysctlbyname(typeSpecifier, answer, &size, NULL, 0); + + NSString *platform = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding]; + free(answer); + + if ([platform isEqualToString:@"iFPGA"]) return @"iFPGA"; + + // iPhone + if ([platform isEqualToString:@"iPhone1,1"]) return @"iPhone 1G"; + if ([platform isEqualToString:@"iPhone1,2"]) return @"iPhone 3G"; + if ([platform hasPrefix:@"iPhone2"]) return @"iPhone 3GS"; + if ([platform hasPrefix:@"iPhone3"]) return @"iPhone 4"; + if ([platform hasPrefix:@"iPhone4"]) return @"iPhone 4S"; + + // iPod + if ([platform hasPrefix:@"iPod1"]) return @"iPod touch 1G"; + if ([platform hasPrefix:@"iPod2"]) return @"iPod touch 2G"; + if ([platform hasPrefix:@"iPod3"]) return @"iPod touch 3G"; + if ([platform hasPrefix:@"iPod4"]) return @"iPod touch 4G"; + + // iPad + if ([platform hasPrefix:@"iPad1"]) return @"iPad 1G"; + if ([platform hasPrefix:@"iPad2"]) return @"iPad 2G"; + if ([platform hasPrefix:@"iPad3"]) return @"iPad 3G"; + + // Apple TV + if ([platform hasPrefix:@"AppleTV2"]) return @"Apple TV 2G"; + + if ([platform hasPrefix:@"iPhone"]) return @"Unknown iPhone"; + if ([platform hasPrefix:@"iPod"]) return @"Unknown iPod touch"; + if ([platform hasPrefix:@"iPad"]) return @"Unknown iPad"; + + // Simulator thanks Jordan Breeding + if ([platform hasSuffix:@"86"] || [platform isEqual:@"x86_64"]) return UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? @"iPad Simulator" : @"iPhone Simulator"; + + return @"Unknown iOS Device"; +#else + NSString *modelIdentifier = @""; + + int nameSuccess = 0; + const int SUCCEEDED = 0; + + size_t size = 0; + nameSuccess = sysctlbyname("hw.machine", NULL, &size, NULL, 0); + if (nameSuccess != SUCCEEDED || size == 0) + return modelIdentifier; + + char *machine = malloc(size); + nameSuccess = sysctlbyname("hw.machine", machine, &size, NULL, 0); + if (nameSuccess == SUCCEEDED) { + modelIdentifier = [NSString stringWithUTF8String:machine]; + } + free(machine); + + return modelIdentifier; +#endif +} + +-(NSString *)pkt_deviceOSVersion{ +#if TARGET_OS_IPHONE + return [[UIDevice currentDevice] systemVersion]; +#else + SInt32 versionMajor = 0; + SInt32 versionMinor = 0; + SInt32 versionBugFix = 0; + Gestalt( gestaltSystemVersionMajor, &versionMajor ); + Gestalt( gestaltSystemVersionMinor, &versionMinor ); + Gestalt( gestaltSystemVersionBugFix, &versionBugFix ); + return [NSString stringWithFormat:@"%d.%d.%d", versionMajor, versionMinor, versionBugFix]; +#endif +} + +@end + +#pragma mark Keychain Credentials + +#import +#import "SFHFKeychainUtils.h" + +@implementation PocketAPI (Credentials) + +-(void)pkt_setKeychainValue:(id)value forKey:(NSString *)key{ + [self pkt_setKeychainValue:value forKey:key serviceName:PocketGlobalKeychainServiceName]; +} + +-(id)pkt_getKeychainValueForKey:(NSString *)key{ + return [self pkt_getKeychainValueForKey:key serviceName:PocketGlobalKeychainServiceName]; +} + +-(void)pkt_setKeychainValue:(id)value forKey:(NSString *)key serviceName:(NSString *)serviceName{ + if(value){ +#if TARGET_IPHONE_SIMULATOR || (DEBUG && !TARGET_OS_IPHONE && TARGET_OS_MAC) + [[NSUserDefaults standardUserDefaults] setObject:value forKey:[NSString stringWithFormat:@"%@.%@", serviceName, key]]; +#else + [SFHFKeychainUtils storeUsername:key andPassword:value forServiceName:serviceName updateExisting:YES error:nil]; +#endif + }else{ +#if TARGET_IPHONE_SIMULATOR || (DEBUG && !TARGET_OS_IPHONE && TARGET_OS_MAC) + [[NSUserDefaults standardUserDefaults] removeObjectForKey:[NSString stringWithFormat:@"%@.%@", serviceName, key]]; +#else + [SFHFKeychainUtils deleteItemForUsername:key andServiceName:serviceName error:nil]; +#endif + } +} + +-(id)pkt_getKeychainValueForKey:(NSString *)key serviceName:(NSString *)serviceName{ +#if TARGET_IPHONE_SIMULATOR || (DEBUG && !TARGET_OS_IPHONE && TARGET_OS_MAC) + return [[NSUserDefaults standardUserDefaults] objectForKey:[NSString stringWithFormat:@"%@.%@", serviceName, key]]; +#else + return [SFHFKeychainUtils getPasswordForUsername:key andServiceName:serviceName error:nil]; +#endif +} + +@end + +#if NS_BLOCKS_AVAILABLE +@implementation PocketAPIBlockDelegate + +@synthesize loginHandler, saveHandler, responseHandler; + +-(void)pocketAPILoggedIn:(PocketAPI *)api{ + if(self.loginHandler){ + self.loginHandler(api, nil); + } +} + +-(void)pocketAPI:(PocketAPI *)api hadLoginError:(NSError *)error{ + if(self.loginHandler){ + self.loginHandler(api, error); + } +} + +-(void)pocketAPI:(PocketAPI *)api savedURL:(NSURL *)url{ + if(self.saveHandler){ + self.saveHandler(api, url, nil); + } +} + +-(void)pocketAPI:(PocketAPI *)api failedToSaveURL:(NSURL *)url error:(NSError *)error{ + if(self.saveHandler){ + self.saveHandler(api, url, error); + } +} + +-(void)pocketAPI:(PocketAPI *)api receivedResponse:(NSDictionary *)response forAPIMethod:(NSString *)APIMethod error:(NSError *)error{ + if(self.responseHandler){ + self.responseHandler(api, APIMethod, response, error); + } +} + ++(id)delegateWithLoginHandler:(PocketAPILoginHandler)handler{ + PocketAPIBlockDelegate *delegate = [[[self alloc] init] autorelease]; + delegate.loginHandler = [[handler copy] autorelease]; + return delegate; +} + ++(id)delegateWithSaveHandler: (PocketAPISaveHandler)handler{ + PocketAPIBlockDelegate *delegate = [[[self alloc] init] autorelease]; + delegate.saveHandler = [[handler copy] autorelease]; + return delegate; +} + ++(id)delegateWithResponseHandler:(PocketAPIResponseHandler)handler{ + PocketAPIBlockDelegate *delegate = [[[self alloc] init] autorelease]; + delegate.responseHandler = [[handler copy] autorelease]; + return delegate; +} + +-(void)dealloc{ + [loginHandler release], loginHandler = nil; + [saveHandler release], saveHandler = nil; + [responseHandler release], responseHandler = nil; + + [super dealloc]; +} + +@end +#endif + +NSString *PocketAPITweetID(unsigned long long tweetID){ + return [NSString stringWithFormat:@"%llu", tweetID]; +} \ No newline at end of file diff --git a/SDK/PocketAPILogin.h b/SDK/PocketAPILogin.h new file mode 100644 index 0000000..4a45a70 --- /dev/null +++ b/SDK/PocketAPILogin.h @@ -0,0 +1,40 @@ +// +// PocketAPILogin.h +// iOS Test App +// +// Created by Steve Streza on 7/23/12. +// Copyright (c) 2012 Read It Later, Inc. All rights reserved. +// + +#import +#import "PocketAPI.h" + +@interface PocketAPILogin : NSObject { + PocketAPI *API; + + NSString *uuid; // unique ID for the login process + + NSString *requestToken; + NSString *accessToken; + + NSOperationQueue *operationQueue; + + id delegate; + + BOOL didStart; + BOOL didFinish; + + BOOL reverseAuth; +} + +-(id)initWithAPI:(PocketAPI *)api delegate:(id)delegate; + +@property (nonatomic, readonly, retain) PocketAPI *API; +@property (nonatomic, readonly, retain) NSString *uuid; +@property (nonatomic, readonly, retain) NSString *requestToken; +@property (nonatomic, readonly, retain) NSString *accessToken; + +-(void)fetchRequestToken; +-(void)convertRequestTokenToAccessToken; + +@end diff --git a/SDK/PocketAPILogin.m b/SDK/PocketAPILogin.m new file mode 100644 index 0000000..ce90426 --- /dev/null +++ b/SDK/PocketAPILogin.m @@ -0,0 +1,218 @@ +// +// PocketAPILogin.m +// iOS Test App +// +// Created by Steve Streza on 7/23/12. +// Copyright (c) 2012 Read It Later, Inc. All rights reserved. +// + +#import "PocketAPILogin.h" +#import "PocketAPIOperation.h" + +const NSString *PocketAPIErrorDomain = @"PocketAPIErrorDomain"; + +const NSString *PocketAPILoginStartedNotification = @"PocketAPILoginStartedNotification"; +const NSString *PocketAPILoginFinishedNotification = @"PocketAPILoginFinishedNotification"; +const NSString *PocketAPILoginFailedNotification = @"PocketAPILoginFailedNotification"; + +@interface PocketAPIOperation (Private) + +-(void)pkt_setBaseURLPath:(NSString *)baseURLPath; + +@end + +@interface PocketAPILogin (Private) + +-(void)loginDidStart; +-(void)loginDidFinish:(BOOL)success; + +-(void)_setReverseAuth:(BOOL)isReverseAuth; + +@end + +@implementation PocketAPILogin + +@synthesize API, uuid, requestToken, accessToken; + +-(void)encodeWithCoder:(NSCoder *)aCoder{ + [aCoder encodeObject:self.requestToken forKey:@"requestToken"]; + [aCoder encodeObject:self.accessToken forKey:@"accessToken" ]; + [aCoder encodeObject:self.uuid forKey:@"uuid" ]; +} + +-(id)initWithCoder:(NSCoder *)aDecoder{ + if(self = [self init]){ + requestToken = [[aDecoder decodeObjectForKey:@"requestToken"] retain]; + accessToken = [[aDecoder decodeObjectForKey:@"accessToken" ] retain]; + uuid = [[aDecoder decodeObjectForKey:@"uuid" ] retain]; + } + return self; +} + +-(void)_setRequestToken:(NSString *)aRequestToken{ + [self willChangeValueForKey:@"requestToken"]; + [requestToken autorelease]; + requestToken = [aRequestToken copy]; + [self didChangeValueForKey:@"requestToken"]; +} + +-(void)_setReverseAuth:(BOOL)isReverseAuth{ + reverseAuth = isReverseAuth; +} + +-(void)openURL:(NSURL *)url{ +#if TARGET_OS_IPHONE + [[UIApplication sharedApplication] openURL:url]; +#else + [[NSWorkspace sharedWorkspace] openURL:url]; +#endif +} + +-(void)dealloc{ + [operationQueue waitUntilAllOperationsAreFinished]; + [operationQueue release], operationQueue = nil; + + [requestToken release], requestToken = nil; + [accessToken release], accessToken = nil; + [API release], API = nil; + [delegate release], delegate = nil; + + [super dealloc]; +} + +-(id)init{ + if(self = [super init]){ + operationQueue = [[NSOperationQueue alloc] init]; + API = [[PocketAPI sharedAPI] retain]; + + CFUUIDRef uuidRef = CFUUIDCreate(NULL); + uuid = (NSString *)CFUUIDCreateString(NULL, uuidRef); + CFRelease(uuidRef); + } + return self; +} + +-(id)initWithAPI:(PocketAPI *)newAPI delegate:(id)aDelegate{ + if(self = [self init]){ + [newAPI retain]; + [API release]; + API = newAPI; + + delegate = [aDelegate retain]; + } + return self; +} + +-(NSURL *)redirectURL{ + return [NSURL URLWithString:[NSString stringWithFormat:@"%@:authorizationFinished", [self.API URLScheme]]]; +} + +-(void)fetchRequestToken{ + [self loginDidStart]; + + PocketAPIOperation *operation = [[PocketAPIOperation alloc] init]; + operation.API = API; + operation.delegate = self; + operation.domain = PocketAPIDomainAuth; + operation.HTTPMethod = PocketAPIHTTPMethodPOST; + operation.APIMethod = @"request"; + + NSString *redirectURLPath = [[self redirectURL] absoluteString]; + + operation.arguments = [NSDictionary dictionaryWithObjectsAndKeys: + self.uuid, @"state", + redirectURLPath, @"redirect_uri", + nil]; + [operationQueue addOperation:operation]; + [operation release]; +} + +-(void)convertRequestTokenToAccessToken{ + PocketAPIOperation *operation = [[PocketAPIOperation alloc] init]; + operation.API = API; + operation.delegate = self; + operation.domain = PocketAPIDomainAuth; + operation.HTTPMethod = PocketAPIHTTPMethodPOST; + operation.APIMethod = @"authorize"; + + NSString *locale = [[NSLocale preferredLanguages] objectAtIndex:0]; + NSString *country = [[NSLocale currentLocale] objectForKey: NSLocaleCountryCode]; + int timeZone = round([[NSTimeZone systemTimeZone] secondsFromGMT] / 60); + + operation.arguments = [NSDictionary dictionaryWithObjectsAndKeys: + self.requestToken, @"code", + locale, @"locale", + country, @"country", + [NSString stringWithFormat:@"%i", timeZone], @"timezone", + nil]; + [operationQueue addOperation:operation]; + [operation release]; +} + +#pragma mark Pocket API Delegate + +-(void)pocketAPI:(PocketAPI *)api receivedRequestToken:(NSString *)aRequestToken{ + [self _setRequestToken:aRequestToken]; + + NSURL *authorizeURL = nil; + NSString *encodedRedirectURLString = [PocketAPIOperation encodeForURL:[[self redirectURL] absoluteString]]; + if([PocketAPI hasPocketAppInstalled]){ + authorizeURL = [NSURL URLWithString:[NSString stringWithFormat:@"pocket-oauth-v1:///authorize?request_token=%@&redirect_uri=%@",requestToken, encodedRedirectURLString]]; + }else{ + authorizeURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://getpocket.com/auth/authorize?request_token=%@&redirect_uri=%@",requestToken, encodedRedirectURLString]]; + } + + [self openURL:authorizeURL]; +} + +-(void)pocketAPILoggedIn:(PocketAPI *)api{ + if(delegate && [delegate respondsToSelector:@selector(pocketAPILoggedIn:)]){ + [delegate pocketAPILoggedIn:self.API]; + } + + [self loginDidFinish:YES]; + + [delegate release], delegate = nil; +} + +-(void)pocketAPI:(PocketAPI *)api hadLoginError:(NSError *)error{ + if(delegate && [delegate respondsToSelector:@selector(pocketAPI:hadLoginError:)]){ + [delegate pocketAPI:api hadLoginError:error]; + } + + [self loginDidFinish:NO]; + + [delegate release], delegate = nil; +} + +-(void)loginDidStart{ + if(!didStart){ + didStart = YES; + + if(delegate && [delegate respondsToSelector:@selector(pocketAPIDidStartLogin:)]){ + [delegate pocketAPIDidStartLogin:self.API]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:(NSString *)PocketAPILoginStartedNotification object:nil]; + } +} + +-(void)loginDidFinish:(BOOL)success{ + if(!didFinish){ + didFinish = YES; + + if(delegate && [delegate respondsToSelector:@selector(pocketAPIDidFinishLogin:)]){ + [delegate pocketAPIDidFinishLogin:self.API]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:(NSString *)PocketAPILoginFinishedNotification object:nil]; + + if(reverseAuth && [PocketAPI hasPocketAppInstalled]){ + NSString *responseURLString = [NSString stringWithFormat:@"%@://reverse/%@/%i",[PocketAPI pocketAppURLScheme], (success ? @"success" : @"failed"), (int)[self.API appID]]; + NSURL *responseURL = [NSURL URLWithString:responseURLString]; + [self openURL:responseURL]; + } + } +} + +@end diff --git a/SDK/PocketAPIOperation.h b/SDK/PocketAPIOperation.h new file mode 100644 index 0000000..0f6e2e5 --- /dev/null +++ b/SDK/PocketAPIOperation.h @@ -0,0 +1,74 @@ +// +// PocketAPIOperation.h +// PocketSDK +// +// Created by Steve Streza on 5/29/12. +// Copyright (c) 2012 Read It Later, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import +#import "PocketAPI.h" +#import "PocketAPITypes.h" + +@interface NSDictionary (PocketAdditions) + +-(NSString *)pkt_URLEncodedFormString; ++(NSDictionary *)pkt_dictionaryByParsingURLEncodedFormString:(NSString *)formString; + +@end + +@interface PocketAPIOperation : NSOperation { + PocketAPI *API; + id delegate; + + PocketAPIDomain domain; + PocketAPIHTTPMethod HTTPMethod; + NSString *APIMethod; + NSDictionary *arguments; + + NSURLConnection *connection; + NSHTTPURLResponse *response; + NSMutableData *data; + NSError *error; + + NSString *baseURLPath; + + BOOL finishedLoading; + BOOL attemptedRelogin; +} + +@property (nonatomic, retain) PocketAPI *API; +@property (nonatomic, retain) id delegate; // we break convention here to ensure the delegate exists for operation lifetime, release on complete + +@property (nonatomic, readonly, retain) NSString *baseURLPath; +@property (nonatomic, assign) PocketAPIDomain domain; +@property (nonatomic, assign) PocketAPIHTTPMethod HTTPMethod; +@property (nonatomic, retain) NSString *APIMethod; +@property (nonatomic, retain) NSDictionary *arguments; + +@property (nonatomic, readonly, retain) NSURLConnection *connection; +@property (nonatomic, readonly, retain) NSHTTPURLResponse *response; +@property (nonatomic, readonly, retain) NSMutableData *data; + +@property (nonatomic, readonly, retain) NSError *error; + ++(NSString *)encodeForURL:(NSString *)urlStr; + +@end + +NSString *PocketAPINameForHTTPMethod(PocketAPIHTTPMethod method); \ No newline at end of file diff --git a/SDK/PocketAPIOperation.m b/SDK/PocketAPIOperation.m new file mode 100644 index 0000000..f1bc1cf --- /dev/null +++ b/SDK/PocketAPIOperation.m @@ -0,0 +1,428 @@ +// +// PocketAPIOperation.m +// PocketSDK +// +// Created by Steve Streza on 5/29/12. +// Copyright (c) 2012 Read It Later, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import "PocketAPIOperation.h" + +NSString *PocketAPINameForHTTPMethod(PocketAPIHTTPMethod method){ + switch (method) { + case PocketAPIHTTPMethodPOST: + return @"POST"; + break; + case PocketAPIHTTPMethodPUT: + return @"PUT"; + break; + case PocketAPIHTTPMethodDELETE: + return @"DELETE"; + break; + case PocketAPIHTTPMethodGET: + default: + return @"GET"; + break; + } +} + +@interface PocketAPI () +-(void)pkt_loggedInWithUsername:(NSString *)username token:(NSString *)accessToken; +-(NSString *)pkt_userAgent; +-(NSString *)pkt_getToken; +@end + +@interface PocketAPIOperation () + +-(void)pkt_connectionFinishedLoading; + +-(NSMutableURLRequest *)pkt_URLRequest; + +@end + +@implementation PocketAPIOperation + +@synthesize API, delegate, error; + +@synthesize domain, HTTPMethod, APIMethod, arguments; +@synthesize connection, response, data; + +-(void)start{ + finishedLoading = NO; + + // if there is no access token and this is not an auth method, fail and login + if(!self.API.loggedIn && !([APIMethod isEqualToString:@"request"] || [APIMethod isEqualToString:@"authorize"] || [APIMethod isEqualToString:@"oauth/authorize"])){ + [self connectionFinishedWithError:[NSError errorWithDomain:(NSString *)PocketAPIErrorDomain code:401 userInfo:nil]]; + return; + } + + NSURLRequest *request = [self pkt_URLRequest]; + connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; + [connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + + [connection start]; +} + +-(BOOL)isConcurrent{ + return YES; +} + +-(BOOL)isExecuting{ + return !finishedLoading; +} + +-(BOOL)isFinished{ + return finishedLoading; +} + +-(id)init{ + if(self = [super init]){ + domain = PocketAPIDomainDefault; + } + return self; +} + +-(void)dealloc{ + [API release], API = nil; + delegate = nil; + + [APIMethod release], APIMethod = nil; + [arguments release], arguments = nil; + + [connection release], connection = nil; + [response release], response = nil; + [data release], data = nil; + + [error release], error = nil; + + [super dealloc]; +} + +-(NSString *)description{ + return [NSString stringWithFormat:@"<%@: %p https://%@%@ %@>", [self class], self, self.baseURLPath, self.APIMethod, self.arguments]; +} + +-(NSString *)baseURLPath{ + switch (self.domain) { + case PocketAPIDomainAuth: + return @"getpocket.com/v3/oauth"; + break; + case PocketAPIDomainDefault: + default: + return @"getpocket.com/v3"; + break; + } +} + +-(NSDictionary *)responseDictionary{ + NSString *contentType = [[self.response allHeaderFields] objectForKey:@"Content-Type"]; + if([contentType isEqualToString:@"application/json"]){ + Class nsJSONSerialization = NSClassFromString(@"NSJSONSerialization"); + return [nsJSONSerialization JSONObjectWithData:self.data options:0 error:nil]; + }else if([contentType rangeOfString:@"application/x-www-form-urlencode"].location != NSNotFound){ + NSString *formString = [[[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding] autorelease]; + return [NSDictionary pkt_dictionaryByParsingURLEncodedFormString:formString]; + }else{ + return nil; + } +} + +#pragma mark NSURLConnectionDelegate + +- (void)connection:(NSURLConnection *)aConnection didReceiveResponse:(NSURLResponse *)receivedResponse{ + response = (NSHTTPURLResponse *)[receivedResponse retain]; + if([response statusCode] == 200){ + data = [[NSMutableData alloc] initWithCapacity:0]; + }else if([[response allHeaderFields] objectForKey:@"X-Error"]){ + [connection cancel]; + NSString *xError = [[response allHeaderFields] objectForKey:@"X-Error"]; + NSDictionary *userInfo = xError ? [NSDictionary dictionaryWithObjectsAndKeys:xError,NSLocalizedDescriptionKey,nil] : nil; + [self connection:connection didFailWithError:[NSError errorWithDomain:@"PocketSDK" + code:[response statusCode] + userInfo:userInfo]]; + } +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)inData{ + [data appendData:inData]; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)theError{ + [self connectionFinishedWithError:theError]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection{ + [self connectionFinishedWithError:nil]; +} + +-(void)connectionFinishedWithError:(NSError *)theError{ + NSInteger statusCode = (self.response ? self.response.statusCode : theError.code); + BOOL needsToRelogin = !attemptedRelogin && statusCode == 401; + BOOL needsToLogout = statusCode == 403; + BOOL serverError = statusCode >= 500; + + NSInteger errorCode = [[self.response.allHeaderFields objectForKey:@"X-Error-Code"] intValue]; + NSString *errorDescription = [self.response.allHeaderFields objectForKey:@"X-Error"]; + if(serverError){ + errorCode = PocketAPIErrorServerMaintenance; + errorDescription = @"There was a server error."; + } + + NSError *pocketError = nil; + if(errorCode){ + pocketError = [NSError errorWithDomain:(NSString *)PocketAPIErrorDomain + code:errorCode + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + errorDescription, @"localizedDescription", + theError, @"HTTPError", + nil]]; + }else if(theError){ + pocketError = [NSError errorWithDomain:(NSString *)PocketAPIErrorDomain + code:statusCode + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + errorDescription, @"localizedDescription", + theError, @"HTTPError", + nil]]; + }else if(needsToLogout){ + pocketError = [NSError errorWithDomain:(NSString *)PocketAPIErrorDomain code:statusCode userInfo:nil]; + } + + error = [pocketError retain]; + + if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPI:receivedResponse:forAPIMethod:error:)]){ + [self.delegate pocketAPI:self.API receivedResponse:[self responseDictionary] forAPIMethod:self.APIMethod error:theError]; + } + + // if the user has deauthorized the app, we bounce them to the Pocket to re-login + // if this succeeds, we re-call the API the app requested + // if it fails, then prompt for an error next time + if(needsToRelogin){ + [self.API loginWithDelegate:self]; + attemptedRelogin = YES; + return; + } + + if(needsToLogout){ + [self.API logout]; + } + + if(error){ + if([self.APIMethod rangeOfString:@"auth"].location != NSNotFound || [self.APIMethod isEqualToString:@"request"]){ + if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPI:hadLoginError:)]){ + [self.delegate pocketAPI:self.API hadLoginError:error]; + } + }else if([self.APIMethod isEqualToString:@"add"]){ + if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPI:failedToSaveURL:error:)]){ + [self.delegate pocketAPI:self.API + failedToSaveURL:[NSURL URLWithString:[self.arguments objectForKey:@"url"]] + error:error]; + } + } + }else{ + if([self.APIMethod isEqualToString:@"auth"]){ + [self.API pkt_loggedInWithUsername:[self.arguments objectForKey:@"username"] token:[self.arguments objectForKey:@"token"]]; + + if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPILoggedIn:)]){ + [self.delegate pocketAPILoggedIn:self.API]; + } + }else if([self.APIMethod isEqualToString:@"add"]){ + if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPI:savedURL:)]){ + NSString *urlString = [self.arguments objectForKey:@"url"]; + NSURL *url = urlString ? [NSURL URLWithString:urlString] : nil; + [self.delegate pocketAPI:self.API + savedURL:url]; + } + } + else if([self.APIMethod isEqualToString:@"request"]){ + NSDictionary *responseDict = [self responseDictionary]; + [self.delegate pocketAPI:self.API receivedRequestToken:[responseDict objectForKey:@"code"]]; + } + else if([self.APIMethod isEqualToString:@"authorize"] || [self.APIMethod isEqualToString:@"oauth/authorize"]){ + NSDictionary *responseDict = [self responseDictionary]; + NSString *username = [responseDict objectForKey:@"username"]; + + if((!username || username == (id)[NSNull null]) && [[[self arguments] objectForKey:@"grant_type"] isEqualToString:@"credentials"]){ + username = [[self arguments] objectForKey:@"username"]; + } + + NSString *token = [responseDict objectForKey:@"access_token"]; + + if((id)username == [NSNull null] && (id)token == [NSNull null]){ + [self.delegate pocketAPI:self.API hadLoginError:[NSError errorWithDomain:@"PocketAPI" code:404 userInfo:nil]]; + }else{ + [self.API pkt_loggedInWithUsername:username token:token]; + if(self.delegate && [self.delegate respondsToSelector:@selector(pocketAPILoggedIn:)]){ + [self.delegate pocketAPILoggedIn:self.API]; + } + } + } + } + [self pkt_connectionFinishedLoading]; +} + +#pragma mark Handling Re-login + +-(void)pocketAPILoggedIn:(PocketAPI *)api{ + [[self.API operationQueue] addOperation:[[self copy] autorelease]]; +} + +-(void)pocketAPI:(PocketAPI *)api hadLoginError:(NSError *)theError{ + [self connectionFinishedWithError:theError]; +} + +#pragma mark Private APIs + +-(NSDictionary *)pkt_requestArguments{ + NSMutableDictionary *dict = [[self.arguments mutableCopy] autorelease]; + if(self.API.consumerKey){ + [dict setObject:self.API.consumerKey forKey:@"consumer_key"]; + } + + NSString *accessToken = [self.API pkt_getToken]; + if(accessToken){ + [dict setObject:accessToken forKey:@"access_token"]; + } + + return dict; +} + +-(NSMutableURLRequest *)pkt_URLRequest{ + NSString *urlString = [NSString stringWithFormat:@"https://%@/%@", self.baseURLPath, self.APIMethod]; + + NSDictionary *requestArgs = [self pkt_requestArguments]; + + if(self.HTTPMethod == PocketAPIHTTPMethodGET && requestArgs.count > 0){ + NSMutableArray *pairs = [NSMutableArray array]; + + for(NSString *key in [requestArgs allKeys]){ + [pairs addObject:[NSString stringWithFormat:@"%@=%@",key, [PocketAPIOperation encodeForURL:[requestArgs objectForKey:key]]]]; + } + + if(pairs.count > 0){ + urlString = [urlString stringByAppendingFormat:@"?%@", [pairs componentsJoinedByString:@"&"]]; + } + } + + NSURL *url = [NSURL URLWithString:urlString]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + [request setHTTPMethod:PocketAPINameForHTTPMethod(self.HTTPMethod)]; + [request setTimeoutInterval:20.]; + [request setCachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData]; + + Class nsJSONSerialization = NSClassFromString(@"NSJSONSerialization"); + + if(self.HTTPMethod != PocketAPIHTTPMethodGET && requestArgs.count > 0){ + if(nsJSONSerialization != nil){ + [request addValue:@"application/json; charset=UTF-8" forHTTPHeaderField:@"Content-Type"]; + [request setHTTPBody:[nsJSONSerialization dataWithJSONObject:requestArgs options:0 error:nil]]; + }else{ + [request addValue:@"application/x-www-form-urlencoded; charset=UTF-8" forHTTPHeaderField:@"Content-Type"]; + [request setHTTPBody:[[requestArgs pkt_URLEncodedFormString] dataUsingEncoding:NSUTF8StringEncoding]]; + } + } + + NSString *userAgent = [self.API pkt_userAgent]; + if(userAgent){ + [request addValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + + if(nsJSONSerialization != nil){ + [request addValue:@"application/json" forHTTPHeaderField:@"X-Accept"]; + }else{ + [request addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"X-Accept"]; + } + + return [request autorelease]; +} + ++(NSString *)encodeForURL:(NSString *)urlStr +{ + NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, + (CFStringRef)urlStr, + NULL, + CFSTR("!*'();:@&=+$,/?%#[]"), + kCFStringEncodingUTF8); + return [result autorelease]; +} + ++(NSString *)decodeForURL:(NSString *)urlStr{ + NSString *result = (NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, + (CFStringRef)urlStr, + CFSTR(""), + kCFStringEncodingUTF8); + return [result autorelease]; +} + +-(void)pkt_connectionFinishedLoading{ + [self willChangeValueForKey:@"isExecuting"]; + [self willChangeValueForKey:@"isFinished"]; + finishedLoading = YES; + [self didChangeValueForKey:@"isFinished"]; + [self didChangeValueForKey:@"isExecuting"]; + + [delegate release], delegate = nil; +} + ++(NSError *)errorFromXError:(NSString *)xError + withErrorCode:(NSUInteger)errorCode + HTTPStatusCode:(NSUInteger)statusCode{ + return nil; +} + +#pragma mark NSCopying + +- (id)copyWithZone:(NSZone *)zone{ + PocketAPIOperation *operation = [[PocketAPIOperation allocWithZone:zone] init]; + operation.API = self.API; + operation.delegate = self.delegate; + operation.domain = self.domain; + operation.HTTPMethod = self.HTTPMethod; + operation.APIMethod = self.APIMethod; + operation.arguments = self.arguments; + return operation; +} + +@end + +@implementation NSDictionary (PocketAdditions) + +-(NSString *)pkt_URLEncodedFormString{ + NSMutableArray *formPieces = [NSMutableArray arrayWithCapacity:self.allKeys.count]; + for(NSString *key in self.allKeys){ + NSString *value = [self objectForKey:key]; + [formPieces addObject:[NSString stringWithFormat:@"%@=%@", [PocketAPIOperation encodeForURL:key], [PocketAPIOperation encodeForURL:value]]]; + } + return [formPieces componentsJoinedByString:@"&"]; +} + ++(NSDictionary *)pkt_dictionaryByParsingURLEncodedFormString:(NSString *)formString{ + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + NSArray *formPieces = [formString componentsSeparatedByString:@"&"]; + for(NSString *formPiece in formPieces){ + NSArray *fieldPieces = [formPiece componentsSeparatedByString:@"="]; + if(fieldPieces.count == 2){ + NSString *fieldKey = [fieldPieces objectAtIndex:0]; + NSString *fieldValue = [fieldPieces objectAtIndex:1]; + [dictionary setObject:[PocketAPIOperation decodeForURL:fieldValue] + forKey:[PocketAPIOperation decodeForURL:fieldKey]]; + } + } + return [[dictionary copy] autorelease]; +} + +@end \ No newline at end of file diff --git a/SDK/PocketAPITypes.h b/SDK/PocketAPITypes.h new file mode 100644 index 0000000..c2d6195 --- /dev/null +++ b/SDK/PocketAPITypes.h @@ -0,0 +1,79 @@ +// +// PocketAPITypes.h +// PocketSDK +// +// Created by Steve Streza on 5/29/12. +// Copyright (c) 2012 Read It Later, Inc. All rights reserved. +// + +#import + +@class PocketAPI; + +@protocol PocketAPIDelegate +@optional +-(void)pocketAPI:(PocketAPI *)api receivedRequestToken:(NSString *)requestToken; + +-(void)pocketAPILoggedIn:(PocketAPI *)api; +-(void)pocketAPI:(PocketAPI *)api hadLoginError:(NSError *)error; + +-(void)pocketAPI:(PocketAPI *)api savedURL:(NSURL *)url; +-(void)pocketAPI:(PocketAPI *)api failedToSaveURL:(NSURL *)url error:(NSError *)error; + +-(void)pocketAPI:(PocketAPI *)api receivedResponse:(NSDictionary *)response forAPIMethod:(NSString *)APIMethod error:(NSError *)error; + +-(void)pocketAPIDidStartLogin:(PocketAPI *)api; +-(void)pocketAPIDidFinishLogin:(PocketAPI *)api; +@end + +@protocol PocketAPISupport +@optional +-(BOOL)shouldAllowPocketReverseAuth; + +@end + +#if NS_BLOCKS_AVAILABLE +typedef void(^PocketAPILoginHandler)(PocketAPI *api, NSError *error); +typedef void(^PocketAPISaveHandler)(PocketAPI *api, NSURL *url, NSError *error); +typedef void(^PocketAPIResponseHandler)(PocketAPI *api, NSString *apiMethod, NSDictionary *response, NSError *error); +#endif + +typedef enum { + PocketAPIDomainDefault = 0, + PocketAPIDomainAuth = 10 +} PocketAPIDomain; + +typedef enum { + PocketAPIHTTPMethodGET, + PocketAPIHTTPMethodPOST, + PocketAPIHTTPMethodPUT, + PocketAPIHTTPMethodDELETE +} PocketAPIHTTPMethod; + +typedef enum { + //OAuth Errors + PocketAPIErrorNoConsumerKey = 138, + PocketAPIErrorNoAccessToken = 107, + PocketAPIErrorInvalidConsumerKey = 136, + PocketAPIErrorInvalidRequest = 130, + PocketAPIErrorNoChangesMade = 131, + PocketAPIErrorConsumerKeyAccessTokenMismatch = 137, + PocketAPIErrorEndpointForbidden = 150, + PocketAPIErrorEndpointRequiresAdditionalPermissions = 151, + + // Signup Errors + PocketAPIErrorSignupInvalidUsernameAndPassword = 100, + PocketAPIErrorSignupInvalidUsername = 101, + PocketAPIErrorSignupInvalidPassword = 102, + PocketAPIErrorSignupInvalidEmail = 103, + PocketAPIErrorSignupUsernameTaken = 104, + PocketAPIErrorSignupEmailTaken = 105, + + // Server Problems + PocketAPIErrorServerMaintenance = 199 +} PocketAPIError; + +extern const NSString *PocketAPIErrorDomain; + +extern const NSString *PocketAPILoginStartedNotification; +extern const NSString *PocketAPILoginFinishedNotification; diff --git a/SDK/SFHFKeychainUtils.h b/SDK/SFHFKeychainUtils.h new file mode 100644 index 0000000..9026bc5 --- /dev/null +++ b/SDK/SFHFKeychainUtils.h @@ -0,0 +1,41 @@ +// +// SFHFKeychainUtils.h +// +// Created by Buzz Andersen on 10/20/08. +// Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone. +// Copyright 2008 Sci-Fi Hi-Fi. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#import + + +@interface SFHFKeychainUtils : NSObject { + +} + ++ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error; ++ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error; ++ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error; + +@end \ No newline at end of file diff --git a/SDK/SFHFKeychainUtils.m b/SDK/SFHFKeychainUtils.m new file mode 100644 index 0000000..0017b83 --- /dev/null +++ b/SDK/SFHFKeychainUtils.m @@ -0,0 +1,438 @@ +// +// SFHFKeychainUtils.m +// +// Created by Buzz Andersen on 10/20/08. +// Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone. +// Copyright 2008 Sci-Fi Hi-Fi. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#import "SFHFKeychainUtils.h" +#import + +static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain"; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR +@interface SFHFKeychainUtils (PrivateMethods) ++ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error; +@end +#endif + +@implementation SFHFKeychainUtils + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR + ++ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error { + if (!username || !serviceName) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + return nil; + } + + SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error]; + + if (*error || !item) { + return nil; + } + + // from Advanced Mac OS X Programming, ch. 16 + UInt32 length; + char *password; + SecKeychainAttribute attributes[8]; + SecKeychainAttributeList list; + + attributes[0].tag = kSecAccountItemAttr; + attributes[1].tag = kSecDescriptionItemAttr; + attributes[2].tag = kSecLabelItemAttr; + attributes[3].tag = kSecModDateItemAttr; + + list.count = 4; + list.attr = attributes; + + OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password); + + if (status != noErr) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil]; + return nil; + } + + NSString *passwordString = nil; + + if (password != NULL) { + char passwordBuffer[1024]; + + if (length > 1023) { + length = 1023; + } + strncpy(passwordBuffer, password, length); + + passwordBuffer[length] = '\0'; + passwordString = [NSString stringWithCString:passwordBuffer]; + } + + SecKeychainItemFreeContent(&list, password); + + CFRelease(item); + + return passwordString; +} + ++ (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error { + if (!username || !password || !serviceName) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + return; + } + + OSStatus status = noErr; + + SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error]; + + if (*error && [*error code] != noErr) { + return; + } + + *error = nil; + + if (item) { + status = SecKeychainItemModifyAttributesAndData(item, + NULL, + strlen([password UTF8String]), + [password UTF8String]); + + CFRelease(item); + } + else { + status = SecKeychainAddGenericPassword(NULL, + strlen([serviceName UTF8String]), + [serviceName UTF8String], + strlen([username UTF8String]), + [username UTF8String], + strlen([password UTF8String]), + [password UTF8String], + NULL); + } + + if (status != noErr) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil]; + } +} + ++ (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error { + if (!username || !serviceName) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: 2000 userInfo: nil]; + return; + } + + *error = nil; + + SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error]; + + if (*error && [*error code] != noErr) { + return; + } + + OSStatus status; + + if (item) { + status = SecKeychainItemDelete(item); + + CFRelease(item); + } + + if (status != noErr) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil]; + } +} + ++ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error { + if (!username || !serviceName) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + return nil; + } + + *error = nil; + + SecKeychainItemRef item; + + OSStatus status = SecKeychainFindGenericPassword(NULL, + strlen([serviceName UTF8String]), + [serviceName UTF8String], + strlen([username UTF8String]), + [username UTF8String], + NULL, + NULL, + &item); + + if (status != noErr) { + if (status != errSecItemNotFound) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil]; + } + + return nil; + } + + return item; +} + +#else + ++ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error { + if (!username || !serviceName) { + if (error != nil) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + } + return nil; + } + + if (error != nil) { + *error = nil; + } + + // Set up a query dictionary with the base query attributes: item type (generic), username, and service + + NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil] autorelease]; + NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, username, serviceName, nil] autorelease]; + + NSMutableDictionary *query = [[[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys] autorelease]; + + // First do a query for attributes, in case we already have a Keychain item with no password data set. + // One likely way such an incorrect item could have come about is due to the previous (incorrect) + // version of this code (which set the password as a generic attribute instead of password data). + + NSDictionary *attributeResult = NULL; + NSMutableDictionary *attributeQuery = [query mutableCopy]; + [attributeQuery setObject: (id) kCFBooleanTrue forKey:(id) kSecReturnAttributes]; + OSStatus status = SecItemCopyMatching((CFDictionaryRef) attributeQuery, (CFTypeRef *) &attributeResult); + + [attributeResult release]; + [attributeQuery release]; + + if (status != noErr) { + // No existing item found--simply return nil for the password + if (error != nil && status != errSecItemNotFound) { + //Only return an error if a real exception happened--not simply for "not found." + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil]; + } + + return nil; + } + + // We have an existing item, now query for the password data associated with it. + + NSData *resultData = nil; + NSMutableDictionary *passwordQuery = [query mutableCopy]; + [passwordQuery setObject: (id) kCFBooleanTrue forKey: (id) kSecReturnData]; + + status = SecItemCopyMatching((CFDictionaryRef) passwordQuery, (CFTypeRef *) &resultData); + + [resultData autorelease]; + [passwordQuery release]; + + if (status != noErr) { + if (status == errSecItemNotFound) { + // We found attributes for the item previously, but no password now, so return a special error. + // Users of this API will probably want to detect this error and prompt the user to + // re-enter their credentials. When you attempt to store the re-entered credentials + // using storeUsername:andPassword:forServiceName:updateExisting:error + // the old, incorrect entry will be deleted and a new one with a properly encrypted + // password will be added. + if (error != nil) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil]; + } + } + else { + // Something else went wrong. Simply return the normal Keychain API error code. + if (error != nil) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil]; + } + } + + return nil; + } + + NSString *password = nil; + + if (resultData) { + password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding]; + } + else { + // There is an existing item, but we weren't able to get password data for it for some reason, + // Possibly as a result of an item being incorrectly entered by the previous code. + // Set the -1999 error so the code above us can prompt the user again. + if (error != nil) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil]; + } + } + + return [password autorelease]; +} + ++ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error +{ + if (!username || !password || !serviceName) + { + if (error != nil) + { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + } + return NO; + } + + // See if we already have a password entered for these credentials. + NSError *getError = nil; + NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error:&getError]; + + if ([getError code] == -1999) + { + // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code. + // Delete the existing item before moving on entering a correct one. + + getError = nil; + + [self deleteItemForUsername: username andServiceName: serviceName error: &getError]; + + if ([getError code] != noErr) + { + if (error != nil) + { + *error = getError; + } + return NO; + } + } + else if ([getError code] != noErr) + { + if (error != nil) + { + *error = getError; + } + return NO; + } + + if (error != nil) + { + *error = nil; + } + + OSStatus status = noErr; + + if (existingPassword) + { + // We have an existing, properly entered item with a password. + // Update the existing item. + + if (![existingPassword isEqualToString:password] && updateExisting) + { + //Only update if we're allowed to update existing. If not, simply do nothing. + + NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, + kSecAttrService, + kSecAttrLabel, + kSecAttrAccount, + nil] autorelease]; + + NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, + serviceName, + serviceName, + username, + nil] autorelease]; + + NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease]; + + status = SecItemUpdate((CFDictionaryRef) query, (CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (NSString *) kSecValueData]); + } + } + else + { + // No existing entry (or an existing, improperly entered, and therefore now + // deleted, entry). Create a new entry. + + NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, + kSecAttrService, + kSecAttrLabel, + kSecAttrAccount, + kSecValueData, + nil] autorelease]; + + NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, + serviceName, + serviceName, + username, + [password dataUsingEncoding: NSUTF8StringEncoding], + nil] autorelease]; + + NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease]; + + status = SecItemAdd((CFDictionaryRef) query, NULL); + } + + if (status != noErr) + { + // Something went wrong with adding the new item. Return the Keychain error code. + if (error != nil) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil]; + } + + return NO; + } + + return YES; +} + ++ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error +{ + if (!username || !serviceName) + { + if (error != nil) + { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + } + return NO; + } + + if (error != nil) + { + *error = nil; + } + + NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil] autorelease]; + NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil] autorelease]; + + NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease]; + + OSStatus status = SecItemDelete((CFDictionaryRef) query); + + if (status != noErr) + { + if (error != nil) { + *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil]; + } + + return NO; + } + + return YES; +} + +#endif + +@end \ No newline at end of file