teaching machines

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:

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!

Sincerelya

P.S. It’s time for a haiku!

Newton had giants
Who were just people themselves
Stacked up pretty high

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *