From b472c85c9d13b9f141f51b14bcfe0ab5d5a2090e Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Wed, 6 Jun 2012 22:38:43 +0200 Subject: [PATCH] Avatar display fixes. [ADDED] A new password type: Secure password. 20 characters, not word-based, very high entropy. [FIXED] UI bugs and improvements with the avatar display and password checking state display. --- External/Pearl | 2 +- MasterPassword/MPAppDelegate_Key.m | 1 + MasterPassword/MPTypes.h | 11 +- MasterPassword/MPTypes.m | 12 +- .../MasterPassword.xcdatamodel/contents | 12 +- MasterPassword/iOS/MPAppDelegate.m | 8 +- .../iOS/MPPreferencesViewController.m | 2 + MasterPassword/iOS/MPSearchDelegate.m | 2 +- MasterPassword/iOS/MPTypeViewController.m | 16 +- MasterPassword/iOS/MPUnlockViewController.h | 2 + MasterPassword/iOS/MPUnlockViewController.m | 99 +++++---- .../iOS/MainStoryboard_iPhone.storyboard | 195 ++++++++++++------ 12 files changed, 224 insertions(+), 138 deletions(-) diff --git a/External/Pearl b/External/Pearl index e9dd56bc..3a61ba22 160000 --- a/External/Pearl +++ b/External/Pearl @@ -1 +1 @@ -Subproject commit e9dd56bc64735ebd28eef6c6dc748f480acc7c67 +Subproject commit 3a61ba22afefb23c2f87a3836e8b996d2b5a96fb diff --git a/MasterPassword/MPAppDelegate_Key.m b/MasterPassword/MPAppDelegate_Key.m index e9df9faa..6c1f43c6 100644 --- a/MasterPassword/MPAppDelegate_Key.m +++ b/MasterPassword/MPAppDelegate_Key.m @@ -81,6 +81,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) { user.keyID = tryKeyID; [[MPAppDelegate_Shared get] saveContext]; } + user.lastUsed = [NSDate date]; #ifdef TESTFLIGHT_SDK_VERSION [TestFlight passCheckpoint:MPTestFlightCheckpointMPEntered]; diff --git a/MasterPassword/MPTypes.h b/MasterPassword/MPTypes.h index 24b4f36c..f2b9aab2 100644 --- a/MasterPassword/MPTypes.h +++ b/MasterPassword/MPTypes.h @@ -31,11 +31,12 @@ typedef enum { } MPElementFeature; typedef enum { - MPElementTypeGeneratedLong = 0x0 | MPElementTypeClassGenerated | 0x0, - MPElementTypeGeneratedMedium = 0x1 | MPElementTypeClassGenerated | 0x0, - MPElementTypeGeneratedShort = 0x2 | MPElementTypeClassGenerated | 0x0, - MPElementTypeGeneratedBasic = 0x3 | MPElementTypeClassGenerated | 0x0, - MPElementTypeGeneratedPIN = 0x4 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedSecure = 0x0 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedLong = 0x1 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedMedium = 0x2 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedShort = 0x3 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedBasic = 0x4 | MPElementTypeClassGenerated | 0x0, + MPElementTypeGeneratedPIN = 0x5 | MPElementTypeClassGenerated | 0x0, MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent, MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate, diff --git a/MasterPassword/MPTypes.m b/MasterPassword/MPTypes.m index a211aedc..28153baa 100644 --- a/MasterPassword/MPTypes.m +++ b/MasterPassword/MPTypes.m @@ -40,9 +40,12 @@ NSString *NSStringFromMPElementType(MPElementType type) { return nil; switch (type) { + case MPElementTypeGeneratedSecure: + return @"Secure Password"; + case MPElementTypeGeneratedLong: return @"Long Password"; - + case MPElementTypeGeneratedMedium: return @"Medium Password"; @@ -72,9 +75,12 @@ Class ClassFromMPElementType(MPElementType type) { return nil; switch (type) { - case MPElementTypeGeneratedLong: + case MPElementTypeGeneratedSecure: return [MPElementGeneratedEntity class]; + case MPElementTypeGeneratedLong: + return [MPElementGeneratedEntity class]; + case MPElementTypeGeneratedMedium: return [MPElementGeneratedEntity class]; @@ -135,7 +141,7 @@ NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, ui [name dataUsingEncoding:NSUTF8StringEncoding], key, [NSData dataWithBytes:&nsalt length:sizeof(nsalt)], - nil] hashWith:PearlDigestSHA1]; + nil] hashWith:PearlDigestSHA256]; trc(@"seed is: %@", seed); const char *seedBytes = seed.bytes; diff --git a/MasterPassword/MasterPassword.xcdatamodeld/MasterPassword.xcdatamodel/contents b/MasterPassword/MasterPassword.xcdatamodeld/MasterPassword.xcdatamodel/contents index fa986b6f..9e3fd78f 100644 --- a/MasterPassword/MasterPassword.xcdatamodeld/MasterPassword.xcdatamodel/contents +++ b/MasterPassword/MasterPassword.xcdatamodeld/MasterPassword.xcdatamodel/contents @@ -4,7 +4,7 @@ - + @@ -15,17 +15,17 @@ - + - - + + - + - + \ No newline at end of file diff --git a/MasterPassword/iOS/MPAppDelegate.m b/MasterPassword/iOS/MPAppDelegate.m index cccd5261..66e0f0f5 100644 --- a/MasterPassword/iOS/MPAppDelegate.m +++ b/MasterPassword/iOS/MPAppDelegate.m @@ -516,7 +516,7 @@ - (NSString *)testFlightToken { - return NullToNil([[self testFlightInfo] valueForKeyPath:@"Team Token"]); + return NSNullToNil([[self testFlightInfo] valueForKeyPath:@"Team Token"]); } @@ -535,7 +535,7 @@ - (NSString *)crashlyticsAPIKey { - return NullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]); + return NSNullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]); } @@ -554,7 +554,7 @@ - (NSString *)apptentiveAPIKey { - return NullToNil([[self apptentiveInfo] valueForKeyPath:@"API Key"]); + return NSNullToNil([[self apptentiveInfo] valueForKeyPath:@"API Key"]); } @@ -574,7 +574,7 @@ - (NSString *)localyticsKey { #ifdef DEBUG - return NullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]); + return NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]); #else return NullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]); #endif diff --git a/MasterPassword/iOS/MPPreferencesViewController.m b/MasterPassword/iOS/MPPreferencesViewController.m index 4c89c728..cdfbf71b 100644 --- a/MasterPassword/iOS/MPPreferencesViewController.m +++ b/MasterPassword/iOS/MPPreferencesViewController.m @@ -98,6 +98,8 @@ else if (cell == self.changeMPCell) [[MPAppDelegate get] changeMP]; + + [tableView deselectRowAtIndexPath:indexPath animated:YES]; } #pragma mark - IASKSettingsDelegate diff --git a/MasterPassword/iOS/MPSearchDelegate.m b/MasterPassword/iOS/MPSearchDelegate.m index 7b71e7c0..28bf5414 100644 --- a/MasterPassword/iOS/MPSearchDelegate.m +++ b/MasterPassword/iOS/MPSearchDelegate.m @@ -131,7 +131,7 @@ assert(self.query); self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@", - self.query, self.query, NilToNull([MPAppDelegate get].activeUser)]; + self.query, self.query, NilToNSNull([MPAppDelegate get].activeUser)]; NSError *error; if (![self.fetchedResultsController performFetch:&error]) diff --git a/MasterPassword/iOS/MPTypeViewController.m b/MasterPassword/iOS/MPTypeViewController.m index 8bd82627..8e1ec781 100644 --- a/MasterPassword/iOS/MPTypeViewController.m +++ b/MasterPassword/iOS/MPTypeViewController.m @@ -90,14 +90,18 @@ // Generated switch (indexPath.row) { case 0: - return MPElementTypeGeneratedLong; + return NSNotFound; case 1: - return MPElementTypeGeneratedMedium; + return MPElementTypeGeneratedSecure; case 2: - return MPElementTypeGeneratedShort; + return MPElementTypeGeneratedLong; case 3: - return MPElementTypeGeneratedBasic; + return MPElementTypeGeneratedMedium; case 4: + return MPElementTypeGeneratedShort; + case 5: + return MPElementTypeGeneratedBasic; + case 6: return MPElementTypeGeneratedPIN; default: @@ -109,8 +113,10 @@ // Stored switch (indexPath.row) { case 0: - return MPElementTypeStoredPersonal; + return NSNotFound; case 1: + return MPElementTypeStoredPersonal; + case 2: return MPElementTypeStoredDevicePrivate; default: diff --git a/MasterPassword/iOS/MPUnlockViewController.h b/MasterPassword/iOS/MPUnlockViewController.h index b7c89187..ac924e30 100644 --- a/MasterPassword/iOS/MPUnlockViewController.h +++ b/MasterPassword/iOS/MPUnlockViewController.h @@ -18,6 +18,8 @@ @property (weak, nonatomic) IBOutlet UILabel *oldNameLabel; @property (weak, nonatomic) IBOutlet UIButton *avatarTemplate; @property (weak, nonatomic) IBOutlet UILabel *deleteTip; +@property (weak, nonatomic) IBOutlet UIView *passwordTipView; +@property (weak, nonatomic) IBOutlet UILabel *passwordTipLabel; @property(nonatomic, strong) UIColor *avatarShadowColor; diff --git a/MasterPassword/iOS/MPUnlockViewController.m b/MasterPassword/iOS/MPUnlockViewController.m index ded748d8..36227759 100644 --- a/MasterPassword/iOS/MPUnlockViewController.m +++ b/MasterPassword/iOS/MPUnlockViewController.m @@ -30,6 +30,8 @@ @synthesize nameLabel, oldNameLabel; @synthesize avatarTemplate; @synthesize deleteTip; +@synthesize passwordTipView; +@synthesize passwordTipLabel; @synthesize avatarShadowColor = _avatarShadowColor; @@ -52,6 +54,7 @@ self.nameLabel.layer.cornerRadius = 5; self.avatarTemplate.hidden = YES; self.spinner.alpha = 0; + self.passwordTipView.alpha = 0; [self updateLayoutAnimated:NO allowScroll:YES completion:nil]; @@ -67,6 +70,8 @@ [self setNameLabel:nil]; [self setAvatarTemplate:nil]; [self setDeleteTip:nil]; + [self setPasswordTipView:nil]; + [self setPasswordTipLabel:nil]; [super viewDidUnload]; } @@ -93,22 +98,23 @@ } - (void)updateUsers { - + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])]; fetchRequest.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO]]; NSArray *users = [[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:nil]; // Clean up avatars. - for (UIView *view in [self.avatarsView subviews]) - if (view != self.avatarTemplate) - [view removeFromSuperview]; + for (UIView *subview in [self.avatarsView subviews]) + if ([[self.avatarToUser allKeys] containsObject:[NSValue valueWithNonretainedObject:subview]]) + // This subview is a former avatar. + [subview removeFromSuperview]; [self.avatarToUser removeAllObjects]; - + // Create avatars. for (MPUserEntity *user in users) [self setupAvatar:[self.avatarTemplate clone] forUser:user]; [self setupAvatar:[self.avatarTemplate clone] forUser:nil]; - + // Scroll view's content changed, update its content size. [self.avatarsView autoSizeContentIgnoreHidden:YES ignoreInvisible:YES limitPadding:NO ignoreSubviews:nil]; @@ -147,11 +153,11 @@ avatar.backgroundColor = [UIColor clearColor]; dbg(@"User: %@, avatar: %d", user.name, user.avatar); + avatar.tag = user.avatar; [avatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%u", user.avatar)] forState:UIControlStateNormal]; - if (user) - [self.avatarToUser setObject:user forKey:[NSValue valueWithNonretainedObject:avatar]]; + [self.avatarToUser setObject:NilToNSNull(user) forKey:[NSValue valueWithNonretainedObject:avatar]]; if (self.selectedUser && user == self.selectedUser) avatar.selected = YES; @@ -165,8 +171,9 @@ [self.passwordField resignFirstResponder]; [self updateLayoutAnimated:YES allowScroll:YES completion:^(BOOL finished) { - if (finished) if (self.selectedUser) - [self.passwordField becomeFirstResponder]; + if (finished) + if (self.selectedUser) + [self.passwordField becomeFirstResponder]; }]; } @@ -197,7 +204,7 @@ } - (void)updateLayoutAnimated:(BOOL)animated allowScroll:(BOOL)allowScroll completion:(void (^)(BOOL finished))completion { - + if (animated) { self.oldNameLabel.text = self.nameLabel.text; self.oldNameLabel.alpha = 1; @@ -232,7 +239,7 @@ self.oldNameLabel.center = self.nameLabel.center; self.avatarShadowColor = [UIColor lightGrayColor]; } - + MPUserEntity *targetedUser = self.selectedUser; UIButton *selectedAvatar = [self avatarForUser:self.selectedUser]; UIButton *targetedAvatar = selectedAvatar; @@ -242,20 +249,26 @@ } [self.avatarsView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) { - const BOOL isTargeted = subview == targetedAvatar; + if (![[self.avatarToUser allKeys] containsObject:[NSValue valueWithNonretainedObject:subview]]) + // This subview is not one of the user avatars. + return; + UIButton *avatar = (UIButton *)subview; + + BOOL isTargeted = avatar == targetedAvatar; + + avatar.userInteractionEnabled = isTargeted; + avatar.alpha = isTargeted ? 1 : self.selectedUser ? 0.1 : 0.4; - subview.userInteractionEnabled = isTargeted; - subview.alpha = isTargeted ? 1 : self.selectedUser ? 0.1 : 0.4; - - [self updateAvatarShadowColor:subview isTargeted:isTargeted]; + [self updateAvatarShadowColor:avatar isTargeted:isTargeted]; } recurse:NO]; if (allowScroll) { - CGPoint targetContentOffset = CGPointMake(targetedAvatar.center.x - self.avatarsView.bounds.size.width / 2, self.avatarsView.contentOffset.y); + CGPoint targetContentOffset = CGPointMake(MAX(0, targetedAvatar.center.x - self.avatarsView.bounds.size.width / 2), + self.avatarsView.contentOffset.y); if (!CGPointEqualToPoint(self.avatarsView.contentOffset, targetContentOffset)) [self.avatarsView setContentOffset:targetContentOffset animated:animated]; } - + self.nameLabel.text = targetedUser ? targetedUser.name : @"New User"; self.nameLabel.bounds = CGRectSetHeight(self.nameLabel.bounds, [self.nameLabel.text sizeWithFont:self.nameLabel.font @@ -266,23 +279,30 @@ completion(YES); } +- (void)setPasswordTip:(NSString *)string { + + if (string.length) + self.passwordTipLabel.text = string; + + [UIView animateWithDuration:0.3f animations:^{ + self.passwordTipView.alpha = string.length? 1: 0; + }]; +} + - (void)tryMasterPassword { [self setSpinnerActive:YES]; - [self changeAvatarShadowColorTo:[UIColor colorWithName:@"lightskyblue"]]; - + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ BOOL unlocked = [[MPAppDelegate get] tryMasterPassword:self.passwordField.text forUser:self.selectedUser]; dispatch_async(dispatch_get_main_queue(), ^{ if (unlocked) { - [self changeAvatarShadowColorTo:[UIColor colorWithName:@"greenyellow"]]; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (long) (NSEC_PER_SEC * 1.5f)), dispatch_get_main_queue(), ^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (long) (NSEC_PER_SEC * 0.5f)), dispatch_get_main_queue(), ^{ [self dismissModalViewControllerAnimated:YES]; }); } else - [self changeAvatarShadowColorTo:[UIColor colorWithName:@"crimson"]]; + [self setPasswordTip:@"Incorrect password."]; [self setSpinnerActive:NO]; }); @@ -309,7 +329,7 @@ - (MPUserEntity *)userForAvatar:(UIButton *)avatar { - return NullToNil([self.avatarToUser objectForKey:[NSValue valueWithNonretainedObject:avatar]]); + return NSNullToNil([self.avatarToUser objectForKey:[NSValue valueWithNonretainedObject:avatar]]); } - (void)setSpinnerActive:(BOOL)active { @@ -342,23 +362,12 @@ }); } -- (void)changeAvatarShadowColorTo:(UIColor *)color { - - self.avatarShadowColor = color; - - if (self.selectedUser) { - UIButton *selectedAvatar = [self avatarForUser:self.selectedUser]; - [selectedAvatar.layer removeAnimationForKey:@"targetedShadow"]; - [self updateAvatarShadowColor:selectedAvatar isTargeted:YES]; - } -} - -- (void)updateAvatarShadowColor:(UIView *)avatar isTargeted:(BOOL)targeted { +- (void)updateAvatarShadowColor:(UIButton *)avatar isTargeted:(BOOL)targeted { if (targeted) { if (![avatar.layer animationForKey:@"targetedShadow"]) { CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"]; - toShadowColorAnimation.toValue = (__bridge id) self.avatarShadowColor.CGColor; + toShadowColorAnimation.toValue = (__bridge id) (avatar.selected? self.avatarTemplate.backgroundColor: [UIColor whiteColor]).CGColor; toShadowColorAnimation.beginTime = 0.0f; toShadowColorAnimation.duration = 0.5f; toShadowColorAnimation.fillMode = kCAFillModeForwards; @@ -404,13 +413,17 @@ #pragma mark - UITextFieldDelegate +- (void)textFieldDidBeginEditing:(UITextField *)textField { + + [self setPasswordTip:nil]; +} + - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; [self setSpinnerActive:YES]; - [self changeAvatarShadowColorTo:[UIColor colorWithName:@"lightskyblue"]]; - + if (self.selectedUser.keyID) [self tryMasterPassword]; @@ -463,11 +476,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { - // CGFloat xOfMiddle = scrollView.contentOffset.x + scrollView.bounds.size.width / 2; - // UIButton *middleAvatar = (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, scrollView.contentOffset.y) ofArray:scrollView.subviews]; - // [self updateLayoutAnimated:NO allowScroll:NO completion:nil]; - // [self scrollToAvatar:middleAvatar animated:NO]; } #pragma mark - IBActions diff --git a/MasterPassword/iOS/MainStoryboard_iPhone.storyboard b/MasterPassword/iOS/MainStoryboard_iPhone.storyboard index 3b9acd45..7cacd1cd 100644 --- a/MasterPassword/iOS/MainStoryboard_iPhone.storyboard +++ b/MasterPassword/iOS/MainStoryboard_iPhone.storyboard @@ -16,18 +16,18 @@ - + - - + + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -128,7 +155,7 @@ The passwords aren't saved anywhere. This is a major advantage: if you lose you - + @@ -148,7 +175,7 @@ The passwords aren't saved anywhere. This is a major advantage: if you lose you - - + @@ -181,7 +208,7 @@ The passwords aren't saved anywhere. This is a major advantage: if you lose you - - + @@ -230,11 +257,11 @@ The passwords aren't saved anywhere. This is a major advantage: if you lose you - - + + - + - - + + + + + + + + + + + + + + - - + @@ -324,9 +363,6 @@ The passwords aren't saved anywhere. This is a major advantage: if you lose you - - - @@ -779,6 +815,25 @@ L4m3P4sSw0rD + + + + + + + + + + + + @@ -786,12 +841,12 @@ L4m3P4sSw0rD - - + + -