Calvin French-Owen Calvin French-Owen on

Data is critical to building great apps. Engineers and analysts can understand how customers interact with their brand at any time of the day, from any place they go, from any device they're using - and use that information to build a product they love. But there are countless ways to track, manage, transform, and analyze that data. And when companies are also trying to understand experiences across devices and the effect of mobile marketing campaigns, data engineering can be even trickier. What’s the right way to use data to help customers better engage with your app?

In this all-star panel hear from mobile experts at Instacart, Branch Metrics, Pandora, Invoice2Go, Gametime and Segment on the best practices they use for tracking mobile data and powering their analytics.

Che Horder is the Director of Analytics at Instacart, and previously led a team data science and engineering team at Netflix as Director of Marketing Analytics.

Gautam Joshi is the Engineering Program Manager of Analytics at Pandora and formerly worked at CNET/CBSi and Rdio. He helped create sustainable solutions for deriving meaning from large datasets. He’s a huge fan of music and technology, a California native and a proud Aggie.

Mada Seghete is the co-founder of Branch Metrics, a powerful tool that helps mobile app developers use data to grow and optimize their apps.

Beth Jubera is Senior Software Engineer at Invoice2Go, and was previously a Systems Engineer at IBM.

John Hession is VP of Growth at Gametime, and was previously Director of Mobile Operations and Client Strategy at Conversant.

Continue
Andrew Copp Andrew Copp on

Authentication is often an afterthought when developing apps. It is a step users will need to complete just once before moving on to the actual features of your product. Hinge’s iOS client was a complicated state machine for a long time. There were a number of paths the user can take and we had zero test cases to ensure we were handling all of them. The mentality was very much, “if it ain’t broke, don’t fix it.”

Our outlook changed when Facebook announced their plans to deprecate v1.0 of the Graph API. We were going to have to update the Facebook SDK and replace many of the classes and methods throughout the app. We took this as an opportunity to revisit the architecture of our authentication process.

Facebook encourages the use of the Singleton Pattern and this had been abused in our code. Our Facebook object had a massive API which led to muddled responsibilities and hidden dependencies. It was confusing to know where in the authentication process a user was in at any given time. Lots of potential steps in the flow were overlooked.

We switched to creating a single instance of our HNGFacebookSession class in the HNGLoginViewController and passing it along through the app. We had immediate wins from this approach. Passing along the HNGFacebookSession object meant no more hidden dependencies. Dependency injection meant simpler tests. Composition left open the possibility for swapping out authentication services at runtime.

Before

FacebookService (Singleton Object)

+ (void) logout
{   
    [FBSession.activeSession closeAndClearTokenInformation];
    [[FacebookService sharedService] setUser:nil]; // clear the current user.
    
    [[SessionController defaultController].machine changeStateTo:LoggedOut];
    
    // Doubling up on screens?
    if (![SharedAppDelegate rootController].presentedViewController)
    {
        [SharedAppDelegate displayOnboarding:^{
            [SharedAppDelegate switchToTab:kHomeTab];
        }];
    }
}

SessionController (Singleton Object)

- (void) executeStep
{
        ...
        case HasAPIVersion:
            [self checkFacebookAuth];
            break;

        case NeedsFBLogin:
            [SharedAppDelegate removeSplash];
            break;
        ...
        case LoggedOut:
        {
            [self.machine changeStateTo:HasAPIVersion];
        }
        ...
    }
}

After
HNGFacebookSession

- (void)logout
{
    [[FBSession activeSession] closeAndClearTokenInformation];
}

- (void)sessionStateDidChangeWithSession:(FBSession *)session state:(FBSessionState)state error:(NSError *)error
{
    switch (state) {
    ...   
        case FBSessionStateClosed:
            [self sessionEnded];
            break;
    }
}

- (void)sessionEnded
{   
    [self.delegate sessionDidLogout:self];
}

HNGLoginViewController

- (void)sessionDidLogout:(NSObject *)session
{   
    [self.navigationController popToRootViewControllerAnimated:YES];
    // Cleanup
}

Don’t repeat yourself repeat yourself


Despite all these wins we found ourselves with a lot of repeated code. We were presenting a login modal anytime our user wasn’t authenticated. This meant every view controller theoretically needed to be able to handle an invalid token by presenting the login modal and handling the corresponding delegate callbacks.

 

Untangling_Facebook_Authentication_on_iOS



Modal Login


This approach did not make sense for us. Not only because of the tedium but because our app won’t function without a valid user. There is no point in letting the user past the login screen if they aren’t authenticated.


Root View Controller


The suggested solution was to optionally swap out the root view controller during app launch if the user wasn’t logged in. We have an asynchronous login on every app launch where we check in with our backend, so this wasn’t a solution. We weren’t big on the idea of swapping out the root view controller at runtime either.

We elected to make the login view controller the root view controller of our app. Doing so made authentication a priority by forcing the user through the authentication process on every app launch. This approach had the additional benefit of being able to pop to the root view controller instead of presenting a modal if the user ever logged out.

Untangling_Facebook_Authentication_on_iOS_(1)


Login as Root


This worked great except for having one major flaw—users saw the login view controller on every app launch. We had some business logic to check for a cached Facebook access token to automate the process but the experience was not ideal for users.

We solved the problem by subclassing the navigation controller and adding an imitation Launch Image as a property. The imitation Launch Image is the frontmost view when the app becomes responsive and gives the illusion the app is still starting. We are running our authentication process underneath and remove the curtain once we know what view controller to drop the user into.

Untangling_Facebook_Authentication_on_iOS_(2)


Login as Root w/ Curtain


Our new authentication flow gives users a consistent navigational experience. There is one way to enter the app and you have to go out the way you came in. The simplicity also allows us to deal with one centralized delegate rather than a handful of notifications strewn about. Less time digging through the app to figure out what caused a side effect means more time working on important new features.

Authentication will continue to evolve to meet the needs of our users and technical partners. Facebook released a new SDK since we broke ground on this project and, as a result, we will be revisiting this architecture shortly. We’d love to hear ways in which you’ve simplified your authentication flow so we can improve further on our next iteration.

Continue
Matthew Reilly Matthew Reilly on

Experiments with Google Play Services

Screen Shot 2015-05-11 at 12.15.18 PM


“I can’t register!” “Your app isn’t available in New York City??!!”


Comments like these (some more vocal than others) began pouring in immediately after we ramped up our staged rollout to Google Play. Hinge is, in fact, available in New York City (our hometown!) Clearly something was amiss.

We had just pushed out an update to our app which included a new map-based geocoding registration. Instead of using a zipcode text field, the device would automatically determine a user’s neighborhood, city and zipcode. For each location the user picked on the map, we reverse-geocoded the latitude and longitude to obtain a general neighborhood, city, or state. This information was then passed to our servers to determine eligibility.

And it worked great! With our internal group of about 5 testers, with a range of devices, the map worked. At least, we thought.

But when we deployed the new version it appeared  that some new users simply could not register at all, which, of course, is a very serious problem. In the case of a few users, the fetching of address information failed, rejecting the user and instructing them to email support. What could be going on?

Discovery

We immediately attempted to reproduce the experience these users where having, with little success. More and more people wrote in, explaining that, yes, repeated attempts to register in a valid location (e.g. New York City) resulted in failure.

Our first attempt involved testing on our devices and emulators. With a group of 4 people with 2-4 devices each, we continually moved around our map, forcing the geocoder to recalculate the position. Ultimately, we failed to reproduce the problem. A group code review of the key points produced no obvious errors or questions. We were lost. With the app live and being pushed to new markets, a fix was needed—and fast.

After several hours of searching online, finally, a break! A report on Google’s bug tracker was discovered. Was it a problem with our code? Nope. Was it a device issue? Negative. It would seem Hinge wasn’t alone in its problematic geocoding situation.

Who is at fault?

The bug exists in Google Play Services, the framework Google distributes to all Android devices. To put it simply, sometimes the Geocoder just doesn’t work. The only valid solution is a device reboot, which obviously isn’t a tip we can provide to our users. It appears that a bug with Google Play Services causes certain devices to completely fail in loading the module responsible for geocoding addresses and locations. This bug still exists in the latest iteration of the geocoding package. (More information here) It throws the following exception, then fails to produce any valuable results to the device:

Geocoder.getFromLocationName : "IOException: Service is not available"


Our existing calls to to the Geocoders getFromLocation method looks like this:
@Override

protected LocationBundle doInBackground(LatLng... params) {
Geocoder geocoder = new Geocoder(mContext);
double latitude = params[0].latitude;
double longitude = params[0].longitude;

List<Address> addresses;

try {
addresses = geocoder.getFromLocation(latitude, longitude,1);
} catch (IOException e) {
Timber.d("Geocoding failed");
addresses = getManualLocation(latitude, longitude);
e.printStackTrace();
}
if(!addresses.isEmpty()){
return parseLocation(addresses, params);
}

return new LocationBundle();
}


A try/catch block was used in case of network failure. After the geocoding returns, we check to ensure the results are valid. So some users were failing, producing 0 results and stopping the registration process.


Problem solving

Because we had already written a (mostly) working geocoding solution, completely replacing the existing logic was not a popular idea. A backup solution was proposed: because we can detect when the system fails (when the exception is thrown), a secondary geocoding solution can be used instead. We found the Google Maps Geocoding API to be relatively stable. It involves passing a lat/long to public, free endpoint. The results are provided in JSON.

Here’s what the code looks like:

private List<Address> getManualLocation(double lat, double lng) {

SharedPreferences settings;
settings = PreferenceManager.getDefaultSharedPreferences(mContext);
settings.edit().putBoolean("SecondaryGeoCode", true).apply();

String address = String.format(Locale.ENGLISH,"http://maps.googleapis.com/maps/api/geocode/json?latlng=%1$f,%2$f&sensor=true&language="+Locale.getDefault().getCountry(), lat, lng);
Timber.d(address);
HttpGet httpGet = new HttpGet(address);
HttpClient client = new DefaultHttpClient();
HttpResponse response;
StringBuilder stringBuilder = new StringBuilder();

List<Address> retList = new ArrayList<>();

try {
response = client.execute(httpGet);
HttpEntity entity = response.getEntity();
InputStream stream = entity.getContent();
int b;
while ((b = stream.read()) != -1) {
stringBuilder.append((char) b);
}

JSONObject jsonObject = new JSONObject(stringBuilder.toString());
retList = new ArrayList<>();

if("OK".equalsIgnoreCase(jsonObject.getString(STATUS))){
JSONArray results = jsonObject.getJSONArray(RESULTS);
boolean hasGoodPartTwo=false;

for (int i=0;i<results.length();i++ ) {

//parse addresses
}


A relatively simple solution with a basic usage of HTTPClient and JSON parsing. Because the Android geocoder handles the parsing of data into Address objects, we also had to parse and build a list of these objects ourselves so we could still use our existing registration logic. Because Google returns a large list of varying specificity, we have to find the best data available. Looping through the results and checking what attributes exist will give us that ability.
if(hasJson(types, NEIGHBORHOOD)) {

partOne = addressObject.getString(LONG_NAME);
break;
}
else if(hasJson(types, LOCALITY)) {
partOne = addressObject.getString(LONG_NAME);
break;
}
else if(hasJson(types, SUBLOCALITY_LEVEL_1)) {
partOne = addressObject.getString(LONG_NAME);
break;
}
else if(hasJson(types, SUBLOCALITY_LEVEL_1)) {
partOne = addressObject.getString(LONG_NAME);
break;
}

We then set each object with the proper values, giving us a list of Address objects that, to our registration logic, works just fine.
addr.setSubLocality(partOne);

addr.setAdminArea(partTwo);

Keeping track

Because this bug exists out of our standard crash and analytics scope, keeping track of how often this happens proved difficult.

We provided a pretty painless solution. If we encountered a failure on registration a bool value in SharedPreferences named “failedGeoCode” was stored.

SharedPreferences settings;

SharedPreferences settings;

settings = PreferenceManager.getDefaultSharedPreferences(mContext);
settings.edit().putBoolean("SecondaryGeoCode", true).apply();

After the user installs, registers, and opens the app, we send off this value to our metrics tracking services. As of April 2015, it appears that almost 4% of users are experiencing this bug across all devices, regions, and Android versions. Which means, for most purposes, the Android geocoder simply isn’t reliable enough to use for any purpose in apps.

Moving down the line, we most likely will remove the Geocoder class altogether in favor of the secondary, more reliable, solution.

Sound off

Experiencing this bug in your own apps? Concocted a novel solution different from this? Shoot an email to matt@hinge.co and we can chat.

 

Continue
Unknown author on

The Android platform consists of a huge amount of code. With so much code, it is not uncommon that bugs and unexpected behavior occurs. In this talk, Erik Hellman explains some of the more interesting bugs and API behaviors that are present and how to deal with them. From subtle UI glitches, concurrency issues, the 64k method limit and more, this session will save you lots of time once you run into these bugs.

Continue
Unknown author on

Today we have a talk by Adam Lashinsky, Fortune Magazine Senior Editor and author of Inside Apple. In this talk Adam will explain what key elements of corporate culture and business processes led Apple to market domination. Talk was recorded at the The Silicon Valley iOS Developers' Meetup.


Have a cool project to share? Join us for our first Hack and Tell in San Francisco.


 

Continue
Unknown author on

We've got a new talk today and it's really special. Seven leaders in mobile development gathered at one of the TechXploration events in San Jose to discuss the Future of Mobile Development. Listen to learn how to keep up with important mobile trends now, and also hear the future forecast from these visionaries.


The panelists:

Continue