Signing In: A Case Study for Android

Reading Time: 16 minutes

My mobile software development students been working hard on a mental health management assistant app called Canary. Here is the code for Canary, if you’re interested. Her screens look like:

So far, the app has no login: it stores mental health data locally on the device. We want to add login for two reasons:

  1. We want to protect people’s mental health data so that they can log out of the app on their phones, such that another person on their phone can’t access the data, without losing all the data.
  2. If we wanted to back up people’s data to a server, we’d need to be able to identify whose data is whose.

So we’ll walk through adding that functionality in this post.

At this point, we have already added the sign in screen, and when you click the “sign in” button, you are taken to the MainActivity (the one with the tabs).

Here are the videos of that process, if you still need to catch up:

Now it is time to make login “work.”

Why is “work” in quotes? Because we are focused on the mobile side of this change, not the server side. Therefore, we are not going to build a server that authenticates and sends back a success or a failure. Instead, we will be making a network call to a fake endpoint.

The endpoint is: http://www.mocky.io/v2/5ed1d35132000070005ca001

The response looks thisaway:

Screen Shot 2020-05-30 at 3.34.40 PM

It always “logs you in successfully”. (You are also always “Chelsea” according to this endpoint. It’s a very welcoming endpoint, but not a very discerning one 😂).

This fake endpoint mimics a common API authentication scheme called token authentication. In this scheme, the client (our app) sends authentication information to the server—usually as some encrypted version of the username and password. If the server finds those credentials satisfactory, it sends back a token—in many implementations, a JSON Web Token (abbreviated “JWT” and pronounced “jot” as in “I’ll jot that down to remember later.”)

The token usually encodes some user info, and it can also encode an expiration period for the token: that is, the token can work for a period of time before the server invalidates it and it can no longer be used to access privileged information. By using a token, APIs allow apps to authenticate and then keep accessing information without having to save the user’s login credentials. That reduces the risk that those credentials get misused, accidentally logged, or otherwise compromised by the app.

Before we start, we need to enable networking in our Android app.

We do that in the AndroidManifest: the file that the Android operating system uses to allocate resources and permissions to each of our applications. We need to add some permissions to our app so that we can use the device’s internet connectivity capabilities. Here is how we do that:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chelseatroy.canary">
<application
</application>
// ADD THESE TWO LINES
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

Note where the added lines are in the file: inside the manifest tags, but outside the application tags. We want to be able to determine if the internet is available and use the internet, but those resources are not unique to our application: they’re device-wide.

Let’s set up our app to check for an internet connection before we make a network call.

Currently in our LoginActivity, the onClickListener for the submit button looks like:


class LoginActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
signInButton.setOnClickListener { view ->
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
}
}

Now we’ll add the ability to check for network connectivity before making a call to our authentication API in this listener.

First, we need to add a method to the LoginActivity itself:


class LoginActivity : AppCompatActivity() {
...
private fun isNetworkConnected(): Boolean {
val connectivityManager =
getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
return networkCapabilities != null &&
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}

We’re getting a reference to a system service of the Android operating system—its connectivity service. We’re then asking that for its currently active network, and determining whether that network can connect to the internet. We’ll return true if it can, and we’ll return false if it cannot.

Next, we’ll update our onClickListener to only move from the LoginActivity to the MainActivity if we have an internet connection.


class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
signInButton.setOnClickListener { view ->
if (isNetworkConnected()) {
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
} else {
AlertDialog.Builder(this).setTitle("No Internet Connection")
.setMessage("Please check your internet connection and try again")
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setIcon(android.R.drawable.ic_dialog_alert).show()
}
}
}
}

If we don’t have an internet connection, we’ll stick a little dialog onscreen that tells the person as much. You can put your device in airplane mode (or turn off wifi on the computer hosting your emulator) to verify that this works.

We’re still not logging in at this point, of course; we are only blocking advancement through the login screen on the device having an internet connection.

Now it’s time to start preparing our network calls.

In Android, we use a library for this called Retrofit. Retrofit, made by Square, wraps an HTTP library called OKHttp3. These are the dependencies we need to add to our build.gradle right now:


android {
}
dependencies {
// ADD THESE
implementation 'com.squareup.retrofit2:retrofit:2.6.3'
implementation 'com.squareup.retrofit2:converter-gson:2.6.3'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
}

We’re also going to be using Kotlin coroutines for this. That’s your third dependency there. I’ll get to that later.

First, we have to get our endpoint set up. Retrofit does this through interfaces. You create a method on an interface, and you annotate it (that’s the thing on top with the @ symbol) to indicate, when the method is called, what endpoint Retrofit should visit and what HTTP verb it should use. (For more on endpoints and HTTP verbs I recommend the History of API Design series).

For this project, make a new file called LoginService. I like to put it in its own API package like this, to keep my app organized:

Screen Shot 2020-05-30 at 6.12.17 PM

Here is what your LoginService is going to look like:


package com.chelseatroy.canary.api
import retrofit2.http.GET
interface LoginService {
@GET("/v2/5ed1d35132000070005ca001")
suspend fun authenticate(): CanarySession
}

view raw

LoginService.kt

hosted with ❤ by GitHub

See the annotation? When this method is called, Retrofit will make a GET request to your base URL with that on the end*. We haven’t set up a Base URL yet, but we will soon.

*One thing to note is that sign in is not typically a GET request to an endpoint; it’s typically a POST request with the username and password, usually encrypted, in the request body. Unfortunately I couldn’t get that kind of endpoint in Mocky without paying for the service, so this isn’t an exact match to how a login call would usually be made. But you still get the opportunity in this exercise to make an API call and do something with the result.

Notice two other things about the interface code:

  1. You see a keyword “suspend” on the beginning of this function. This keyword is used within Kotlin coroutines to schedule the point at which the coroutine executes on its thread. I really like this explanation of Kotlin coroutines—Joffrey does a great job of breaking down the main idea without getting too far into the implementation details.
  2. The function returns an object called CanarySession. What is that? Well, when we get the JSON result from the API, we hydrate it into an object inside the app. We do this in iOS apps when we make a model object conform to the “Codable” protocol. We don’t even need to do that, because we are using a library called Gson (out of Google) to handle that for us: provided the data class’s definition matches the JSON, Gson takes care of coding and decoding for us.

So, we make a file for this data class in the same place as our LoginService interface, data class whose attributes match the keys in the JSON, like this:


package com.chelseatroy.canary.api
data class CanarySession(
val name: String?,
val token: String?
)

Now we need to create a class that will make our API call. In Retrofit, we usually also define our base URL in here. Create a new class called MockyAPIImplementation and put it in the same package as CanarySession and LoginService.


package com.chelseatroy.canary.api
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MockyAPIImplementation {
private val service: LoginService
companion object {
const val BASE_URL = "http://www.mocky.io/&quot;
}
init {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
service = retrofit.create(LoginService::class.java)
}
suspend fun authenticate(): CanarySession {
return service.authenticate()
}
}

Here we:

  1. Define our base URL for these calls (line 10)
  2. Instantiate our retrofit object (line 14) and give it the baseURL (line 15)
  3. Say which library we are using to convert between JSON and objects (line 16)
  4. Pass it a service that we instantiate with retrofit (line 18)
  5. Create a method on this implementation class to call the method on the interface (lines 21-23)

All right, now we have added the capability to make our API Call in our app.

Now it’s time to make the call to the API.

One housekeeping thing first: Mocky uses a particular kind of certificate that makes it…not the most secure server. For our purposes of practicing making an API call, this is fine, but Android doesn’t like it. So we have to do a little workaround with the security configurations. There are two steps to that. First we add a line to the manifest:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chelseatroy.canary">
<application
//ADD THIS LINE
android:networkSecurityConfig="@xml/network_security_config"
</application>
</manifest>

Then we make a directory and an xml file in this location in our app:

Screen Shot 2020-05-30 at 11.05.21 PM

The directory must have the name xml because that is where Android will look for xml resources. The file can be named whatever you like, as long as the declaration in the manifest matches it.

Here are the contents you need to put into that file:


<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

OK, housekeeping finished.

Next, we implement our coroutine in the LoginActivity to make the call to our API. Let’s add the following two methods to our activity:


class LoginActivity : AppCompatActivity() {
fun authenticate() {
val loginJob = Job()
val errorHandler = CoroutineExceptionHandler { _, exception ->
AlertDialog.Builder(this).setTitle("Error")
.setMessage(exception.message)
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setIcon(android.R.drawable.ic_dialog_alert).show()
}
val coroutineScope = CoroutineScope(loginJob + Dispatchers.Main)
var session: CanarySession? = null
coroutineScope.launch(errorHandler) {
session = MockyAPIImplementation().authenticate()
//DON'T ACTUALLY log people's authentication tokens in a production application
Log.i("Session Retrieved:", session.toString())
if (session?.token != null) {
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
} else {
deliverAuthFailureMessage()
}
}
}
private fun deliverAuthFailureMessage() {
AlertDialog.Builder(this).setTitle("Whoops")
.setMessage("Wrong credentials. Try again?")
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setIcon(android.R.drawable.ic_dialog_alert).show()
}
}

view raw

LoginActivity

hosted with ❤ by GitHub

Whatall is happening here:

  1. Line 6: We initiate a Job. The “Job” class is used to delineate the scope of when the coroutine will run.
  2. Line 8: We create a lambda called errorHandler that says what should happen if something goes wrong with the code in the coroutine.
  3. Line 15: We define the scope of our coroutine.
  4. Line 18: We pass the errorHandler to this scope and start describing the code that should run within this coroutine scope. (Again, if something goes wrong, we’ll call that error handler).
  5. Line 19: We call the authenticate() method on the MockyAPIImplementation that we created in an earlier step. Then we update the variable “session” that we defined on line 17 with the value of that call.
  6. Line 24: We check the value. This code assumes that if a token is present, authentication worked. If that token isn’t present (say we got back a 401 unauthorized response, or an error response), it assumes that authentication didn’t work. So we go to the MainActivity for successful logins, and we show a dialog for failed logins.

Note startActivity() on line 26. That is the point where, upon a successful login, we will move into our MainActivity. So now let’s replace startActivity() in our onClickListener with a call to the authenticate() method that we just added:


class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
signInButton.setOnClickListener { view ->
if (isNetworkConnected()) {
authenticate() //REPLACE CALL TO startActivity() WITH THIS LINE
} else {
...
}

Now authentication “works.”

Let’s add a Progress Bar

We want a progress indicator to show up while our network call is being made, like so:

ProgressBar

There are two steps to this. First, we need to add it to our view. I add it at the bottom of the activity_login layout like so:


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="10dp"
android:indeterminateTint="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

I have constrained it to all four edges of the parent, set its color to the primary color of the app, set its elevation to 10dp (so that it appears on top of the other views), and set its visibility to “gone” (such that it does not appear on the screen when we first load it).

In the LoginActivity code, we can toggle its visibility between “visible” and “gone.”


package com.chelseatroy.canary
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
signInButton.setOnClickListener { view ->
if (isNetworkConnected()) {
//HERE'S WHERE WE SHOW THE PROGRESS BAR
progressBar.visibility = View.VISIBLE
authenticate()
} else {
...
}
}
}
fun authenticate() {
...
session = MockyAPIImplementation().authenticate()
//HERE'S WHERE WE HIDE THE PROGRESS BAR
progressBar.visibility = View.GONE
...
}
}
}

So we will make it visible when we start making the network call, and we will make it gone when the network call completes.

Let’s store (and use) the result from our API!

We get two things back from our mock login call: the JSON object has a token attribute, and it also has a name. Let’s save these temporarily in the application and use them elsewhere.

This would not be an unusual practice in an authenticated app; we want the person who logged in to be granted access on any screen that tries to fetch their data, and an authenticated API will require the token that we got back from the sign-in call in order to do that.

Currently, our version of the app stores data locally, so we don’t need to pull anything from an authenticated server. But we will save the token as if we did. We will also save the name, which we will use to change the top bar of the app so that it says “Hello, ${name}!” instead of “Canary.” Feel free to call your own fake mocky endpoint with your own name in there instead of mine—that would be less confusing to hiring managers if you want to make a video tour of this app and show it to them :). It’s not required, though.

Screen Shot 2020-05-31 at 9.49.31 PM

So far in Android we have persisted data by storing stuff in a local SQLite database. That’s the third of three main ways that you might store data locally on a phone:

  1. Keys and values: stored in an app-level object. Easy access, not a lot of setup. Best for individual items that aren’t themselves very large. (The implementation is called UserDefaults on iOS and SharedPreferences on Android).
  2. Files: stored on the app file system. Some setup. Best for images, texts, and similar large wads of block-structured or unstructured data. (Both iOS and Android have File Manager protocols).
  3. Tables: stored in the app database. The most setup. Best for complex data, or data that is linked to each other. (The implementation is Core Data on iOS and a relatively un-retouched SQLite database on Android).

For this, we don’t need tables. We just want to store two strings: the name and the token. So we will do that with keys and values.

First, let’s import the androidx library that Android wants us to use nowadays for SharedPreferences:


dependencies {
implementation "androidx.preference:preference-ktx:1.1.0"
}

(you can use the android standard implementation, but it is deprecated, and if you use it, you will see the methods are struck through in your editor).

Now, let’s add a method to the LoginActivity to save our two strings to SharedPreferences.


import androidx.preference.PreferenceManager
//SEE IMPORT ABOVE
class LoginActivity : AppCompatActivity() {
...
private fun saveSessionData(name: String?, token: String?) {
val preferences =
PreferenceManager.getDefaultSharedPreferences(this)
val editor = preferences.edit()
editor.putString("Name", name)
editor.putString("Token", token)
editor.apply()
}
}

You see here that we getDefaultSharedPreferences(), which provides us with a singleton instance of the app’s key-value store for us to use. Then we use a method to indicate the type of data we want to add (putString()), the key we want to link to the data (first argument), and the data itself (second argument).

We call that method after successful login and right before we go to the MainActivity:


class LoginActivity : AppCompatActivity() {
...
fun authenticate() {
...
if (session?.token != null) {
//HERE'S WHERE WE SAVE THE SESSION DAA
saveSessionData(session!!.name, session!!.token)
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
} else {
deliverAuthFailureMessage()
}
}
}

Finally, in the MainActivity, we pull out the value for the name key, and we set it in the text for the top bar:


class MainActivity : AppCompatActivity(), Updatable {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//CHANGING THE TEXT OF THE TOP BAR
val toolbarTitle = findViewById<TextView>(R.id.title)
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
var name = preferences.getString("Name", "")
if (!name.equals("", ignoreCase = true)) {
toolbarTitle.text = getString(R.string.greeting, name)
}
}

view raw

MainActivity.kt

hosted with ❤ by GitHub

You’ll notice, we interpolate the named variable into a saved string there! Here is what that looks like in the strings file:


<string name="greeting">Hello, %1$s!</string>

view raw

strings.xml

hosted with ❤ by GitHub

Your app works great! But hey, the login test fails.

Check it out:

Screen Shot 2020-05-31 at 10.15.48 PM

Why does this happen? Well, our app doesn’t automatically go to that MainActivity anymore. A test of this functionality now has to account for authentication.

The code we have written in this tutorial is not particularly well-designed for testing. We tossed a lot of functionality into the activities themselves. This is often the fastest way to get from nothing to something that works.

But  want you to imagine, for a moment, adding every feature in a complex application the way we added this one. The activity files would get really big. It would be hard for developers to understand and maintain those large files.

That’s why we split code into smaller files with individual responsibilities; code is easier to maintain in one file when an app is small, but it’s easier to maintain in an organized system of several files when the app gets large. This system also makes it easier to test components in isolation.

One way we might do that in this part of the app is to have the authenticate() method in the LoginActivity accept a success callback (a block of code it’s supposed to run when authentication succeeds) and a failure callback (a block of code it’s supposed to run when authentication fails). We could then test that, upon successful login, we get to the MainActivity.

We did something similar to this in our iOS App:

So, for bonus on this assignment…

Refactor the authenticate() method in the LoginActivity so that you can test the success and failure cases.

You can either leave all this functionality in the LoginActivity test, or you can pull the authenticate() method into another class.

What would be next on this app?

We have an app that facilitates signing in and visiting four different tabs of information. We can make mood entries, create custom pastimes, and look at some visualizations of our data. What more might we do on this app, if we had more time to work on it?

Really, the sky is the limit. But if you’re interested in continuing to practice, some ideas might be:

  1. Upload user data to a server. This is one of the main things that adding login would facilitate. Of course, this would require a functioning server to do “for real.” If you’re interested in continuing to focus on the mobile side, you can make a fake endpoint to accept your uploads of mood entries.
  2. Make the design of the app your own. Experiment with changing the look of the chart or the colors of the app. Display the weather as a selector with little weather icons, the same way that mood is a selector with little face icons, instead of displaying weather a series of radio buttons.
  3. Let people do more to customize their profile. Currently, they can set custom pastimes. Maybe they would be able to like to change their name in the app (again, typically you’d use a functioning server for this, but you can get practice with mock endpoints).
  4. Allow people to set a reminder for themselves to log their mood. For this, you’ll need to use Android’s Calendar API and the Notification API.

That’s all for now. I hope this tutorial has been somewhat useful and informative 😊

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.