Table of Contents

Lab 02. Communication between Android components: Intents, Broadcast Receivers

Objectives

What are Intents?

In Android, applications and application components can interact with one another by using Intent objects. An Intent is a messaging object used to request an action from another application component. It can be used between components of the same app or from different applications.

Usually an intent is used for:

An Intent is an object of type android.content.Intent. This is sent as a parameter for methods, startActivity(), startService() or sendBroadcast(), for starting an activity, a service or sending a broadcast message. The intent can transmit data to the components via a Bundle object.

Example explicit intent

Launch another activity using an explicit intent.

Intent intent = new Intent(
  context,             // Application context
  Activity.class);     // Component name 
startActivity(intent); // Launch component

Example implicit intent

Open an url in your phone's browser using an implicit intent - not specifying the exact app which will open the url, only tell the system that it need to be opened.

Intent intent = new Intent(Intent.ACTION_VIEW); // We've set the ACTION
intent.setData(Uri.parse("https://ocw.cs.pub.ro/smd/lab2"); // We've set DATA
 
// We check before if an application that can support our intent exists. 
// This blocks the app from crashing. We should inform the user about 
// this or to disable this feature in our app. 
if(intent.resolveActivity(getPackageManager())!=null) {
    startActivity(intent);
}

Explicit Intents

They specify the target component name, using the Java class name. Usually they are used inside applications. A good example is when an activity starts another activity as presented below:

Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent);

The intent uses a Context and the Class of the second activity. A context represents an interface to the current state of the application. It allows access to application specific resources and operations such as launching an activity or starting a service. In order to obtain the context you can use a reference to the current instance of the activity class by using this.

Task 1 - Use explicit intents to start another activity (2p)

a) startActivity

Create a new project and replace the content of the activity layout with activity_main.xml provided in the Resources section. You can use/make your own layout but make sure it allows you to test all the tasks in this lab.

Get a reference to “Go to secondary activity” button in the onCreate method of MainActivity. Add a new Activity to the project. You can automatically add a new activity by right clicking on the project and selecting New > Activity > Empty Activity from the menu. This will start a wizard similar to the one at application creation.

Back in MainActivity, add code to the onClick of the View.OnClickListener so that the new activity gets started. To start a new Activity you have to create an intent first which will explicitly take two parameters: a Context and the Class of the second activity.

Before launching the new activity, it must be declared in the AndroidManifest.xml file. If you manually created the activity, you will have to add a similar line to the following one to the manifest file.

<activity android:name="com.smd.lab2.SecondActivity" />

If you created the Activity manually, in the onCreate of the second activity make sure you call the onCreate of the super class and use setContentView to the layout of the second activity for which you will have to create an empty layout. If not you will get a NPE.

b) Add extras

Now we are going to add some extras to our intent. Get a reference to the EditText in the main activity and add a TextView to your second activity. When clicking the button, get the text from the EditText element and add it to the intent.

intent.putExtra("key", "value");

Then, in your second activity, first, you have to access the intent which launched the activity. This can be done using the getIntent method. Afterwards, get the string from the intent and display it in the TextView. (Hint: Intent.getStringExtra ​and Activity.getIntent)

Implicit intents

Implicit intents do not specify the target component name. In exchange an action and optionally data are specified. The operating system checks all matching registered components. If only one component is appropriate the system starts it and sends it the Intent. If there are more components the system offers the user the option to choose from the ones available. For example, viewing an URL will present the user the option to choose between the installed browsers from the phone.

Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(https://www.google.com));
startActivity(i);

https://developer.android.com/guide/components/intents-filters#java

Fig. 1 Implicit intent through the system

The Intent contains the necessary info for the Android system to identify the component to start and the data needed for the specified action. These are specified in the <intent-filter> section in the manifest file, in which are declared the capabilities of the component (or what broadcast receiver can handle).

Intent format

An Intent contains the following primary info:

Component Name

Usually the fully qualified name, including the package name, com.smd.lab2.MainActivity. This is what creates an explicit intent. The intent will only be delivered to the specified component. Without this an intent is implicit. The system will check for available components that can receive this intent based on the provided information.

Action

It is represented by a string that describes the action, for example some defined in the Intent class:

You can have actions defined in other classes, such as AlarmClock with actions for managing the alarms

Intent intent = new Intent(Intent.ACTION_SENDTO); // setting the action

You can also add custom actions. They respect the same rules by adding the application package name before the name of the action:

static final String ACTION_START_ENGINE = "com.smd.lab2.START_ENGINE";

Data

Based on the specified action the data field can take different values. In general it is an URI and/or MIME type of the data. The URI is a reference to the data on which the action will be done. If the URIs are similar and the MIME type can not be inferred it should be specified.

Intent intent = new Intent(Intent.ACTION_SENDTO); // setting the action
intent.setData(Uri.parse("smsto:")); // setting the data (this uri can also be specified as second param in the constructor)

These are some standard data fields used in an Intent: BCC, CC, EMAIL, TITLE, TEXT - these are all used to create an intent for an email message that will contain BCC(blind CC), CC(carbon copy), EMAIL(address), SUBJECT(email subject), TEXT(the text that forms the body of the email).

Category

This specifies which component can handle the intent. These are some standard categories used in an Intent:

If you want that one of your application components to accept implicit intents, you need to add the DEFAULT category. Without it the system will not accept that component as handler for an Intent even if it can handle it.

intent.addCategory(Intent.CATEGORY_APP_EMAIL); // add the category for the activity to be able to send or receive emails

Extras

Extras are key-value pairs which contain additional information needed to finish the specified action. Extra data is added through the putExtra() method, using key value pairs or by adding a Bundle object. The Intent class contains EXTRA* constants key for different data types. As in the example with custom actions you can create custom extras, using the same method of having the application package as a prefix.

intent.putExtra("address", new String ("123456789")); // add extra for the phone number
intent.putExtra("sms_body","Important message.."); // add extra for the sms content

App chooser

Sometimes you want to let the user select the desired handler app every time. This can be done by forcing a chooser dialog. This will not override the default option but in our app the user will have to choose every time. This is most used when using the share action. As an example think about a user who wants to send a photo through another app of his choice. In this case we need to supply him a chooser dialog every time, considering the fact that he can use different apps for different cases.

Intent intent = new Intent(Intent.ACTION_SEND);
String chooserTitle = "Share photo with";
Intent intentChooser = Intent.createChooser(intent, chooserTitle);

if(intent.resolveActivity(getPackageManager()) != null) {
   startActivity(intentChooser);
}

Your app will crash if there is no app on device that can receive the implicit intents it sends. Make sure to check if any app is available like in the above example.

Task 2 - Go to a web page (1p)

Let’s say that we want to send the user to a web page from our app. Get a reference to “Go to web page” button and add an intent with the web page URL and a ACTION_VIEW action in the click listener.

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.webpage.com"));
startActivity(intent);

Task 3 - Make a call (1p)

In this task we will try to make a call from our app. In order to do this our application will not have permission to make a call so we will only send an intent with the phone number to the main app that handles calls. Get a reference to “Make a call” button and write a click listener. Create an Intent with ACTION_DIAL and enter the phone number that you want to call.

Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:"+"enter the phonenumber"));
startActivity(intent);

Try to use a phone to run the app and make a call.

Task 4 - Send an email (2p)

For this task you need to have configured in your emulator's email client an email address or to run the app on a phone which has been already configured

Now we are going to send an email. Edit the fields below with the receiver email, title and subject. The text of the email will be from the MainActivity EditText. Get a reference to “Send email” button and add a click listener as in the previous tasks.

String text = editText.getText().toString().trim();
// We only send an email if there is text in the EditText
if (TextUtils.isEmpty(text)) {
    Toast.makeText(MainActivity.this, "Insert text", Toast.LENGTH_SHORT).show();
} else {
    Intent sendEmailIntent = new Intent(Intent.ACTION_SEND);
    sendEmailIntent.setType("text/html");
    sendEmailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"this@smd.com"});
    sendEmailIntent.putExtra(Intent.EXTRA_SUBJECT, "First email");
    sendEmailIntent.putExtra(Intent.EXTRA_TEXT, editText.getText().toString().trim());
    startActivity(sendEmailIntent);
}

Task 5 - Get data from an activity (2p)

In this task we start an activity and and also receive data from it.

Create a third activity in the project and add an EditText and a Button. In MainActivity, get a reference to “Get data from activity” button which will start the third activity. We will use startActivityForResult() in order to start the activity.

Intent intent = new Intent(this, ThirdActivity.class);
startActivityForResult(intent, 1); // 1 is the request code

Now we need to add some code that handles the Intent that will be received from the started activity. For this we need to override onActivityResult, which is called when the started activity is finished. You can see that we used a request code. This is used in order to differentiate between multiple requests code used for multiple activities. Besides the request code we have resultCode which identifies if the started activity acted as we wanted, by setting RESULT_OK before finishing.

   @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 1) {
            if (resultCode == RESULT_OK) {
                TextView textView = findViewById(R.id.textView);
                textView.setText(data.getStringExtra("text"));
            }
        }
    }

In the third activity get a reference to the added Button and write a click listener in which we take the text from the EditText and send it to the activity that started it. We do this by adding the text into a simple Intent object and by setting the wanted result code. In order to close this activity will call finish().

String text = editText.getText().toString().trim();
// We only send text back if there is any
if (TextUtils.isEmpty(text)) {
    Toast.makeText(ThirdActivity.this, "Insert text", Toast.LENGTH_SHORT).show();
} else {
    Intent intent = new Intent();
    intent.putExtra("text", text);
    setResult(RESULT_OK, intent);
    finish();
}

Intent Filters

In order to receive implicit intents we need to define in the app manifest an <intent-filter>. This specifies the type of intent that we support using action, data and category. If we have defined multiple intents and the system verifies that we support one of them it will send us the intent or add us in the chooser dialog screen. The intent filter is defined in the target component declaration (<activity>, <service>, <receiver>). As stated before we need to define the intent type using action, data and category using the name attribute:

Example:

<activity android:name="EditPhotoActivity">
	<intent-filter>
		<action android:name="android.intent.action.EDIT"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<data android:mimeType="image"/>
	</intent-filter>
</activity>

Multiple actions, categories and data can be used in an intent filter and an activity can contain multiple intent filters.

For security reasons you should avoid declaring intent-filters for components used just inside the app. You should use explicit intent for actions internal to your app. The reason is that intent-filters need to be declared in the manifest, therefore visible to anyone. A malicious app can then start your exported components (obtaining their names is not that difficult) using explicit intents

If we need to define an intent filter for just a period of time for a broadcast receiver, we can do it by registering it dynamically.

Broadcast Receivers

A Broadcast Receiver is an Android component that allows an Android application to intercept broadcast events and perform an action based on that event.

There are multiple types of receivers:

In Manifest declared receivers the system wakes the application by sending the intent to the app through the broadcast receiver. This can happen even when our app is not running. This is an example of receiver registered in the manifest:

<receiver android:name=".MyReceiver"
 android:exported="true/false"  // accessible/not accessible by other applications with different used id
 android:enabled="true/false" /> // can/cannot be registered by the system

A Context broadcast receiver is a receiver which is dynamically registered. In this case the receiver will live as long as the Activity is not destroyed or the receiver is unregistered. Also we can use the Application context, in this case the receiver will live as long our application is not destroyed.

If you define a Broadcast Receiver dynamically you have to pass to the registerReceiver() two objects: a receiver and an IntentFilter. The only condition is that we can not use an empty IntentFilter:

IntentFilter intentFilter = new IntentFilter();
registerReceiver(receiver, intentFilter);

In order for it to work we need to define an action. This will identify our broadcast receiver.

// MyReceiver.class.getName() - will return com.smd.lab2.MyReceiver
IntentFilter intentFilter = new IntentFilter(MyReceiver.class.getName());

Then in the broadcast intent we need to specify the action:

Intent intent = new Intent();
intent.setAction(MyReceiver.class.getName());
intent.putExtra("key", text);
sendBroadcast(intent);

If we want to create a receiver that only works inside our application we can use LocalBroadcastManager. This has the same methods registerReceiver(), sendBroadcast() and unregisterReceiver() and are used like this:

LocalBroadcastManager.getInstance(context).registerReceiver(receiver, intentFilter);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);

The difference is that the intent is sent and received only inside our app. When using Context.registerReceiver() or ApplicationContext.registerReceiver() we can receive broadcasts from outside the app as the receiver is globally registered, no matter if that is only for the lifetime of the activity or the app. By using this we avoid security concerns like other apps sending broadcasts to our receiver or other apps defining a receiver with the same action in which case when we send the broadcast intent it will be received by both receivers.

For all the types of broadcast receivers we need to create a class, for our example MyReceiver, and extend the BroadcastReceiver abstract class. Inside it we need to override the onReceive() method. This will be called when we receive the broadcast intent.

public class MyReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        String text = intent.getStringExtra("text");
        Toast.makeText(context, text, Toast.LENGTH_LONG).show();
    }
}

Task 6 - Broadcast receivers (2p)

Add a Broadcast Receiver to the project New > Other > Broadcast Receiver and name it MyReceiver. We will want to use this receiver to pop a simple message on the screen when clicking on the Send text to receiver button. The message will contain the text inserted in the EditText.

In order to achieve this, create a ClickListener with an Intent containing the context object and the receiver's class name. Add the text using putExtra. Afterwards, use sendBroadcast to send the intent to the BroadcastReceiver.

String text = editText.getText().toString().trim();
// We only send text back if there is any
if (TextUtils.isEmpty(text)) {
    Toast.makeText(MainActivity.this, "Insert text", Toast.LENGTH_SHORT).show();
} else {
    Intent i = new Intent(MainActivity.this, MyReceiver.class);
    i.putExtra("text", text);
    MainActivity.this.sendBroadcast(i);
}

Inside the receiver, in the onReceive method, retrieve the String added to the intent and display it using a toast notification.

Toast.makeText(context, text, Toast.LENGTH_LONG).show();

Do not use broadcast receivers to maintain state or to be interacted with. It “lives” only until the onReceive method finishes. Also, do not forget to register and unregister your broadcast receiver in the activity's lifecycle callbacks, e.g. in onPause and onResume.

Summary

Security concerns summary

  • Make sure to check your manifest for any components marked as exported by default when you created them. This flag tells the system that the component can be invoked by other apps (see info here, exploit example on broadcast receivers here)
  • If your broadcast only targets your app, then declare the receivers locally (see LocalBroadcastManager)
  • There are limitations to what implicit broadcasts are supported for manifest-declared broadcast receivers (see info here)

Resources

Layout: