Table of contents
Lab 4
In this lab we will use:
- Broadcast receivers to receive incoming SMS messages.
- Adapters, ListView & RecyclerView to show a list of messages that is kept up to date as new messages arrive.
1. Receiving SMS Broadcasts
- Download and open this Base Android Studio Project
BroadcastReceiver
- The project includes a class SmsReceiver that extends BroadcastReceiver.
- Update its
onReceive()
method. Make it log the action of the Intent argument. Useintent.getAction()
- Update its
In MainActivity, let's add code which registers and unregisters our SmsReceiver.
- Create a new instance of your SmsReceiver class.
- Then, create a new IntentFilter, this specifies which kind of broadcasts you are interested in receiving. Since we want to receive SMS-related events, we will use the action Telephony.Sms.Intents.SMS_RECEIVED_ACTION in our IntentFilter.
- You can provide the action either in the constructor of IntentFilter or use the
addAction(..)
method of IntentFilter
- You can provide the action either in the constructor of IntentFilter or use the
- Use the registerReceiver(..) and unregisterReceiver(..) methods of Context, to activate your SmsReceiver, also passing the IntentFilter as the other argument.
- You should decide when to unregister the receiver based on where you registered it. The registering and unregistering should be done in a pair corresponding to lifecycle methods.
- Registering in onCreate() and unregistering in onDestroy() is one reasonable approach.
On the other hand if you register in onCreate() but unregister in onPause(), you will get unexpected behaviour, as onPause is likely to be called more often than onCreate - e.g. if the user puts your app in the background by using the "Home" button, your receiver is unregistered, but when the user returns to the app immediately afterwards, your receiver does not get re-registered since onCreate() is not called in such situation (recall HW1).
1.2 Permissions
For this lab, we need two permissions:
android.permission.RECEIVE_SMS
Doc- allows us to receive
SMS_RECEIVED_ACTION
broadcasts.
- allows us to receive
android.permission.READ_SMS
Doc- allows us to read the Extra containing the SMS message contents
- Add them to your manifest file using the
<uses-permission>
tag.- Place them just outside of the
<application>
tag.
- Place them just outside of the
As discussed in lecture 4, "dangerous" permissions require the user to explicitly give permissions, e.g. via a pop-up dialogue. For now, let's manually grant these permissions from the Android device's Settings:
- In the phone, find your app in the app drawer. Long-click on the icon, "App info" should appear, click on it. There you will find "Permissions", under which you can manually grant permissions.
At the end of this lab material you can also read how to grant permissions manually using the command line or how to programmatically request them from the user.
- Try to run the application, send a SMS to the device and verify that you can see the action in LogCat.
1.3 Handling Intent Extras in SmsReceiver
Now let's implement the handling of the event (in SmsReceiver.onReceive() )
- The Telephony.Sms.Intents.SMS_RECEIVED_ACTION will contains some Extra data:
- pdus - An Object[] of byte[]s containing the PDUs that make up the message
- Luckily, Android also has a helper method for parsing the byte array of the PDU message into a SmsMessage[] getMessagesFromIntent( .. ).
- Use getMessagesFromIntent( ) to get an SmsMessage and log the body and origin of the message.
2. Displaying SMS-s in a list
We will now add some UI to our app, so that new incoming messages are immediately shown in a list. Think of it as a simplified "SMS log" app.
2.1 Storing call data in a list
First, we will start storing the message sender numbers in a List of Strings. for this, the MainActivity of the base project includes a class property messagesList''
Next, we want MainActivity to add the SMS data to the list upon every broadcast.
- We will define an interface called MessageHandler with 1 abstract method.
- MainActivity implements the logic of this interface, and passes the implementation to SmsReceiver.
- SmsReceiver will invoke this interface method on every broadcast
- Create an interface
MessageHandler
in SmsReceiver.- Interface should define one method
fun onSms(smsMessage: SmsMessage)
- Update the constructor of SmsBroadcastReceiver so that it accepts an implementation of this interface as an argument:
SmsReceiver(private val smsHandler: MessageHandler): BroadcastReceiver()
- Interface should define one method
Note: the above syntax introduces smsHandler as a class property to SmsBroadcastReceiver. This is very short syntax compared how to get a similar result in Java, where the above code corresponds to something like this: (Show Code)
public class JavaSmsReceiver extends BroadcastReceiver { private MessageHandler smsHandler; public CallReceiver(MessageHandler smsHandler) { this.smsHandler = smsHandler; } }
- in onReceive(..), invoke the interface method with SmsMessage
smsHandler.onSms( smsMessage)
- in onReceive(..), invoke the interface method with SmsMessage
Now, let's implement this interface in MainActivity
- In MainActivity,
val smsHandler = object : SmsReceiver.MessageHandler { override fun onSms(message: SmsMessage) { // TODO: add message to smsList } }
- Update the initialization of SmsReceiver in MainActivity to match the new constructor.
Now, MainActivity's smsHandler code gets run with the incoming messages, and can add each one to the List of messages.
2.2 ListView & Adapter
- Add a ListView to your MainActivity XML layout file. Set its' ID as
numbers_listview
. - In MainActivity.kt, create an ArrayAdapter
- Specify as the 2nd argument
android.R.layout.simple_list_item_1
, which is a default layout from Android SDK that consists of one TextView - For the 3rd argument pass the list object we created earlier.
- Specify as the 2nd argument
- Set the ListView to use the Adapter by calling
numbers_listview.adapter = myAdapterName
- Update your smsHandler.onSms() implementation to add the SmsMessage.originatingAddress to the smsList , and notify the adapter that the data has changed by calling
adapter.notifyDatasetChanged()
after adding the new list element.- To access the adapter from onSms(), make sure the adapter is defined at the class-level.
The application should now be able to handle incoming SMS-es and visually present the numbers in the ListView:
3. Custom Adapter with RecyclerView
Above, we used a pre-defined View and ArrayAdapter to display the text items in a ListView. ArrayAdapter displays the individual elements in layouts with exactly 1 TextView. Sometimes we need more control over our UI - what if we want to show multiple data items (e.g. number and call time) in multiple Views. What if we want to include different kinds of Views, not just TextView? To manage how data is mapped to some views, we can create our own adapter.
Secondly, a more modern and efficient replacement for ListViews is RecyclerView. Let's re-create the list using a RecyclerView and implement our own Adapter.
RecyclerViews use ViewHolders to represent individual items in a dataset. The ViewHolder should contain a View object (this can be a single View like TextView or a Layout containing multiple Views). Let's create the ViewHolder for a single list item which is a single sms in our case.
- Create a new class called SmsViewHolder
- It's primary constructor should accept 1 argument:
val item: View
- It should extend
RecyclerView.ViewHolder
, and pass the item property to the superclass constructor- Hint:
class SmsViewHolder(val item: View) : RecyclerView.ViewHolder(item)
- Hint:
- It's primary constructor should accept 1 argument:
Now let's implement the Adapter for RecyclerView
- Create a new class called SmsAdapter
- Make its primary constructor have 1 parameter of type List<String> called numbers, set it as a class property.
- Hint:
CustomArrayAdapter(var numbers: List<String>)
- Hint:
SmsAdapter should extend RecyclerView.Adapter. When extending RecyclerView.Adapter, we need to override the implemetation of 3 methods that RecyclerView for displaying data:
- onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder -- Creates new ViewHolder instances. Here, you may inflate some XML to initialize the View the ViewHolder contains. RecyclerView creates just enough ViewHolders to display that portion of the dataset which is currently visible (and a few extra for buffer), as RecyclerView re-uses ViewHolder objects.onBindViewHolder(holder: MyViewHolder, position: Int) -- Given the position of a single item in the dataset, this method updates the View of a ViewHolder with the correct data for that item. E.g. sets some TextViews, display an icon based on the value, etc. This gets called more frequently than onCreateViewHolder(..), as RecyclerView tries to re-use existing ViewHolders when possible.getItemCount(): Int -- total number of data elements in the dataset
- Make SmsAdapter extend
RecyclerView.Adapter
.- You have to provide the type of the ViewHolder the adapter will use, we will use the one we just created (SmsViewHolder).
- class SmsAdapter(var numbers: ArrayList<String>): RecyclerView.Adapter<SmsViewHolder>()
- You have to provide the type of the ViewHolder the adapter will use, we will use the one we just created (SmsViewHolder).
Now override the above-mentioned 3 methodsif RecyclerView.Adapter:
- For getItemCount(..), return the size of the numbers list (this is our dataset).
- For onCreateViewHolder(..) return an instance of SmsViewHolder.
- SmsViewHolder has a View object (
listItem
) as a field, it describes the View to show in the actual List for a single dataset item. We should initialize it here. Let's inflate it from XML:
- SmsViewHolder has a View object (
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_row, parent, false)
- In the above code
R.layout.list_row
is a new Layout XML, you have to create it. Create a simple horizontal LinearLayout that contains 1 TextView
- In the above code
- For onBindViewHolder( ..) , you should access the View object contained in the ViewHolder and update it to display the data at the provided position.
- Find the right phone number for the given position
- Update the TextView contained within the view of SmsViewHolder instance available from the method argument
Now we have finished our Adapter. Let's add a RecyclerView to our MainActivity Layout and configure it to use the adapter.
- Add a RecyclerView to your MainActivity XML layout.
In onCreate() of MainActivity:
- update the layoutmanager of your RecyclerView
myRecyclerView.layoutManager = LinearLayoutManager(this)
- Set the adapter of your RecyclerView
recyclerView.adapter = MyAdapter(numbersList)
Run the app and send some SMS, you should be able to see your RecyclerView working.
Other information
Background reading: About Kotlin class fields and class constructors
- This Android Guide has some examples about how & why we use can use lateinit keyword when creating variables
- Kotlin docs about class constructors
Writing code for handling permission requests
To implement pop-up dialogs which request the user to grant required permissions, refer here
How to grant an app permissions via ADB commandline.
Using Android Debug Bridge (ADB), you can send various commands to an Android Device (physical or emulator), it is how the app is installed to the device after compilation. Using ADB, it is also possible to manually grant an application permissions, e.g. :
adb shell pm grant ee.ut.cs.lab4app android.permission.READ_CALL_LOG
this grants the READ_CALL_LOG permission
to the app with package ee.ut.cs.lab4app
.
You can find the adb binary located in your Android SDK installation directory:
<<AndroidSDKInstallDirectory>>/Sdk/platform-tools/adb(.exe)