swift: Fixes to enable dice roller to work, plus logout

Until merge is working, uses a scheme whereby each device has its
own unique collection name but updates the UI on any dice collection
change. While each device has its own collection, the UI looks as if
it has just the one, thereby achieving the desired UX.

NOTE: This will quickly change as we'll be using the userdata
collection instead, but that work will come in another CL. This is
more of a transition CL.

Logout is implemented for dice roller.

This CL contains the following other fixes for this example to work:

- Make User a class & hashable so it may be stored in a Set/Map & used
  as a segue sender (via AnyObject cast)
- Allow stopScan to work even if not yet init/logged in
- LoginCallback uses SyncbaseError instead of ErrorType
- Syncbase.configure's default for rootDir is the corrected
  defaultRootDir variable
- Added NoAccess exception, used for knowing when to delete the
  Syncbase rootDir after logging out and back in as a different user
- Rename the typo defaultSyncbasePerms -> defaultSyncgroupPerms

Change-Id: Ia2856eb8426ce86ffb27f1eb8f41b9e325371980
diff --git a/Syncbase/Example/Dice Roller/AppDelegate.swift b/Syncbase/Example/Dice Roller/AppDelegate.swift
index ee13229..f7a84bb 100644
--- a/Syncbase/Example/Dice Roller/AppDelegate.swift
+++ b/Syncbase/Example/Dice Roller/AppDelegate.swift
@@ -11,20 +11,27 @@
   var window: UIWindow?
 
   func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
-    // Configure Google Sign-In
+    AppDelegate.configureGoogleSignIn()
+    AppDelegate.configureSyncbase()
+    return true
+  }
+
+  static var googleSignInClientID: String {
     let infoPath = NSBundle.mainBundle().pathForResource("GoogleService-Info", ofType: "plist")!
     let info = NSDictionary(contentsOfFile: infoPath)!
-    let clientID = info["CLIENT_ID"] as! String
-    GIDSignIn.sharedInstance().clientID = clientID
+    return info["CLIENT_ID"] as! String
+  }
 
-    // Configure Syncbase
+  static func configureGoogleSignIn() {
+    GIDSignIn.sharedInstance().clientID = AppDelegate.googleSignInClientID
+  }
+
+  static func configureSyncbase() {
     try! Syncbase.configure(adminUserId: "zinman@google.com",
       // Craft a blessing prefix using google sign-in and the dev.v.io blessings provider.
-      defaultBlessingStringPrefix: "dev.v.io:o:\(clientID):",
+      defaultBlessingStringPrefix: "dev.v.io:o:\(AppDelegate.googleSignInClientID):",
       // Cloud mount-point.
-      // TODO(zinman): Determine if this is correct.
       mountPoints: ["/ns.dev.v.io:8101/tmp/ios/diceroller/users/"])
-    return true
   }
 
   func application(app: UIApplication, openURL url: NSURL, options: [String: AnyObject]) -> Bool {
diff --git a/Syncbase/Example/Dice Roller/Base.lproj/Main.storyboard b/Syncbase/Example/Dice Roller/Base.lproj/Main.storyboard
index 905db02..17dee1d 100644
--- a/Syncbase/Example/Dice Roller/Base.lproj/Main.storyboard
+++ b/Syncbase/Example/Dice Roller/Base.lproj/Main.storyboard
@@ -48,12 +48,28 @@
                     <connections>
                         <outlet property="signInButton" destination="jVb-kU-JqR" id="caw-1r-K5Z"/>
                         <outlet property="spinner" destination="QOU-JV-r2d" id="Wj7-EZ-8Jr"/>
-                        <segue destination="70a-ZD-vEg" kind="showDetail" identifier="LoggedInSegue" id="UW0-Zc-4i8"/>
+                        <segue destination="KPY-km-KOL" kind="showDetail" identifier="LoggedInSegue" id="Kl1-6W-rp8"/>
                     </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
             </objects>
-            <point key="canvasLocation" x="53" y="375"/>
+            <point key="canvasLocation" x="-283" y="391"/>
+        </scene>
+        <!--Navigation Controller-->
+        <scene sceneID="nwv-Br-yC5">
+            <objects>
+                <navigationController id="KPY-km-KOL" sceneMemberID="viewController">
+                    <navigationBar key="navigationBar" contentMode="scaleToFill" id="oh2-bt-Q2o">
+                        <rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </navigationBar>
+                    <connections>
+                        <segue destination="70a-ZD-vEg" kind="relationship" relationship="rootViewController" id="dfS-gp-ION"/>
+                    </connections>
+                </navigationController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="ZJK-Xx-84S" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="569" y="391"/>
         </scene>
         <!--Dice Roller-->
         <scene sceneID="bSh-d5-IOu">
@@ -99,13 +115,21 @@
                             <constraint firstItem="APD-dz-sU0" firstAttribute="centerX" secondItem="Ksc-lJ-zhb" secondAttribute="centerX" id="z53-sC-Pvb"/>
                         </constraints>
                     </view>
+                    <navigationItem key="navigationItem" id="4Vi-Tj-k1m">
+                        <barButtonItem key="leftBarButtonItem" title="Logout" id="QPP-VQ-J0Q">
+                            <connections>
+                                <action selector="didPressLogout:" destination="70a-ZD-vEg" id="2C8-a5-nuJ"/>
+                            </connections>
+                        </barButtonItem>
+                    </navigationItem>
                     <connections>
                         <outlet property="numberLabel" destination="180-RC-nik" id="nyZ-Kj-0LD"/>
+                        <segue destination="01J-lp-oVM" kind="showDetail" identifier="LogoutSegue" id="fOL-0c-Tbh"/>
                     </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="2Kd-Zv-Yw1" userLabel="First Responder" sceneMemberID="firstResponder"/>
             </objects>
-            <point key="canvasLocation" x="803" y="375"/>
+            <point key="canvasLocation" x="1497" y="391"/>
         </scene>
     </scenes>
     <resources>
diff --git a/Syncbase/Example/Dice Roller/DiceViewController.swift b/Syncbase/Example/Dice Roller/DiceViewController.swift
index ddff770..f0b7155 100644
--- a/Syncbase/Example/Dice Roller/DiceViewController.swift
+++ b/Syncbase/Example/Dice Roller/DiceViewController.swift
@@ -5,28 +5,52 @@
 import Syncbase
 import UIKit
 
-let collectionName = "dice"
-let rowKey = " result"
+let deviceUUID = UIDevice.currentDevice().identifierForVendor ?? NSUUID()
+let collectionName = "dice_\(deviceUUID.UUIDString.stringByReplacingOccurrencesOfString("-", withString: ""))"
+let rowKey = "result"
 
 class DiceViewController: UIViewController {
   @IBOutlet weak var numberLabel: UILabel!
+  var collection: Collection?
 
   override func viewDidLoad() {
     super.viewDidLoad()
+
+    do {
+      collection = try Syncbase.database().collection(collectionName)
+    } catch let e {
+      print("Unexpected error: \(e)")
+    }
+  }
+
+  override func viewWillAppear(animated: Bool) {
+    super.viewWillAppear(animated)
     do {
       try Syncbase.database().addWatchChangeHandler(
-        pattern: CollectionRowPattern(collectionName: collectionName, rowKey: rowKey),
+        pattern: CollectionRowPattern(rowKey: rowKey),
         handler: WatchChangeHandler(
           onInitialState: onWatchChanges,
           onChangeBatch: onWatchChanges,
           onError: onWatchError))
     } catch let e {
-      print("Unable to start watch handler: \(e)")
+      print("Unexpected error: \(e)")
+    }
+  }
+
+  override func viewWillDisappear(animated: Bool) {
+    super.viewWillDisappear(animated)
+    do {
+      try Syncbase.database().removeAllWatchChangeHandlers()
+    } catch let e {
+      print("Unexpected error: \(e)")
     }
   }
 
   func onWatchChanges(changes: [WatchChange]) {
     let lastValue = changes
+    // Only look at the prefix so that different devices (with different collection names)
+    // are examined for their values.
+    .filter { $0.collectionId?.name.hasPrefix("dice") ?? false }
       .filter { $0.entityType == .Row && $0.changeType == .Put }
       .last?
       .value
@@ -50,10 +74,20 @@
     // it to work properly.
     let value = NSData(bytes: &nextNum, length: 1)
     do {
-      try Syncbase.database().collection(collectionName).put(rowKey, value: value)
+      try collection?.put(rowKey, value: value)
     } catch let e {
       print("Unexpected error: \(e)")
     }
   }
+
+  @IBAction func didPressLogout(sender: UIBarButtonItem) {
+    performSegueWithIdentifier("LogoutSegue", sender: self)
+  }
+
+  override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+    if let loginVC = segue.destinationViewController as? LoginViewController {
+      loginVC.doLogout = true
+    }
+  }
 }
 
diff --git a/Syncbase/Example/Dice Roller/LoginViewController.swift b/Syncbase/Example/Dice Roller/LoginViewController.swift
index 4a8c9ad..84d5879 100644
--- a/Syncbase/Example/Dice Roller/LoginViewController.swift
+++ b/Syncbase/Example/Dice Roller/LoginViewController.swift
@@ -9,6 +9,7 @@
 class LoginViewController: UIViewController, GIDSignInUIDelegate {
   @IBOutlet weak var signInButton: GIDSignInButton!
   @IBOutlet weak var spinner: UIActivityIndicatorView!
+  var doLogout: Bool = false
 
   override func viewDidLoad() {
     super.viewDidLoad()
@@ -16,6 +17,33 @@
     GIDSignIn.sharedInstance().uiDelegate = self
     signInButton.colorScheme = .Light
     spinner.alpha = 0
+    signInButton.alpha = 0
+
+    if doLogout {
+      // Coming back from the game picker -- log out as the current user and reset the world.
+      logout()
+    } else {
+      // Normal case.
+      attemptAutomaticLogin()
+    }
+  }
+
+  func logout() {
+    doLogout = false
+    showSpinner()
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
+      Syncbase.shutdown()
+      // Because the user might sign in with a different user, we must shutdown Syncbase and
+      // delete it's root directory, otherwise the existing credentials will be invalid.
+      // TODO(zinman): Remove this once https://github.com/vanadium/issues/issues/1381 is fixed.
+      try! NSFileManager.defaultManager().removeItemAtPath(Syncbase.defaultRootDir)
+      AppDelegate.configureSyncbase()
+      // This will call back via the delegate below -- it will redirect to didDisconnectForLogout.
+      GIDSignIn.sharedInstance().disconnect()
+    }
+  }
+
+  func attemptAutomaticLogin() {
     // Advance if already logged in or we have exchangable oauth keys.
     do {
       if try Syncbase.isLoggedIn() {
@@ -28,6 +56,8 @@
     if GIDSignIn.sharedInstance().hasAuthInKeychain() {
       showSpinner()
       GIDSignIn.sharedInstance().signInSilently()
+    } else {
+      showSignInButton()
     }
   }
 
@@ -79,10 +109,29 @@
           self.didSignIn()
         } else {
           print("Unable to login to Syncbase: \(err) ")
+          switch err! {
+          case .NoAccess:
+            // We signed in with a different user -- our credentials are now invalid. We must
+            // delete the Syncbase database and retry.
+            self.logout()
+          default:
+            // Delete the credentials so the user has the chance to sign-in again.
+            GIDSignIn.sharedInstance().disconnect()
+          }
           self.showSignInButton()
           self.showErrorMsg("Unable to login to Syncbase. Try again.")
         }
       }
     }
   }
+
+  func signIn(signIn: GIDSignIn!, didDisconnectWithUser user: GIDGoogleUser!, withError error: NSError!) {
+    dispatch_async(dispatch_get_main_queue()) {
+      self.showSignInButton()
+      if error != nil {
+        print("Couldn't logout: \(error)")
+        self.showErrorMsg("Unable to logout of Syncbase. Try again.")
+      }
+    }
+  }
 }
\ No newline at end of file
diff --git a/Syncbase/Source/Blessing.swift b/Syncbase/Source/Blessing.swift
index ff3c1e7..1a05607 100644
--- a/Syncbase/Source/Blessing.swift
+++ b/Syncbase/Source/Blessing.swift
@@ -55,7 +55,7 @@
     Tags.Admin.rawValue: selfAndCloud]
 }
 
-func defaultSyncbasePerms() throws -> SyncbaseCore.Permissions {
+func defaultSyncgroupPerms() throws -> SyncbaseCore.Permissions {
   let selfAndCloud = try selfAndCloudAL()
   return [
     Tags.Read.rawValue: selfAndCloud,
diff --git a/Syncbase/Source/Error.swift b/Syncbase/Source/Error.swift
index bd2e716..7c9d998 100644
--- a/Syncbase/Source/Error.swift
+++ b/Syncbase/Source/Error.swift
@@ -31,6 +31,7 @@
   case InvalidUTF8(invalidUtf8: String)
   case CastError(obj: Any)
   case IllegalArgument(detail: String)
+  case NoAccess(detail: String)
   case UnknownVError(err: VError)
 
   init(coreError: SyncbaseCore.SyncbaseError) {
@@ -56,6 +57,7 @@
     case .InvalidUTF8(let invalidUtf8): self = .InvalidUTF8(invalidUtf8: invalidUtf8)
     case .CastError(let obj): self = .CastError(obj: obj)
     case .IllegalArgument(let detail): self = .IllegalArgument(detail: detail)
+    case .NoAccess(let detail): self = .NoAccess(detail: detail)
     case .UnknownVError(let err): self = .UnknownVError(err: err)
     }
   }
@@ -102,6 +104,7 @@
     case .InvalidUTF8(let invalidUtf8): return "Unable to convert to utf8: \(invalidUtf8)"
     case .CastError(let obj): return "Unable to convert to cast: \(obj)"
     case .IllegalArgument(let detail): return "Illegal argument: \(detail)"
+    case .NoAccess(let detail): return "Access Denied: \(detail)"
     case .UnknownVError(let err): return "Unknown error: \(err)"
     }
   }
diff --git a/Syncbase/Source/Syncbase.swift b/Syncbase/Source/Syncbase.swift
index 0632fd8..2332590 100644
--- a/Syncbase/Source/Syncbase.swift
+++ b/Syncbase/Source/Syncbase.swift
@@ -43,7 +43,7 @@
     return NSFileManager.defaultManager()
       .URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
       .URLByAppendingPathComponent("Syncbase")
-      .absoluteString
+      .path!
   }
 
   static var publishSyncbaseName: String? {
@@ -75,10 +75,7 @@
   public static func configure(
     adminUserId adminUserId: String,
     // Default to Application Support/Syncbase.
-    rootDir: String = NSFileManager.defaultManager()
-      .URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
-      .URLByAppendingPathComponent("Syncbase")
-      .path!,
+    rootDir: String = defaultRootDir,
     mountPoints: [String] = ["/ns.dev.v.io:8101/tmp/todos/users/"],
     defaultBlessingStringPrefix: String = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com:",
     disableSyncgroupPublishing: Bool = false,
@@ -192,7 +189,7 @@
     return db
   }
 
-  public typealias LoginCallback = (err: ErrorType?) -> Void
+  public typealias LoginCallback = (err: SyncbaseError?) -> Void
 
   /// Authorize using an oauth token. Right now only Google OAuth token is supported
   /// (you should use the Google Sign In SDK to get this).
@@ -206,21 +203,19 @@
       SyncbaseCore.GoogleOAuthCredentials(token: credentials.token),
       callback: { err in
         guard err == nil else {
-          if let e = err as? SyncbaseCore.SyncbaseError {
-            callback(err: SyncbaseError(coreError: e))
-          } else {
-            callback(err: err)
-          }
+          callback(err: SyncbaseError(coreError: err!))
           return
         }
         // postLoginCreateDefaults can be blocking when performing create-or-join. Run on
         // a background queue to prevent blocking from the Go callback.
         dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
-          var callbackErr: ErrorType?
+          var callbackErr: SyncbaseError?
           do {
             try postLoginCreateDefaults()
-          } catch let e {
+          } catch let e as SyncbaseError {
             callbackErr = e
+          } catch {
+            preconditionFailure("Unsupported ErrorType: \(error)")
           }
           dispatch_async(Syncbase.queue) {
             callback(err: callbackErr)
diff --git a/Syncbase/Source/Syncgroup.swift b/Syncbase/Source/Syncgroup.swift
index 4aeb837..78c4483 100644
--- a/Syncbase/Source/Syncgroup.swift
+++ b/Syncbase/Source/Syncgroup.swift
@@ -21,7 +21,7 @@
     let spec = SyncgroupSpec(
       description: "",
       collections: cxCoreIds,
-      permissions: try defaultSyncbasePerms(),
+      permissions: try defaultSyncgroupPerms(),
       publishSyncbaseName: Syncbase.publishSyncbaseName,
       mountTables: Syncbase.mountPoints,
       isPrivate: false)
diff --git a/Syncbase/Source/User.swift b/Syncbase/Source/User.swift
index 47b91bb..ffd72ba 100644
--- a/Syncbase/Source/User.swift
+++ b/Syncbase/Source/User.swift
@@ -6,6 +6,18 @@
 import SyncbaseCore
 
 /// Represents a user.
-public struct User {
+public class User: Hashable {
   public let alias: String
+
+  public init(alias:String) {
+    self.alias = alias
+  }
+
+  public var hashValue: Int {
+    return alias.hashValue
+  }
+}
+
+public func == (lhs: User, rhs: User) -> Bool {
+  return lhs.alias == rhs.alias
 }
diff --git a/SyncbaseCore/Source/Errors.swift b/SyncbaseCore/Source/Errors.swift
index bab30b7..cb4d63d 100644
--- a/SyncbaseCore/Source/Errors.swift
+++ b/SyncbaseCore/Source/Errors.swift
@@ -26,6 +26,7 @@
   case InvalidUTF8(invalidUtf8: String)
   case CastError(obj: Any)
   case IllegalArgument(detail: String)
+  case NoAccess(detail: String)
   case UnknownVError(err: VError)
 
   init(_ err: VError) {
@@ -45,6 +46,7 @@
     case "v.io/v23/services/syncbase.InvalidPermissionsChange": self = SyncbaseError.InvalidPermissionsChange
     case "v.io/v23/verror.Exist": self = SyncbaseError.Exist
     case "v.io/v23/verror.NoExist": self = SyncbaseError.NoExist
+    case "v.io/v23/verror.NoAccess": self = SyncbaseError.NoAccess(detail: err.msg)
     default: self = SyncbaseError.UnknownVError(err: err)
     }
   }
@@ -73,6 +75,7 @@
     case .InvalidUTF8(let invalidUtf8): return "Unable to convert to UTF-8: \(invalidUtf8)"
     case .CastError(let obj): return "Unable to convert to cast: \(obj)"
     case .IllegalArgument(let detail): return "Illegal argument: \(detail)"
+    case .NoAccess(let detail): return "Access Denied: \(detail)"
     case .UnknownVError(let err): return "Unknown error: \(err)"
     }
   }
diff --git a/SyncbaseCore/Source/Neighborhood.swift b/SyncbaseCore/Source/Neighborhood.swift
index 2fbe7f2..4247a58 100644
--- a/SyncbaseCore/Source/Neighborhood.swift
+++ b/SyncbaseCore/Source/Neighborhood.swift
@@ -94,9 +94,6 @@
   }
 
   public static func stopScan(handler: NeighborhoodScanHandler) {
-    if !Syncbase.didInit || !Syncbase.isLoggedIn {
-      return
-    }
     handler.isStoppedMu.lock()
     defer { handler.isStoppedMu.unlock() }
     if handler.isStopped {
diff --git a/SyncbaseCore/Source/Syncbase.swift b/SyncbaseCore/Source/Syncbase.swift
index 25a151e..62ba3aa 100644
--- a/SyncbaseCore/Source/Syncbase.swift
+++ b/SyncbaseCore/Source/Syncbase.swift
@@ -97,7 +97,7 @@
     return Principal.blessingsDebugDescription
   }
 
-  public typealias LoginCallback = (err: ErrorType?) -> Void
+  public typealias LoginCallback = (err: SyncbaseError?) -> Void
 
   /// Authorize using an oauth token. Right now only Google OAuth token is supported
   /// (you should use the Google Sign In SDK to get this), and you should use the
@@ -115,7 +115,7 @@
     }
     // Go's login is blocking, so call on a background concurrent queue.
     dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
-      var err: ErrorType? = nil
+      var err: SyncbaseError? = nil
       do {
         try VError.maybeThrow { errPtr in
           v23_syncbase_Login(
@@ -123,8 +123,10 @@
             try credentials.token.toCgoString(),
             errPtr)
         }
-      } catch (let e) {
+      } catch let e as SyncbaseError {
         err = e
+      } catch {
+        preconditionFailure("Invalid ErrorType: \(error)")
       }
       dispatch_async(Syncbase.queue) {
         callback(err: err)