Lecture 7 Intents

This lecture discusses how to use Intents to communicate between different Activities and Applications. The Intent system allows Activities to communicate, even though they don’t have references to each other (and thus we can’t just call a method on them).

This lecture references code found at https://github.com/info448/lecture07-intents. Note that you will need to have a working camera on your device for this demo. To enable the camera in the emulator, use the Tools > Android > AVD menu to modify the emulator, and select “webcam” for the front camera option. Confirm that it is enabled by launching the Camera app.

An Intent is a message that is sent between app components, allowing them to communicate!

  • Most object communication we do is via direct method call; you have a reference to an Object and then you call a method on it. We’ve also seen event callbacks, where on an event one of our callbacks gets executed by the system—but this is really just a wrapper around direct method call via the Observer pattern.

  • Intents work a bit differently: they allow us to create an object that can be “given” to another component (read: Activity), who can then respond upon receiving it. This is similar to an event callback, but working at a slightly higher system level.

Intents were previously introduced in Lecture 3. But to reiterate: you can think of Intents as like letters you’d send through the mail: they are addressed to a particular target (e.g., another Activity—more properly a Context), and have room for some data called extras to go inside (held in a Bundle). When the envelope arrives, the recipient can get that data out and do something with it… and possibly send a response back.

Note that there are couple of different kinds of Intents; we’ll go through examples of each.

7.1 Intents for Another Activity (Explicit)

The most basic kind of Intent is an Intent sent to a specific Activity/Context, such as for telling that Activity to open. This was the same type of Intent introduced in Lecture 3.

//java
//                         context,           target
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
//kotlin
//                  context,           target
val intent = Intent(this@MainActivity, SecondActivity::class.java)

We instantiate the Intent (specifying the Contextit should be delivered from and the Class it should be delivered to), and then use that message to start an Activity by delivering the Intent via the startActivity() method.

  • The startActivity() method effectively means “this is an Intent used to launch an Activity: whoever receives this Intent should start!” You can almost think of it as saying that the letters should be delivered to the “start activity” mailbox.

This type of Intent is called an Explicit Intent because we’re explicit about what target we want to receive it. It’s a letter to a specific Activity.

Extras

We can also specify some extra data inside our envelope. These data are referred to as Extras. This is a Bundle (a set of primitive key-value pairs) that we can use to pass limited information around!

intent.putExtra("package.name.key", "value");
  • The Android documentation notes that best practice is to include the full package name in Bundle keys, so avoid any collisions or misreading of data (since Intents can move outside of the Application). There are also some pre-defined key values (constants) that you can and will use in the Intent class.

We can then get the extras from the Intent in the Activity that receives it:

//java
//in onCreate()
Bundle extras = getIntent().getExtras(); //All activities are started with an Intent!
String value = extras.getString("key");
//kotlin
val extras = intent.extras //All Activities are started with an Intent! (so have that property)
val value = extras.getString("key")

So we can have Activities communicate, and even share information between them! Yay!

7.2 Intents for Another App (Implicit)

We can send Intents to our own Activities, but we can also address them to other applications. When calling on other apps, we usually use an Implicit Intent.

  • This is a little bit like letters that have weird addresses, but still get delivered. “For that guy at the end of the block with the red mailbox.”

An Implicit Intent includes an Action and some Data. The Action says what the target should do with the intent (a Command), and the Data gives more detail about what to run that action on.

  • Actions can be things like ACTION_VIEW to view some data, or ACTION_PICK to choose an item from a list. See a full list under “Standard Action Activities”21.

    • ACTION_MAIN is the most common (just start the Activity as if it were a “main” launching point). So when we don’t specify anything else, this is used!
  • Data gives detail about what to do with the action (e.g., the Uri to VIEW or the Contact to DIAL).

    • Extras then support this data!

For example, if we specify a DIAL action, then we’re saying that we want our Intent to be delivered to an application that is capable of dialing a telephone number.

  • If there is more than one app that supports this action, the user will need to pick one! This is key: we’re not saying exactly what app to use, just what kind of functionality we need to be supported! It’s a kind of abstraction!
//java
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:206-685-1622"));
if (intent.resolveActivity(getPackageManager()) != null) {
  startActivity(intent);
}
//kotlin
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:206-685-1622")
if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

Here we’ve specified the Action (ACTION_DIAL) for our Intent, as well as some Data (a phone number, converted into a Uri). The resolveActivity() method looks up what Activity is going to receive our action—we check that it’s not null before trying to start it up.

  • This should allow us to “dial out” !

Note that we can open up all sorts of apps. See Common Intents22 for a list of common implicit events (with useful examples!).

7.3 Intents for a Response

We’ve been using Intents to start Activities, but what if we’d like to get a result back from the Activity? That is, what if we want to look up a Contact or take a picture, and then be able to use the Contact’s number or show the picture?

To do this, we’re going to create Intents in the same way, but use a different method to launch them: startActivityForResult(). This will launch the resolved Activity. But once that Action is finished, the launched Activity will send another Intent back to us, which we can then react to in order to handle the result.

  • This is a bit like including an “urgent, please respond!” note on the letter!

For fun, let’s do it with the Camera—we’ll launch the Camera to take a picture, and then get the picture and show it in an ImageView we have.

  • Note that your Emulator will need to have Camera emulation on!
//java
static final int REQUEST_IMAGE_CAPTURE = 1;

private void dispatchTakePictureIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
    }
}
//kotlin
private val REQUEST_IMAGE_CAPTURE = 1

fun dispatchTakePictureIntent() {
    val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    if (takePictureIntent.resolveActivity(packageManager) != null) {
        startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
    }
}
  • We start by specifying an Intent that uses the MediaStore.ACTION_IMAGE_CAPTURE action (the action for “take a still picture and return it”). We will then send this Intent for a result, similar to other Implicit Intents.

  • We also include a second argument: a “request code”. A request code is used to distinguish this Intent from others we may send (kind of like a tag). It let’s us know that the Result Intent (the “RSVP”) is for our “took a picture” letter, and not for some other Intent we sent.

  • Note that we could pass an Extra for where we want to save the large picture file to. However, we’re going to leave that off and just work with the thumbnail for this demonstration. See the guide23 for details. This topic is also covered in a later chapter.

In order to handle the “response” Intent, we need to provide a callback that will get executed when that Intent arrives. This callback is called onActivityResult().

//java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        Bundle extras = data.getExtras();
        Bitmap imageBitmap = (Bitmap) extras.get("data");
        ImageView imageView = (ImageView) findViewById(R.id.img_thumbnail);
        imageView.setImageBitmap(imageBitmap); //show the image
    }
}
//kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK) {
        val extras = data.extras?.apply {
            val imageBitmap = get("data") as Bitmap
            val imageView = findViewById<ImageView>(R.id.img_thumbnail)
            imageView.setImageBitmap(imageBitmap)
        }
    }
}
  • We can get information about the Intent we’re receiving from the params, including which Intent led to the result (the requestCode) and the status of that request (the resultCode). We can get access to the returned data (e.g., the image) by getting the "data" value from the extras.

  • Note that this is a Bitmap, which is the Android class representing a raster image. We’ll play with Bitmaps more in a couple weeks, because I like graphics.

7.4 Listening for Intents

We’re able to send implicit Intents that can be heard by other Apps, but what if we wanted to receive Implicit Intents ourselves? What if we want to be able to handle phone dialing?!

In order to receive an Implicit Intent, we need to declare that our Activity is able to handle that request. Since we’re specifying an aspect of our application (that other apps need to be aware of), we’ll do this in the Manifest using what is called an <intent-filter>.

  • The idea is that we’re “hearing” all the intents, and we’re “filtering” for the ones that are relevant to us. Like sorting out the junk mail.

An <intent-filter> tag is nested inside the element that it applies to (e.g., the <activity>). In fact, you can see there is already one there: that responds to the MAIN action sent with the LAUNCHER category (meaning that it responds to Intents from the app launcher).

Similarly, we can specify three “parts” of a filter:

  • a <action android:name="action"> filter, which describes the Action we can respond to.

  • a <data ...> filter, which specifies aspects of the data we accept (e.g., only respond to Uri’s that look like telephone numbers)

  • a <category android:name="category"> filter, which is basically a “more information” piece. You can see the “Standard Categories” in the documentation.

  • Note that you must include the DEFAULT category to receive Implicit Intents. This is the category used by startActivity() and startActivityForResult().

You can include multiple actions, data, and category tags. You just need to make sure that you can handle all possible combinations selected from each type (multiple types are combined with an “or”, not an “and”).

Responding to that dial command:

<activity android:name=".SecondActivity">
  <intent-filter>
      <action android:name="android.intent.action.DIAL"/>
      <category android:name="android.intent.category.DEFAULT" />
      <data android:scheme="tel" /> <!-- telephone Uris -->
  </intent-filter>
</activity>

You can see many more examples in the Intent documentation.

7.5 Broadcasts and Receivers

There is one other kind of Intent I want to talk about today: Broadcasts. A broadcast is a message that any app can receive. Unlike Explicit and Implicit Intents, broadcasts are heard by the entire system—anything you “shout” with a broadcast is publicly available (so think about security concerns when using these!)

  • To extend the snail mail metaphor, these are mass mailings perhaps.

Other than who receives them, broadcasts work the same as normal Implicit Intents! We create an Intent with an Action and Data (and Category and Extras…). But instead of using the startActivity() method, we use the sendBroadcast() method. That Intent can now be heard by all Activities on the phone.

  • We’ll skip a demo for lack of time and motivation… but we will generate broadcasts later in the course.

For us, more common than sending broadcasts will be receiving broadcasts; that is, we want to listen and respond to System broadcasts that are produced (things like power events, wifi status, etc). This would also include listening for things like incoming phone calls or text messages!

We can receive broadcasts by using a BroadcastReceiver. This is a base class that is used by any class that can receive broadcast Intents. We subclass it and implement the onReceive(Context, Intent) callback in order to handle when broadcasts are received.

//java
public void onReceive(Context context, Intent intent) {
    Log.v("TAG", "received! "+intent.toString());
    if(intent.getAction() == Intent.ACTION_BATTERY_LOW){
        Toast.makeText(context, "Battery is low!", Toast.LENGTH_SHORT).show();
    }
}
//kotlin
override fun onReceive(context: Context, intent: Intent) {
    Log.v("TAG", "received! ${intent.toString()}")
    when(intent.action) {
        Intent.ACTION_BATTERY_LOW -> {
            Toast.makeText(context, "Battery is low!", Toast.LENGTH_SHORT).show();    
        }
    }
}
  • While we can use Android Studio to produce this; we’re going to make it by hand to see all the steps.

But in order to register our receiver (so that Intents go past its desk), we also need to specify it in the Manifest. We do this by including a <receiver> element inside our <application>. Note that this is not an Activity, but a separate component! We can put an <intent-filter> inside of this to filter for broadcasts we care about.

<receiver android:name=".PowerReceiver">
    <intent-filter>
        <action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
        <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
        <action android:name="android.intent.action.BATTERY_CHANGED" />
        <action android:name="android.intent.action.BATTERY_OKAY" />
        <!-- no category because not for an activity! -->
    </intent-filter>
</receiver>

As of Oreo, registering for implicit broadcasts in the Manifest is not allowed and will not be received correctly, with a few exceptions. Instead, you would need to register receivers in code (described below). Note that setting the targetSdk to less than 25 is a temporary workaround.

We can test these power events easily using the emulator. In the “extra options” button (the three dots at the bottom) in the emulator’s toolbar, we can get to the Battery tab where we can effectively change the battery status of the device (which our app can respond to!). Remember to “disconnect” from the AC Charger first!

We can also register these receivers in code (rather than in the manifest). This is good for if we only want to temporarily listen for some kind of events, or if we want to determine the intent-filter on the fly:

//java
IntentFilter batteryFilter = new IntentFilter();
batteryFilter.addAction(Intent.ACTION_BATTERY_LOW);
batteryFilter.addAction(Intent.ACTION_BATTERY_OKAY);
batteryFilter.addAction(Intent.ACTION_POWER_CONNECTED);
batteryFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
this.registerReceiver(new PowerReceiver(), batteryFilter);
//kotlin
val batteryFilter = IntentFilter()
batteryFilter.addAction(Intent.ACTION_BATTERY_LOW)
batteryFilter.addAction(Intent.ACTION_BATTERY_OKAY)
batteryFilter.addAction(Intent.ACTION_POWER_CONNECTED)
batteryFilter.addAction(Intent.ACTION_POWER_DISCONNECTED)
this.registerReceiver(PowerReceiver(), batteryFilter)
  • We’re dynamically declaring an intent-filter as well! This can be used not just for BroadcastReceivers, but Activities too, if you dynamically want to change if an Activity can receive an Intent (i.e., sometimes I can make phone calls, but sometimes I can’t).

7.7 An Intent Example: SMS

An optional extra example, for the curious.

One specific use of Intents is when working with text messages (SMS, Short Messaging Service, the most popular form of data communication in the world). In particular, Intents are used to send and receive SMS messages (you can get a list of messages already received using a ContentProvided, described in a later lecture).

  • Important note: the SMS APIs changed drastically in KitKat (API 19). So we’re going to make sure that is our minimum so we can get all the helpful methods and support newer stuff (check gradle to confirm!).

The main thing to note about sending SMS is that as of KitKat, each system has a default messaging client—which is the only app that can actually send messages. Luckily, the API lets you get access to that messaging client’s services in order to send a message through it:

SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage("5554", null, "This is a test message!", null, null);
//                         target,       message
  • 5554 is the default “phone number” of the emulator (the port it is running on).

We will also need permission: <uses-permission android:name="android.permission.SEND_SMS" /> (in the Manifest). Note that you’ll need to target SDK 22 or lower for this to work without extra details; see the lecture on Permissions for more information.

You can see that this works by looking at the inbox in the Messages app… but there is another way as well! If you look at the documentation for this method27, you can see that the last two parameters are for PendingIntents: one for when messages are sent and one for when messages are delivered.

  • What’s a PendingIntent? The details are not super readable… It’s basically a wrapper around an Intent that we give to another class. Then when that class receives our PendingIntent and reacts to it, it can run the Intent (command) we sent it with as if that Activity were us (whew).

    • This is like when you ask a professor for a letter of recommendation, and you give them the stamped envelope (which you should always do!). When they get the packet, then can then send their letter using your envelop and stamp!
    • Alternatively: “I am mailing you my car keys, when you get them you can come pick me up”.
  • So the idea is we specify what Intent should be delivered when the message is finished being sent (that Intent becomes “pending”). Effectively, this let’s us send Intents in response to some other kind of event.

Let’s go ahead and set one up:

public static final String ACTION_SMS_STATUS = "edu.uw.intentdemo.ACTION_SMS_STATUS";

//...

Intent intent = new Intent(ACTION_SMS_STATUS);
PendingIntent pendingIntent = PendingIntent.getBroadcast(MainActivity.this, 0, intent, 0);

smsManager.sendTextMessage("5554", null, "This is a test message!", pendingIntent, null);

We’re doing a couple of steps here:

  • We’re defining our own custom Action. It’s just a String, but name-spaced to avoid conflicts
  • We then create an Implicit Intent for this action
  • And then create a PendingIntent. We’re using the getBroadcast() method to specify that the Intent should be sent via a Broadcast (c.f. getActivity() for startActivity()).
    • First param is the Context that should send the Intent, then a request code (e.g., for result callbacks if we wanted), then the Intent we want to be pending, and finally any extra flags (none for now).

We can then have our BroadcastReceiver respond to this Intent just like any other one!

if(intent.getAction() == MainActivity.ACTION_SMS_STATUS) {
    if (getResultCode() == Activity.RESULT_OK) {
        Toast.makeText(context, "Message sent!", Toast.LENGTH_SHORT).show();
    }
    else {
        Toast.makeText(context, "Error sending message", Toast.LENGTH_SHORT).show();
    }
}
  • Don’t forget to add our custom intent to the <intent-filter>!

We’ll see more with PendingIntents in the next chapter when we talk about notifications.