SENG 440: Lecture 5 – List Activity and Implicit Intents
Dear students,
A major theme of mobile development (and the web) is services. Our apps tend not to be monolithic beasts that do everything themselves. Rather, they offload some of their work to other apps that have intentionally exposed as service providers. Today we’ll see Android’s Intent
model for invoking services from other apps. We’ll also see a giant among UI widgets: the list view. Our learning context will be an app that provides a directory of the people in this class.
Before we forget, here’s your TODO list for next time:
- Read Interacting with Other Apps.
- Read A Curious Question Of Vanity, Urgency, Pleasure And Anxiety.
- On a quarter sheet of paper to be turned in at the beginning of the next class, describe your feelings about cell phones and children. Should you some day have children, what policies will you adopt?
Let’s start making our app, which we’ll call Connect440.
ListActivity
The first screen of our app will be a list of our names. Lists are a big deal in the mobile world. We certainly see them in desktop computing, too, but I’d wager that they are much more prevalent on mobile. Why is that? We deal with collections on both kinds of systems, but on desktop computers we tend to have to have more screen space. That room makes it easier to lay out and organize the content across two dimensions as we do with file icons. We can also more easily show hierarchies. But on mobile, the UI mechanics have largely converged around two primary actions: vertical swiping and tapping.
There is a ListView
widget that we can embed in our layouts, but because this widget is so common, there’s a pre-configured Activity
subclass named ListActivity
that already has one baked in. Let’s create one of those.
class MainActivity : ListActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
We don’t need to call setContentView
because the superclass takes care of that.
Now it’s time to populate our list. The software developers that have gone before us provided a framework to guide us whenever we do UI programming. That pattern is called model-view-controller. The name lists off the three buckets that our code should fall into. Code that manages the state with no regard to its presentation is the model. Code that manages the presentation is the view. Roughly, both model and view should be reusable in other contexts and not tightly coupled to each other or the controller. The specialized glue that binds them together is the controller.
The ListView
has already been written for us, so let’s write a model. Each entry will represent a peer or friend from this class, and we will store the many possible ways we might contact them:
class Friend(val name: String, val slackId: String, val home: String, val email: String, val phone: String) {
}
We hardcore an array of our friends into our activity like so:
private val friends = arrayOf<Friend>(
Friend("Chris Johnson", "Eau Claire, WI", "foo@canterbury.ac.nz", "#########")
)
To bridge between our model and view, we need a controller, but that has also been written for us. The ArrayAdapter
class will wrap around an array. It has a method getView
that yields a TextView
for each visible item in the list. We create and register the adapter in onCreate
:
listAdapter = ArrayAdapter<Friend>(this, android.R.layout.simple_list_item_1, friends)
The middle parameter references one of the many prefabricated layouts that Android provides. These layouts seem to drop magically out of the sky. If you encounter one, visit the Android source to figure out what’s going on.
We could just as easily have written our own layout resource. To get it to work with ArrayAdapter
, we’d need to give it the ID text1
. The advantage of using the stock item is that it will look like the user expects.
Our list is now populated, and it’s time to handle taps on the names. Because ListActivity
was born for this moment, we need only override a superclass method:
override fun onListItemClick(l: ListView?, v: View?, friendId: Int, id: Long) {
Log.d("FOO", "$friendId")
}
AlertDialog
When a person’s name is tapped, let’s display a dialog that prompts the user for how they want to contact the tapped individual: by email, by telephone, by going to their house, and so on. Android provides a comprehensive dialog system via the AlertDialog
class. It comes with an inner Builder
class to help you set up your dialog. You can make it have one button, or two, or three. You can have it show a title and a message. Or you can have it show a list of options, which is what we want:
override fun onListItemClick(l: ListView?, v: View?, friendId: Int, id: Long) {
val options = arrayOf("Map", "Email", "Text", "Call", "Slack")
val builder = AlertDialog.Builder(this)
builder.setTitle("Connect how?")
builder.setItems(options) { _, optionId ->
dispatchAction(optionId, friends[friendId])
}
builder.show()
}
fun dispatchAction(optionId: Int, friend: Friend) {
when (optionId) {
0 -> TODO("Map")
1 -> TODO("Email")
2 -> TODO("Text")
3 -> TODO("Call")
4 -> TODO("Slack")
}
}
The lambda that we feed to setItems
is expected to have two parameters, one for the dialog and one for the index of the selected option. We don’t care about the dialog, so we use an underscore in its place.
Intents
We have reached our limits now. We don’t know how to send an email. We don’t know the cell protocols to call someone. We don’t how to draw a map. But other apps do. The last piece of our app is figuring out how to ask other apps to do things on our app’s behalf. We use Android’s Intent
class to express our request. Before we look at the technical side of it, I’d like to discuss the nomenclature a bit.
Some years ago I read an article that claimed that many communication failures happen because we communicate our positions rather than our interests. Instead of saying our interest, “I want to spend more time together,” we say, “Let’s take dance lessons.” Instead of saying our interest, “I’m nervous that we won’t finish the project in time,” we say, “I need you here on the weekends.” Our positions are overconstrained, which means they are more likely to be rejected by the listener. There may be ways to spend more time together and the complete the project than the particular position that we, the speaker, have taken.
The intents that we consider today are like these interests. We will only make a very vague request for a service, and any app on the system is eligible to provide that service. Intents generally have this form:
val intent = Intent(REQUEST_DESCRIPTION, uri)
intent.putExtra(KEY1, VALUE1)
intent.putExtra(KEY2, VALUE2)
startActivity(intent)
There are a number of builtin requestion descriptions. We will see just a couple today. Let’s start with sending an email. We use the request Intent.ACTION_SEND
with an email address as an extra parameter:
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_EMAIL, friend.email)
startActivity(intent)
The operating system will present us with a list of all possible apps that can provide this service of sending a message to an email address for us. That’s what make this Intent
more like an interest than a position. We only announce our intent, which can be satisfied in many ways.
To view a location on a map, we use the Intent.ACTION_VIEW
request, and we send along a uniform resource identifier (URI) that specifies the location. The URI can take several forms. To query for a location based on its name, we’d write:
geo:0,0?q=Christchurch, New Zealand
But the pioneers of the internet like Tim Berners-Lee tell us (in section 8.2.2 of RFC 1866) that query parameters need to be encoded according to the application/x-www-form-urlencoded
standard, like this:
geo:0,0?q=Christchurch%2C%20New%20Zealand
Thankfully the Java standard library has some methods for dealing with URIs and encoding. We issue our intent to map a friend’s home like so:
val uri = Uri.parse("geo:0,0?q=${URLEncoder.encode(friend.home, "UTF-8")}")
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
To text someone, we use the Intent.ACTION_SENDTO
request. The Android documentation says this of SEND
:
Activity Action: Deliver some data to someone else. Who the data is being delivered to is not specified; it is up to the receiver of this action to ask the user where the data should be sent.
And it says this of SENDTO
:
Activity Action: Send a message to someone specified by the data.
Of course, we just sent an email to a specific someone above using SEND
. The difference here is that the recipient is identified in the SMS URI:
val uri = Uri.parse("smsto:${friend.phone}")
val intent = Intent(Intent.ACTION_SENDTO, uri)
startActivity(intent);
To call someone, we may use the Intent.ACTION_DIAL
request:
val uri = Uri.parse("tel:${friend.phone}")
val intent = Intent(Intent.ACTION_DIAL, uri)
startActivity(intent);
This pops open the dialer. But there’s also Intent.ACTION_CALL
. This takes the extra step of dialing the number. But the Android folks thought automatically dialing a number from an app might lead to abuse. To unlock that feature, we need to do two things. First, we announce that our app needs that permission in the manifest
element of the manifest:
<uses-permission android:name="android.permission.CALL_PHONE" />
There are several classes of permissions. For the normal permissions, we need only declare them in the manifest. The user will see these permissions in the Play Store. For the dangerous permissions, we must declare them in the manifest and dynamically prompt the user to give them the okay. We’ll drop in a check for and request the CALL_PHONE
permission in onCreate
:
override fun onCreate(savedInstanceState: Bundle?) {
...
val dangers = arrayOf(Manifest.permission.CALL_PHONE)
val ungranted = dangers.filter { checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED }
requestPermissions(ungranted, 1)
}
For our last service request, let’s open up a direct message on Slack. The Slack API describes how we can form URIs that the Slack app can respond to. For a direct message, we use:
slack://user?team={TEAM_ID}&id={USER_ID}
Finding the team ID and user ID are not as straightforward as they could be, but once we have them, we can use the ACTION_VIEW
request to trigger the app:
val uri = Uri.parse("slack://user?team=$teamId&id=${friend.slackId}")
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent);
In all these cases, we farm out the real work to other apps. They don’t communicate any result back to us. Next time, we’ll see how to get the apps to communicate results back to the caller. That’s enough for today, however. See you next time!
P.S. It’s time for a haiku!
Newton had giants
Who were just people themselves
Stacked up pretty high