5
\$\begingroup\$

I am trying to come up with a way to fetch the same data both when the application is running and when it is in the background. The code I have written so far works... I just think it is very messy and could probably be refactored, but I'm not sure where to start.

I have put all the files up on GitHub as they are rather large GitHub Repo.

Here's the code to review though:

#import "TabBarViewController.h"
#import "UIView+Border.h"
#import "UIColor+HexColors.h"
#import "Achievement.h"
#import "Achievement+Create.h"
#import "AppDelegate.h"
#import "AchievementModalViewController.h"

@interface TabBarViewController () <GameCenterManagerDelegate>
{
    NSDictionary *dataTypes;
    NSMutableDictionary *achievementData;
    NSMutableArray *achievementsToUpdate;
}

@property (strong, nonatomic) HKHealthStore *healthStore;

@end

@implementation TabBarViewController

@synthesize homeButton, settingsButton, achievementsButton, profileButton, gameCenterManager;

- (void)viewDidLoad {

    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.selectedIndex = 3;

    UIView *bar = [[[NSBundle mainBundle] loadNibNamed:@"CustomTabBar" owner:self options:nil] objectAtIndex:0];
    bar.frame = CGRectMake(0, self.view.bounds.size.height - 49, self.view.bounds.size.width, 49);

    [self.view addSubview:bar];

    UIView *profileBorder = [[UIView alloc] initWithFrame:CGRectMake((self.view.frame.size.width / 4) - 1, 0, 1, 49)];
    UIView *achievementsBorder = [[UIView alloc] initWithFrame:CGRectMake((self.view.frame.size.width / 4) - 1, 0, 1, 49)];
    UIView *settingsBorder = [[UIView alloc] initWithFrame:CGRectMake((self.view.frame.size.width / 4) - 1, 0, 1, 49)];

    profileBorder.backgroundColor = [UIColor colorWithHexString:@"e7e7e7"];
    achievementsBorder.backgroundColor = [UIColor colorWithHexString:@"e7e7e7"];
    settingsBorder.backgroundColor = [UIColor colorWithHexString:@"e7e7e7"];

    [profileButton addSubview:profileBorder];
    [achievementsButton addSubview:achievementsBorder];
    [settingsButton addSubview:settingsBorder];

    UIBezierPath *path = [UIBezierPath bezierPathWithRect:bar.bounds];
    bar.layer.masksToBounds = NO;
    bar.layer.shadowColor = [UIColor colorWithHexString:@"000000"].CGColor;
    bar.layer.shadowOffset = CGSizeMake(0, -.25);
    bar.layer.shadowOpacity = 0.5;
    bar.layer.shadowPath = path.CGPath;

    [homeButton setSelected:YES];

    self.gameCenterManager = [[GameCenterManager alloc] init];
    [self.gameCenterManager setDelegate:self];
    [self.gameCenterManager authenticateUser];

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(updateAchievementData) userInfo:nil repeats:YES];

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSString *firstRun = [defaults objectForKey:@"firstRun"];

    __block NSMutableArray *achievements = [NSMutableArray new];

    if ([firstRun isEqualToString:@"yes"]) {

        dispatch_queue_t queue = dispatch_queue_create([@"achievement.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_t query = dispatch_group_create();

        dispatch_group_async(query, queue, ^{

            //get all of achievements
            PFQuery *earnedQuery = [PFQuery queryWithClassName:@"Achievement"];
            [earnedQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {

                if (!error) {

                    achievements = [objects mutableCopy];
                    [self saveAchievements:achievements andThenCheck:NO];

                } else {

                    NSLog(@"Error: %@ %@", error, [error userInfo]);

                }

            }];

        });

        dispatch_group_notify(query, queue, ^{

            NSLog(@"Finished loading achievements");

            [defaults setObject:@"no" forKey:@"firstRun"];

        });

    }


}

- (void)displayModalViewControllerWithInfo:(NSDictionary *)info {

    AchievementModalViewController *amvc = [[AchievementModalViewController alloc] initWithNibName:@"AchievementModalView" bundle:nil];
    amvc.achievementTitle = info[@"title"];
    amvc.achievementMessage = info[@"message"];
    amvc.achievementDescription = info[@"description"];
    amvc.achievementImage.image = [UIImage imageWithContentsOfFile:info[@"image"]];

    [self presentViewController:amvc animated:YES completion:nil];

}

- (void)fetchNewDataWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

    __block UIBackgroundFetchResult result = UIBackgroundFetchResultNoData;

    dispatch_queue_t queue = dispatch_queue_create([@"my.query.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t queries = dispatch_group_create();

    PFUser *currentUser = [PFUser currentUser];

    [currentUser fetchIfNeededInBackground];

    if (!self.healthStore) {
        self.healthStore = [HKHealthStore new];
    }

    dataTypes = [NSDictionary dictionaryWithObjectsAndKeys:
                 [NSNumber numberWithInt:1], HKQuantityTypeIdentifierStepCount,
                 [NSNumber numberWithInt:2], HKQuantityTypeIdentifierFlightsClimbed,
                 [NSNumber numberWithInt:3], HKQuantityTypeIdentifierDistanceWalkingRunning,
                 [NSNumber numberWithInt:4], HKQuantityTypeIdentifierDistanceCycling, nil];

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *items = [defaults objectForKey:@"achievementItems"];

    NSDate *lastCheckedDate = items[@"lastCheckedDate"];

    NSCalendar *calendar = [NSCalendar currentCalendar];

    NSDate *startDate = [calendar startOfDayForDate:currentUser[@"dateSignedUp"]];
    [calendar setTimeZone:[NSTimeZone localTimeZone]];

    if (lastCheckedDate)
        startDate = lastCheckedDate;

    NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
    NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];

    for (NSString *key in dataTypes) {

        dispatch_block_t queryBlock = ^{

            __block BOOL success = NO;

            dispatch_semaphore_t lock = dispatch_semaphore_create(0);

            HKSampleType *sampleType = [HKSampleType quantityTypeForIdentifier:key];

            HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {

                if (!error) {

                    success = YES;

                    if (!results) {

                        NSLog(@"No results were returned form query");

                    } else {

                        [self processNewDataWithResults:results andType:key];

                        dispatch_semaphore_signal(lock);

                    }

                } else {

                    result = UIBackgroundFetchResultFailed;

                    NSLog(@"Error: %@ %@", error, [error userInfo]);

                }

            }];

            [self.healthStore executeQuery:query];

            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);

            if (success)
                result = UIBackgroundFetchResultNewData;

        };

        dispatch_group_async(queries, queue, queryBlock);

    }

    dispatch_group_notify(queries, dispatch_get_main_queue(), ^{

        dispatch_queue_t achievementQueue = dispatch_queue_create([@"achievement.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_t achievementQueries = dispatch_group_create();

        __block NSMutableArray *achievements = [NSMutableArray new];

        dispatch_block_t achievementBlock = ^{

            dispatch_semaphore_t lock = dispatch_semaphore_create(0);

            //get all of achievements
            PFQuery *earnedQuery = [PFQuery queryWithClassName:@"Achievement"];
            [earnedQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {

                if (!error) {

                    achievements = [objects mutableCopy];
                    [self saveAchievements:achievements andThenCheck:YES];

                    dispatch_semaphore_signal(lock);

                } else {

                    NSLog(@"Error: %@ %@", error, [error userInfo]);

                }

            }];

            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);

        };

        dispatch_group_async(achievementQueries, achievementQueue, achievementBlock);

        dispatch_group_notify(achievementQueries, achievementQueue, ^{

            completionHandler(result);

        });

    });

}

- (void)updateAchievementData {

    dispatch_queue_t queue = dispatch_queue_create([@"my.query.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t queries = dispatch_group_create();

    AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *context = delegate.managedObjectContext;

    PFUser *currentUser = [PFUser currentUser];

    [currentUser fetchIfNeededInBackground];

    if (!self.healthStore) {
        self.healthStore = [HKHealthStore new];
    }

    dataTypes = [NSDictionary dictionaryWithObjectsAndKeys:
                 [NSNumber numberWithInt:1], HKQuantityTypeIdentifierStepCount,
                 [NSNumber numberWithInt:2], HKQuantityTypeIdentifierFlightsClimbed,
                 [NSNumber numberWithInt:3], HKQuantityTypeIdentifierDistanceWalkingRunning,
                 [NSNumber numberWithInt:4], HKQuantityTypeIdentifierDistanceCycling, nil];

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *items = [defaults objectForKey:@"achievementItems"];

    NSDate *lastCheckedDate = items[@"lastCheckedDate"];

    NSDateFormatter *formatter = [NSDateFormatter new];
    [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    [formatter setTimeZone:[NSTimeZone localTimeZone]];

    NSCalendar *calendar = [NSCalendar currentCalendar];

    NSDate *startDate = currentUser[@"dateSignedUp"];

    if (lastCheckedDate)
        startDate = lastCheckedDate;

    startDate = [calendar startOfDayForDate:startDate];
    NSLog(@"%@", startDate);
    NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
    NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];

    for (NSString *key in dataTypes) {

        dispatch_block_t queryBlock = ^{

            dispatch_semaphore_t lock = dispatch_semaphore_create(0);

            HKSampleType *sampleType = [HKSampleType quantityTypeForIdentifier:key];

            HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {

                if (!error) {

                    if (!results) {

                        NSLog(@"No results were returned form query");

                    } else {

                        [self processNewDataWithResults:results andType:key];

                        dispatch_semaphore_signal(lock);

                    }

                } else {

                    NSLog(@"Error: %@ %@", error, [error userInfo]);

                }

            }];

            [self.healthStore executeQuery:query];

            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);

        };

        dispatch_group_async(queries, queue, queryBlock);

    }

    dispatch_group_notify(queries, queue, ^{

        [self storeDataOnServer:achievementData];
        [self checkAchievements:nil withManagedContext:context];

    });

}

- (void)saveAchievements:(NSMutableArray *)achievements andThenCheck:(BOOL)check {

    AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *context = delegate.managedObjectContext;

    NSMutableArray *achievementsToCheck = [NSMutableArray new];


    for (PFObject *object in achievements) {

        Achievement *achievement = [Achievement achievementWithObject:object inManagedContext:context];

        if ([achievement.earned isEqualToNumber:[NSNumber numberWithBool:NO]]) {

            //only add achievements that the user has not earned yet!
            [achievementsToCheck addObject:achievement];

        }

    }

    if (check)
        [self checkAchievements:achievementsToCheck withManagedContext:context];

}

- (void)checkAchievements:(NSArray *)achievements withManagedContext:(NSManagedObjectContext *)context {

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *achievementItems = [defaults objectForKey:@"achievementItems"];

    achievementsToUpdate = [NSMutableArray new];

    NSDate *today = [NSDate date];

    int userSteps = [[achievementItems objectForKey:@"steps"] intValue];
    int userDistance = [[achievementItems objectForKey:@"distance"] intValue];
    int userFlights = [[achievementItems objectForKey:@"altitude"] intValue];

    NSLog(@"%@------", achievementItems);

    if (achievements == nil) {

        //called statically, use database
        NSFetchRequest *request = [NSFetchRequest new];
        [request setEntity:[NSEntityDescription entityForName:@"Achievement" inManagedObjectContext:context]];
        [request setPredicate:[NSPredicate predicateWithFormat:@"earned == %@", [NSNumber numberWithBool:NO]]];

        NSError *error;

        achievements = [context executeFetchRequest:request error:&error];

    }

    for (Achievement *achievement in achievements) {

        NSString *type = achievement.type;

        int achievementCount = [achievement.count intValue];

        if ([type isEqualToString:@"steps"]) {

            //steps achievement
            if (userSteps >= achievementCount) {

                //user earned this achievement
                achievement.earned = [NSNumber numberWithBool:YES];
                achievement.earnedDate = today;

                [achievementsToUpdate addObject:achievement];

            }


        } else if ([type isEqualToString:@"distance"]) {

            //distance achievement
            if (userDistance >= achievementCount) {

                //user earned this achievement
                achievement.earned = [NSNumber numberWithBool:YES];
                achievement.earnedDate = today;

                [achievementsToUpdate addObject:achievement];

            }

        } else if ([type isEqualToString:@"altitude"]) {

            //altitude achievement
            if (userFlights >= achievementCount) {

                //user earned this achievement
                achievement.earned = [NSNumber numberWithBool:YES];
                achievement.earnedDate = today;

                [achievementsToUpdate addObject:achievement];

            }

        }

    }

    for (Achievement *achievement in achievementsToUpdate) {

        PFObject *achievementToSave = [PFObject objectWithClassName:@"EarnedAchievement"];
        achievementToSave[@"achievementId"] = achievement.id;
        achievementToSave[@"user"] = [PFUser currentUser];
        achievementToSave[@"earnedOn"] = achievement.earnedDate;

        [achievementToSave saveInBackground];

        //update achievement in database and send the user a notification!
        UILocalNotification *notification = [UILocalNotification new];
        notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];
        notification.alertBody = @"Achievement Unlocked! Come take a look.";
        notification.timeZone = [NSTimeZone localTimeZone];
        [[UIApplication sharedApplication] scheduleLocalNotification:notification];

        NSMutableDictionary *info = [NSMutableDictionary new];
        info[@"title"] = achievement.title;
        info[@"message"] = achievement.name;
        info[@"description"] = achievement.descriptor;
        info[@"image"] = achievement.image;

        [self displayModalViewControllerWithInfo:info];

    }

    NSError *error;

    [context save:&error];

}

- (void)processNewDataWithResults:(NSArray *)results andType:(NSString *)type {

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *unitPreference = [defaults objectForKey:@"unitPreference"];
    achievementData = [[defaults objectForKey:@"achievementItems"] mutableCopy];

    int typeValue = [[dataTypes objectForKey:type] intValue];

    //initialization
    double totalSteps = 0;
    double totalFlights = 0;
    double totalDistanceWalking = 0;
    double totalDistanceCycling = 0;

    int distanceWalkingCount = 0;
    int distanceCyclingCount = 0;

    int existingSteps = [achievementData[@"steps"] intValue];
    int existingFlights = [achievementData[@"altitude"] intValue];
    int existingDistance = [achievementData[@"distance"] intValue];

    switch (typeValue) {
        case 1:
        {
            //Steps Traveled
            for (HKQuantitySample *sample in results) {

                double steps = [[sample quantity] doubleValueForUnit:[HKUnit countUnit]];

                totalSteps += steps;

            }

            double difference = totalSteps - existingSteps;

            if (difference > 0)
                existingSteps += (int)difference;

            break;
        }
        case 2:
        {
            //Flights Climbed
            for (HKQuantitySample *sample in results) {

                double flights = [[sample quantity] doubleValueForUnit:[HKUnit countUnit]];

                totalFlights += flights;

            }

            double difference = totalFlights - existingFlights;

            if (difference > 0)
                existingFlights += (int)difference;

            break;
        }
        case 3:
        {
            //Distance Traveled
            for (HKQuantitySample *sample in results) {

                HKUnit *unit = nil;

                if ([unitPreference isEqualToString:@"standard"]) {
                    unit = [HKUnit mileUnit];
                } else if ([unitPreference isEqualToString:@"metric"]) {
                    unit = [HKUnit meterUnitWithMetricPrefix:HKMetricPrefixKilo];
                }

                double distance = [[sample quantity] doubleValueForUnit:unit];

                totalDistanceWalking += distance;

                distanceWalkingCount = (int)totalDistanceWalking;

            }

            break;
        }
        case 4:
        {
            //Cycling
            for (HKQuantitySample *sample in results) {

                HKUnit *unit = nil;

                if ([unitPreference isEqualToString:@"standard"]) {
                    unit = [HKUnit mileUnit];
                } else if ([unitPreference isEqualToString:@"metric"]) {
                    unit = [HKUnit meterUnitWithMetricPrefix:HKMetricPrefixKilo];
                }

                double miles = [[sample quantity] doubleValueForUnit:[HKUnit mileUnit]];

                totalDistanceCycling += miles;

                distanceCyclingCount = (int)totalDistanceCycling;

            }

            break;
        }
        default:
            break;

    }

    double totalDistance = totalDistanceWalking + totalDistanceCycling;

    double difference = totalDistance - existingDistance;

    if (difference > 0)
        existingDistance += (int)difference;

    [achievementData setObject:[NSNumber numberWithInt:existingSteps] forKey:@"steps"];
    [achievementData setObject:[NSNumber numberWithInt:existingFlights] forKey:@"altitude"];
    [achievementData setObject:[NSNumber numberWithInt:existingDistance] forKey:@"distance"];
    [achievementData setObject:[NSDate date] forKey:@"lastCheckedDate"];

    [defaults setObject:achievementData forKey:@"achievementItems"];

}

- (void)storeDataOnServer:(NSMutableDictionary *)data {

    NSDate *now = [NSDate date];
    PFUser *currentUser = [PFUser currentUser];

    PFQuery *query = [PFQuery queryWithClassName:@"Progress"];
    [query whereKey:@"user" equalTo:currentUser];
    [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {

        if (!error) {

            if ([objects count] > 0) {

                PFObject *object = [objects firstObject];

                //user exists, update
                object[@"steps"] = data[@"steps"];
                object[@"altitude"] = data[@"altitude"];
                object[@"distance"] = data[@"distance"];
                object[@"lastCheckedDate"] = now;

                [object saveInBackground];

            } else {

                //user doesn't exist create new
                PFObject *entry = [PFObject objectWithClassName:@"Progress"];
                entry[@"user"] = currentUser;
                entry[@"steps"] = data[@"steps"];
                entry[@"altitude"] = data[@"altitude"];
                entry[@"distance"] = data[@"distance"];
                entry[@"lastCheckedDate"] = now;

                [entry saveInBackground];

            }

        } else {

            NSLog(@"Error: %@ %@", error, [error userInfo]);

        }

    }];

}

- (void)didReceiveMemoryWarning {

    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.

}

- (IBAction)goToProfile:(id)sender {

    self.selectedIndex = 0;
    [profileButton setSelected:YES];
    [achievementsButton setSelected:NO];
    [homeButton setSelected:NO];
    [settingsButton setSelected:NO];

}

- (IBAction)goToAchievements:(id)sender {

    self.selectedIndex = 1;
    [profileButton setSelected:NO];
    [achievementsButton setSelected:YES];
    [homeButton setSelected:NO];
    [settingsButton setSelected:NO];

}

- (IBAction)goToSettings:(id)sender {

    self.selectedIndex = 2;
    [profileButton setSelected:NO];
    [achievementsButton setSelected:NO];
    [homeButton setSelected:NO];
    [settingsButton setSelected:YES];

}

- (IBAction)goToHome:(id)sender {

    self.selectedIndex = 3;
    [profileButton setSelected:NO];
    [achievementsButton setSelected:NO];
    [homeButton setSelected:YES];
    [settingsButton setSelected:NO];

}

- (void)authenticateUserWithViewController:(UIViewController *)viewController {

    [self presentViewController:viewController animated:YES completion:nil];

}

@end

If anyone has any questions or comments, please just let me know.

\$\endgroup\$
4
\$\begingroup\$

You've posted a lot of code, so I'm going to give a general overview of some common mistakes I see people making (and are also happening here).

  • You are using lots of magic numbers and magic strings. Let's instead create constants somewhere for all of these.
  • You're using NSLog statements without #if DEBUG, which can be problematic.
  • You have a lot of unnecessary vertical white space (empty lines). This decreases the readability when it's done to this degree.
  • The word "and" should only be used if the method name is describing two separate actions.
  • If all you're going to do is call super, just leave the method out entirely, otherwise it's clutter (didReceiveMemoryWarning).

Now, for some less common mistakes. Some advice slightly more specific to your code...

If looks like, at the end of the day, this is just a tab view controller with some slightly special code, right?

The IBAction methods at the bottom seem redundant... and it mostly seems like a default UITabViewController would handle these. We can subclass UITabViewController and put some authentication logic in it and let its default stuff handle a lot of stuff. There's nothign wrong with that.


if (difference > 0)
    existingDistance += (int)difference;

Where are your braces? If you must omit the braces, keep everything on one line, as such:

if (difference > 0) existingDistance += (int)difference;

But I must insist that you go ahead and add braces:

if (difference > 0) {
    existingDistance += (int)difference;
}

Your viewDidLoad method has way too much code in it. Moreover, it's very confusing that we keep switching back and forth between what view's we are setting up. Set one up completely, then set up the next completely, then the next, etc.


You flip flop back and forth between new and old style for things like NSDictionary.

myDict[@"myKey"] = someValue;

vs

[myDict setObject:someValue forKey:@"myKey"];

The former is vastly preferred to the latter.

We can also autobox NSNumber objects.

[NSNumber numberWithInt:1];

Could instead be simply:

@1

And this sort of thing is entirely unnecessary:

[defaults setObject:@"no" forKey:@"firstRun"];

NSUserDefaults has a setBool:forKey:, so if we need a boolean value, use that one:

[defaults setBool:NO forKey:@"firstRun"];

Finally, colorWithHexString: can be useful if we're fetching hex strings from a .plist file or downloading them from remote site, as hex strings are a fairly common means of storing colors. It may even be useful if we're letting the user enter a custom hex string for their color.

But if we're hard-coding it into our app, we should definitely not do this. Why would you want to make your app go through the hard work of parsing a string when we can just calculate ahead of time what these values should be and create the color using colorWithRed:green:blue:alpha:

\$\endgroup\$
1
  • \$\begingroup\$ Thank you so much for all of your advice. The IBActions at the bottom are all for a custom view that I am using as the TabBar, I wasn't sure if this was the best way to do it, but it works. I honestly didn't know that you could just do @number to autobox, I just discovered what autobox is today, so thank you! If you ever get a minute, could you take a look more specifically at the methods that are fetching data: fetchInBackground and updateAchievementData, one is for the background and one is not, but they are both very similar and I think they can be refactored. \$\endgroup\$ – Brandon Shega May 21 '15 at 13:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.