/* Copyright (c) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//
// GTMOAuth2ViewControllerTouch.m
//

#import <Foundation/Foundation.h>
#import <Security/Security.h>

#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES

#if TARGET_OS_IPHONE

#define GTMOAUTH2VIEWCONTROLLERTOUCH_DEFINE_GLOBALS 1
#import "GTMOAuth2ViewControllerTouch.h"

#import "GTMOAuth2SignIn.h"
#import "GTMOAuth2Authentication.h"

static NSString * const kGTMOAuth2AccountName = @"OAuth";
static GTMOAuth2Keychain* sDefaultKeychain = nil;

@interface GTMOAuth2ViewControllerTouch()

@property (nonatomic, copy) NSURLRequest *request;

- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request;
- (void)signIn:(GTMOAuth2SignIn *)signIn
finishedWithAuth:(GTMOAuth2Authentication *)auth
         error:(NSError *)error;
- (BOOL)isNavigationBarTranslucent;
- (void)moveWebViewFromUnderNavigationBar;
- (void)popView;
- (void)clearBrowserCookies;
@end

@implementation GTMOAuth2ViewControllerTouch

// IBOutlets
@synthesize request = request_,
            backButton = backButton_,
            forwardButton = forwardButton_,
            navButtonsView = navButtonsView_,
            rightBarButtonItem = rightBarButtonItem_,
            webView = webView_;

@synthesize keychainItemName = keychainItemName_,
            keychainItemAccessibility = keychainItemAccessibility_,
            initialHTMLString = initialHTMLString_,
            browserCookiesURL = browserCookiesURL_,
            signIn = signIn_,
            userData = userData_,
            properties = properties_;

#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (id)controllerWithScope:(NSString *)scope
                 clientID:(NSString *)clientID
             clientSecret:(NSString *)clientSecret
         keychainItemName:(NSString *)keychainItemName
                 delegate:(id)delegate
         finishedSelector:(SEL)finishedSelector {
  return [[[self alloc] initWithScope:scope
                             clientID:clientID
                         clientSecret:clientSecret
                     keychainItemName:keychainItemName
                             delegate:delegate
                     finishedSelector:finishedSelector] autorelease];
}

- (id)initWithScope:(NSString *)scope
           clientID:(NSString *)clientID
       clientSecret:(NSString *)clientSecret
   keychainItemName:(NSString *)keychainItemName
           delegate:(id)delegate
   finishedSelector:(SEL)finishedSelector {
  // convenient entry point for Google authentication

  Class signInClass = [[self class] signInClass];

  GTMOAuth2Authentication *auth;
  auth = [signInClass standardGoogleAuthenticationForScope:scope
                                                  clientID:clientID
                                              clientSecret:clientSecret];
  NSURL *authorizationURL = [signInClass googleAuthorizationURL];
  return [self initWithAuthentication:auth
                     authorizationURL:authorizationURL
                     keychainItemName:keychainItemName
                             delegate:delegate
                     finishedSelector:finishedSelector];
}

#if NS_BLOCKS_AVAILABLE

+ (id)controllerWithScope:(NSString *)scope
                 clientID:(NSString *)clientID
             clientSecret:(NSString *)clientSecret
         keychainItemName:(NSString *)keychainItemName
        completionHandler:(void (^)(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error))handler {
  return [[[self alloc] initWithScope:scope
                             clientID:clientID
                         clientSecret:clientSecret
                     keychainItemName:keychainItemName
                    completionHandler:handler] autorelease];
}

- (id)initWithScope:(NSString *)scope
           clientID:(NSString *)clientID
       clientSecret:(NSString *)clientSecret
   keychainItemName:(NSString *)keychainItemName
  completionHandler:(void (^)(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error))handler {
  // convenient entry point for Google authentication

  Class signInClass = [[self class] signInClass];

  GTMOAuth2Authentication *auth;
  auth = [signInClass standardGoogleAuthenticationForScope:scope
                                                  clientID:clientID
                                              clientSecret:clientSecret];
  NSURL *authorizationURL = [signInClass googleAuthorizationURL];
  self = [self initWithAuthentication:auth
                     authorizationURL:authorizationURL
                     keychainItemName:keychainItemName
                             delegate:nil
                     finishedSelector:NULL];
  if (self) {
    completionBlock_ = [handler copy];
  }
  return self;
}

#endif // NS_BLOCKS_AVAILABLE
#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT

+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
                  authorizationURL:(NSURL *)authorizationURL
                  keychainItemName:(NSString *)keychainItemName
                          delegate:(id)delegate
                  finishedSelector:(SEL)finishedSelector {
  return [[[self alloc] initWithAuthentication:auth
                              authorizationURL:authorizationURL
                              keychainItemName:keychainItemName
                                      delegate:delegate
                              finishedSelector:finishedSelector] autorelease];  
}

- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
            authorizationURL:(NSURL *)authorizationURL
            keychainItemName:(NSString *)keychainItemName
                    delegate:(id)delegate
            finishedSelector:(SEL)finishedSelector {

  NSString *nibName = [[self class] authNibName];
  NSBundle *nibBundle = [[self class] authNibBundle];

  self = [super initWithNibName:nibName bundle:nibBundle];
  if (self != nil) {
    delegate_ = [delegate retain];
    finishedSelector_ = finishedSelector;

    Class signInClass = [[self class] signInClass];

    // use the supplied auth and OAuth endpoint URLs
    signIn_ = [[signInClass alloc] initWithAuthentication:auth
                                         authorizationURL:authorizationURL
                                                 delegate:self
                                       webRequestSelector:@selector(signIn:displayRequest:)
                                         finishedSelector:@selector(signIn:finishedWithAuth:error:)];
    
    // if the user is signing in to a Google service, we'll delete the
    // Google authentication browser cookies upon completion
    //
    // for other service domains, or to disable clearing of the cookies,
    // set the browserCookiesURL property explicitly
    NSString *authorizationHost = [signIn_.authorizationURL host];
    if ([authorizationHost hasSuffix:@".google.com"]) {
      NSString *urlStr = [NSString stringWithFormat:@"https://%@/",
                          authorizationHost];
      NSURL *cookiesURL = [NSURL URLWithString:urlStr];
      [self setBrowserCookiesURL:cookiesURL];
    }

    [self setKeychainItemName:keychainItemName];
  }
  return self;
}

#if NS_BLOCKS_AVAILABLE
+ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
                  authorizationURL:(NSURL *)authorizationURL
                  keychainItemName:(NSString *)keychainItemName
                 completionHandler:(void (^)(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error))handler {
  return [[[self alloc] initWithAuthentication:auth
                              authorizationURL:authorizationURL
                              keychainItemName:keychainItemName
                             completionHandler:handler] autorelease];
}

- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
            authorizationURL:(NSURL *)authorizationURL
            keychainItemName:(NSString *)keychainItemName
           completionHandler:(void (^)(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error))handler {
  // fall back to the non-blocks init
  self = [self initWithAuthentication:auth
                     authorizationURL:authorizationURL
                     keychainItemName:keychainItemName
                             delegate:nil
                     finishedSelector:NULL];
  if (self) {
    completionBlock_ = [handler copy];
  }
  return self;
}
#endif

- (void)dealloc {
  [webView_ setDelegate:nil];

  [backButton_ release];
  [forwardButton_ release];
  [navButtonsView_ release];
  [rightBarButtonItem_ release];
  [webView_ release];
  [signIn_ release];
  [request_ release];
  [delegate_ release];
#if NS_BLOCKS_AVAILABLE
  [completionBlock_ release];
#endif
  [keychainItemName_ release];
  [initialHTMLString_ release];
  [browserCookiesURL_ release];
  [userData_ release];
  [properties_ release];

  [super dealloc];
}

+ (NSString *)authNibName {
  // subclasses may override this to specify a custom nib name
  return @"GTMOAuth2ViewTouch";
}

+ (NSBundle *)authNibBundle {
  // subclasses may override this to specify a custom nib bundle
  return nil;
}

#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
                                                     clientID:(NSString *)clientID
                                                 clientSecret:(NSString *)clientSecret {
  Class signInClass = [self signInClass];
  NSURL *tokenURL = [signInClass googleTokenURL];
  NSString *redirectURI = [signInClass nativeClientRedirectURI];
  
  GTMOAuth2Authentication *auth;
  auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
                                                           tokenURL:tokenURL
                                                        redirectURI:redirectURI
                                                           clientID:clientID
                                                       clientSecret:clientSecret];
  [[self class] authorizeFromKeychainForName:keychainItemName
                              authentication:auth];
  return auth;
}
#endif

+ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
                      authentication:(GTMOAuth2Authentication *)newAuth {
  newAuth.accessToken = nil;

  BOOL didGetTokens = NO;
  GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
  NSString *password = [keychain passwordForService:keychainItemName
                                            account:kGTMOAuth2AccountName
                                              error:nil];
  if (password != nil) {
    [newAuth setKeysForResponseString:password];
    didGetTokens = YES;
  }
  return didGetTokens;
}

+ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName {
  GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
  return [keychain removePasswordForService:keychainItemName
                                    account:kGTMOAuth2AccountName
                                      error:nil];
}

+ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName
                     authentication:(GTMOAuth2Authentication *)auth {
  return [self saveParamsToKeychainForName:keychainItemName
                             accessibility:NULL
                            authentication:auth];
}

+ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName
                      accessibility:(CFTypeRef)accessibility
                     authentication:(GTMOAuth2Authentication *)auth {
  [self removeAuthFromKeychainForName:keychainItemName];
  // don't save unless we have a token that can really authorize requests
  if (![auth canAuthorize]) return NO;

  if (accessibility == NULL
      && &kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly != NULL) {
    accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
  }

  // make a response string containing the values we want to save
  NSString *password = [auth persistenceResponseString];
  GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
  return [keychain setPassword:password
                    forService:keychainItemName
                 accessibility:accessibility
                       account:kGTMOAuth2AccountName
                         error:nil];
}

- (void)loadView {
  NSString *nibPath = nil;
  NSBundle *nibBundle = [self nibBundle];
  if (nibBundle == nil) {
    nibBundle = [NSBundle mainBundle];
  }
  NSString *nibName = self.nibName;
  if (nibName != nil) {
    nibPath = [nibBundle pathForResource:nibName ofType:@"nib"];
  }
  if (nibPath != nil && [[NSFileManager defaultManager] fileExistsAtPath:nibPath]) {
    [super loadView];
  } else {
    // One of the requirements of loadView is that a valid view object is set to
    // self.view upon completion. Otherwise, subclasses that attempt to
    // access self.view after calling [super loadView] will enter an infinite
    // loop due to the fact that UIViewController's -view accessor calls
    // loadView when self.view is nil.
    self.view = [[[UIView alloc] init] autorelease];

#if DEBUG
    NSLog(@"missing %@.nib", nibName);
#endif
  }
}


- (void)viewDidLoad {
  // the app may prefer some html other than blank white to be displayed
  // before the sign-in web page loads
  NSString *html = self.initialHTMLString;
  if ([html length] > 0) {
    [[self webView] loadHTMLString:html baseURL:nil];
  }

  rightBarButtonItem_.customView = navButtonsView_;
  self.navigationItem.rightBarButtonItem = rightBarButtonItem_;
}

- (void)popView {
  if (self.navigationController.topViewController == self) {
    if (!self.view.isHidden) {
      // Set the flag to our viewWillDisappear method so it knows
      // this is a disappearance initiated by the sign-in object,
      // not the user cancelling via the navigation controller
      didDismissSelf_ = YES;

      [self.navigationController popViewControllerAnimated:YES];
      self.view.hidden = YES;
    }
  }
}

- (void)notifyWithName:(NSString *)name
               webView:(UIWebView *)webView
                  kind:(NSString *)kind {
  BOOL isStarting = [name isEqual:kGTMOAuth2WebViewStartedLoading];
  if (hasNotifiedWebViewStartedLoading_ == isStarting) {
    // Duplicate notification
    //
    // UIWebView's delegate methods are so unbalanced that there's little
    // point trying to keep a count, as it could easily end up stuck greater
    // than zero.
    //
    // We don't really have a way to track the starts and stops of
    // subframe loads, too, as the webView in the notification is always
    // for the topmost request.
    return;
  }
  hasNotifiedWebViewStartedLoading_ = isStarting;

  // Notification for webview load starting and stopping
  NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                        webView, kGTMOAuth2WebViewKey,
                        kind, kGTMOAuth2WebViewStopKindKey, // kind may be nil
                        nil];
  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc postNotificationName:name
                    object:self
                  userInfo:dict];
}

- (void)cancelSigningIn {
  // The application has explicitly asked us to cancel signing in
  // (so no further callback is required)
  hasCalledFinished_ = YES;

  [delegate_ autorelease];
  delegate_ = nil;

#if NS_BLOCKS_AVAILABLE
  [completionBlock_ autorelease];
  completionBlock_ = nil;
#endif

  // The sign-in object's cancel method will close the window
  [signIn_ cancelSigningIn];
  hasDoneFinalRedirect_ = YES;
}

static Class gSignInClass = Nil;

+ (Class)signInClass {
  if (gSignInClass == Nil) {
    gSignInClass = [GTMOAuth2SignIn class];
  }
  return gSignInClass;
}

+ (void)setSignInClass:(Class)theClass {
  gSignInClass = theClass;
}

#pragma mark Token Revocation

#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
  [[self signInClass] revokeTokenForGoogleAuthentication:auth];
}
#endif

#pragma mark Browser Cookies

- (GTMOAuth2Authentication *)authentication {
  return self.signIn.authentication;
}

- (void)clearBrowserCookies {
  // if browserCookiesURL is non-nil, then get cookies for that URL
  // and delete them from the common application cookie storage
  NSURL *cookiesURL = [self browserCookiesURL];
  if (cookiesURL) {
    NSHTTPCookieStorage *cookieStorage;

    cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    NSArray *cookies =  [cookieStorage cookiesForURL:cookiesURL];

    for (NSHTTPCookie *cookie in cookies) {
      [cookieStorage deleteCookie:cookie];
    }
  }
}

#pragma mark Accessors

- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val {
  signIn_.networkLossTimeoutInterval = val;
}

- (NSTimeInterval)networkLossTimeoutInterval {
  return signIn_.networkLossTimeoutInterval;
}

- (BOOL)shouldUseKeychain {
  NSString *name = self.keychainItemName;
  return ([name length] > 0);
}

#pragma mark User Properties

- (void)setProperty:(id)obj forKey:(NSString *)key {
  if (obj == nil) {
    // User passed in nil, so delete the property
    [properties_ removeObjectForKey:key];
  } else {
    // Be sure the property dictionary exists
    if (properties_ == nil) {
      [self setProperties:[NSMutableDictionary dictionary]];
    }
    [properties_ setObject:obj forKey:key];
  }
}

- (id)propertyForKey:(NSString *)key {
  id obj = [properties_ objectForKey:key];

  // Be sure the returned pointer has the life of the autorelease pool,
  // in case self is released immediately
  return [[obj retain] autorelease];
}

#pragma mark SignIn callbacks

- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request {
  // this is the signIn object's webRequest method, telling the controller
  // to either display the request in the webview, or close the window
  //
  // All web requests and all window closing goes through this routine

#if DEBUG
  if (self.navigationController) {
    if (self.navigationController.topViewController != self && request != nil) {
      NSLog(@"Unexpected: Request to show, when already on top. request %@", [request URL]);
    } else if(self.navigationController.topViewController != self && request == nil) {
      NSLog(@"Unexpected: Request to pop, when not on top. request nil");
    }
  }
#endif

  if (request != nil) {
    const NSTimeInterval kJanuary2011 = 1293840000;
    BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011);
    if (isDateValid) {
      // Display the request.
      self.request = request;
      BOOL shouldWaitForHTML = ([self.initialHTMLString length] > 0);
      if (shouldWaitForHTML) {
        [self.webView performSelector:@selector(loadRequest:)
                           withObject:request
                           afterDelay:0.05];
      } else {
        [self.webView loadRequest:request];
      }
    } else {
      // clock date is invalid, so signing in would fail with an unhelpful error
      // from the server. Warn the user in an html string showing a watch icon,
      // question mark, and the system date and time. Hopefully this will clue
      // in brighter users, or at least give them a clue when they report the
      // problem to developers.
      //
      // Even better is for apps to check the system clock and show some more
      // helpful, localized instructions for users; this is really a fallback.
      NSString *html = @"<html><body><div align=center><font size='7'>"
        @"&#x231A; ?<br><i>System Clock Incorrect</i><br>%@"
        @"</font></div></body></html>";
      NSString *errHTML = [NSString stringWithFormat:html, [NSDate date]];

      [[self webView] loadHTMLString:errHTML baseURL:nil];
    }
  } else {
    // request was nil.
    [self popView];
  }
}

- (void)signIn:(GTMOAuth2SignIn *)signIn
  finishedWithAuth:(GTMOAuth2Authentication *)auth
             error:(NSError *)error {
  if (!hasCalledFinished_) {
    hasCalledFinished_ = YES;

    if (error == nil) {
      if (self.shouldUseKeychain) {
        NSString *keychainItemName = self.keychainItemName;
        if (auth.canAuthorize) {
          // save the auth params in the keychain
          CFTypeRef accessibility = self.keychainItemAccessibility;
          [[self class] saveParamsToKeychainForName:keychainItemName
                                      accessibility:accessibility
                                     authentication:auth];
        } else {
          // remove the auth params from the keychain
          [[self class] removeAuthFromKeychainForName:keychainItemName];
        }
      }
    }

    if (delegate_ && finishedSelector_) {
      SEL sel = finishedSelector_;
      NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel];
      NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
      [invocation setSelector:sel];
      [invocation setTarget:delegate_];
      [invocation setArgument:&self atIndex:2];
      [invocation setArgument:&auth atIndex:3];
      [invocation setArgument:&error atIndex:4];
      [invocation invoke];
    }

    [delegate_ autorelease];
    delegate_ = nil;

#if NS_BLOCKS_AVAILABLE
    if (completionBlock_) {
      completionBlock_(self, auth, error);

      // release the block here to avoid a retain loop on the controller
      [completionBlock_ autorelease];
      completionBlock_ = nil;
    }
#endif
  }
}

- (void)moveWebViewFromUnderNavigationBar {
  CGRect dontCare;
  CGRect webFrame = self.view.bounds;
  UINavigationBar *navigationBar = self.navigationController.navigationBar;
  CGRectDivide(webFrame, &dontCare, &webFrame,
    navigationBar.frame.size.height, CGRectMinYEdge);
  [self.webView setFrame:webFrame];
}

// isTranslucent is defined in iPhoneOS 3.0 on.
- (BOOL)isNavigationBarTranslucent {
  UINavigationBar *navigationBar = [[self navigationController] navigationBar];
  BOOL isTranslucent =
    ([navigationBar respondsToSelector:@selector(isTranslucent)] &&
     [navigationBar isTranslucent]);
  return isTranslucent;
}

#pragma mark -
#pragma mark Protocol implementations

- (void)viewWillAppear:(BOOL)animated {
  if (!isViewShown_) {
    isViewShown_ = YES;
    if ([self isNavigationBarTranslucent]) {
      [self moveWebViewFromUnderNavigationBar];
    }
    if (![signIn_ startSigningIn]) {
      // Can't start signing in. We must pop our view.
      // UIWebview needs time to stabilize. Animations need time to complete.
      // We remove ourself from the view stack after that.
      [self performSelector:@selector(popView)
                 withObject:nil
                 afterDelay:0.5
                    inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
    }
  }
  [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated {
  didViewAppear_ = YES;
  [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated {
  if (!didDismissSelf_) {
    // We won't receive further webview delegate messages, so be sure the
    // started loading notification is balanced, if necessary
    [self notifyWithName:kGTMOAuth2WebViewStoppedLoading
                 webView:self.webView
                    kind:kGTMOAuth2WebViewCancelled];

    // We are not popping ourselves, so presumably we are being popped by the
    // navigation controller; tell the sign-in object to close up shop
    //
    // this will indirectly call our signIn:finishedWithAuth:error: method
    // for us
    [signIn_ windowWasClosed];
  }

  // prevent the next sign-in from showing in the WebView that the user is
  // already signed in
  [self clearBrowserCookies];

  [super viewWillDisappear:animated];
}

- (BOOL)webView:(UIWebView *)webView
  shouldStartLoadWithRequest:(NSURLRequest *)request
              navigationType:(UIWebViewNavigationType)navigationType {

  if (!hasDoneFinalRedirect_) {
    hasDoneFinalRedirect_ = [signIn_ requestRedirectedToRequest:request];
    if (hasDoneFinalRedirect_) {
      // signIn has told the view to close
      return NO;
    }
  }
  return YES;
}

- (void)updateUI {
  [backButton_ setEnabled:[[self webView] canGoBack]];
  [forwardButton_ setEnabled:[[self webView] canGoForward]];
}

- (void)webViewDidStartLoad:(UIWebView *)webView {
  [self notifyWithName:kGTMOAuth2WebViewStartedLoading
               webView:webView
                  kind:nil];
  [self updateUI];
}

- (void)webViewDidFinishLoad:(UIWebView *)webView {
  [self notifyWithName:kGTMOAuth2WebViewStoppedLoading
               webView:webView
                  kind:kGTMOAuth2WebViewFinished];

  NSString *title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
  if ([title length] > 0) {
    [signIn_ titleChanged:title];
  } else {
#if DEBUG
    // Verify that Javascript is enabled
    NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"1+1"];
    NSAssert([result integerValue] == 2, @"GTMOAuth2: Javascript is required");
#endif
  }

  [signIn_ cookiesChanged:[NSHTTPCookieStorage sharedHTTPCookieStorage]];

  [self updateUI];
}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
  [self notifyWithName:kGTMOAuth2WebViewStoppedLoading
               webView:webView
                  kind:kGTMOAuth2WebViewFailed];

  // Tell the sign-in object that a load failed; if it was the authorization
  // URL, it will pop the view and return an error to the delegate.
  if (didViewAppear_) {
    BOOL isUserInterruption = ([error code] == NSURLErrorCancelled
                               && [[error domain] isEqual:NSURLErrorDomain]);
    if (isUserInterruption) {
      // Ignore this error:
      // Users report that this error occurs when clicking too quickly on the
      // accept button, before the page has completely loaded.  Ignoring
      // this error seems to provide a better experience than does immediately
      // cancelling sign-in.
      //
      // This error also occurs whenever UIWebView is sent the stopLoading
      // message, so if we ever send that message intentionally, we need to
      // revisit this bypass.
      return;
    }

    [signIn_ loadFailedWithError:error];
  } else {
    // UIWebview needs time to stabilize. Animations need time to complete.
    [signIn_ performSelector:@selector(loadFailedWithError:)
                  withObject:error
                  afterDelay:0.5
                     inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
  }
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
  BOOL value = YES;
  if (!isInsideShouldAutorotateToInterfaceOrientation_) {
    isInsideShouldAutorotateToInterfaceOrientation_ = YES;
    UIViewController *navigationController = [self navigationController];
    if (navigationController != nil) {
      value = [navigationController shouldAutorotateToInterfaceOrientation:interfaceOrientation];
    } else {
      value = [super shouldAutorotateToInterfaceOrientation:interfaceOrientation];
    }
    isInsideShouldAutorotateToInterfaceOrientation_ = NO;
  }
  return value;
}

@end


#pragma mark Common Code

@implementation GTMOAuth2Keychain

+ (GTMOAuth2Keychain *)defaultKeychain {
  if (sDefaultKeychain == nil) {
    sDefaultKeychain = [[self alloc] init];
  }
  return sDefaultKeychain;
}


// For unit tests: allow setting a mock object
+ (void)setDefaultKeychain:(GTMOAuth2Keychain *)keychain {
  if (sDefaultKeychain != keychain) {
    [sDefaultKeychain release];
    sDefaultKeychain = [keychain retain];
  }
}

- (NSString *)keyForService:(NSString *)service account:(NSString *)account {
  return [NSString stringWithFormat:@"com.google.GTMOAuth.%@%@", service, account];
}

// The Keychain API isn't available on the iPhone simulator in SDKs before 3.0,
// so, on early simulators we use a fake API, that just writes, unencrypted, to
// NSUserDefaults.
#if TARGET_IPHONE_SIMULATOR && __IPHONE_OS_VERSION_MAX_ALLOWED < 30000
#pragma mark Simulator

// Simulator - just simulated, not secure.
- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
  NSString *result = nil;
  if (0 < [service length] && 0 < [account length]) {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *key = [self keyForService:service account:account];
    result = [defaults stringForKey:key];
    if (result == nil && error != NULL) {
      *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
                                   code:kGTMOAuth2KeychainErrorNoPassword
                               userInfo:nil];
    }
  } else if (error != NULL) {
    *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
                                 code:kGTMOAuth2KeychainErrorBadArguments
                             userInfo:nil];
  }
  return result;

}


// Simulator - just simulated, not secure.
- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
  BOOL didSucceed = NO;
  if (0 < [service length] && 0 < [account length]) {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *key = [self keyForService:service account:account];
    [defaults removeObjectForKey:key];
    [defaults synchronize];
  } else if (error != NULL) {
    *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
                                 code:kGTMOAuth2KeychainErrorBadArguments
                             userInfo:nil];
  }
  return didSucceed;
}

// Simulator - just simulated, not secure.
- (BOOL)setPassword:(NSString *)password
         forService:(NSString *)service
      accessibility:(CFTypeRef)accessibility
            account:(NSString *)account
              error:(NSError **)error {
  BOOL didSucceed = NO;
  if (0 < [password length] && 0 < [service length] && 0 < [account length]) {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *key = [self keyForService:service account:account];
    [defaults setObject:password forKey:key];
    [defaults synchronize];
    didSucceed = YES;
  } else if (error != NULL) {
    *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
                                 code:kGTMOAuth2KeychainErrorBadArguments
                             userInfo:nil];
  }
  return didSucceed;
}

#else // ! TARGET_IPHONE_SIMULATOR
#pragma mark Device

+ (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
  NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                         (id)kSecClassGenericPassword, (id)kSecClass,
                         @"OAuth", (id)kSecAttrGeneric,
                         account, (id)kSecAttrAccount,
                         service, (id)kSecAttrService,
                         nil];
  return query;
}

- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
  return [[self class] keychainQueryForService:service account:account];
}



// iPhone
- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
  OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
  NSString *result = nil;
  if (0 < [service length] && 0 < [account length]) {
    CFDataRef passwordData = NULL;
    NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];

    status = SecItemCopyMatching((CFDictionaryRef)keychainQuery,
                                       (CFTypeRef *)&passwordData);
    if (status == noErr && 0 < [(NSData *)passwordData length]) {
      result = [[[NSString alloc] initWithData:(NSData *)passwordData
                                      encoding:NSUTF8StringEncoding] autorelease];
    }
    if (passwordData != NULL) {
      CFRelease(passwordData);
    }
  }
  if (status != noErr && error != NULL) {
    *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
                                 code:status
                             userInfo:nil];
  }
  return result;
}


// iPhone
- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
  OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
  if (0 < [service length] && 0 < [account length]) {
    NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
    status = SecItemDelete((CFDictionaryRef)keychainQuery);
  }
  if (status != noErr && error != NULL) {
    *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
                                 code:status
                             userInfo:nil];
  }
  return status == noErr;
}

// iPhone
- (BOOL)setPassword:(NSString *)password
         forService:(NSString *)service
      accessibility:(CFTypeRef)accessibility
            account:(NSString *)account
              error:(NSError **)error {
  OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
  if (0 < [service length] && 0 < [account length]) {
    [self removePasswordForService:service account:account error:nil];
    if (0 < [password length]) {
      NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
      NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
      [keychainQuery setObject:passwordData forKey:(id)kSecValueData];

      if (accessibility != NULL && &kSecAttrAccessible != NULL) {
        [keychainQuery setObject:(id)accessibility
                          forKey:(id)kSecAttrAccessible];
      }
      status = SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
    }
  }
  if (status != noErr && error != NULL) {
    *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
                                 code:status
                             userInfo:nil];
  }
  return status == noErr;
}

#endif // ! TARGET_IPHONE_SIMULATOR

@end

#endif // TARGET_OS_IPHONE

#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
