Institute of Computer Science
  1. Courses
  2. 2022/23 fall
  3. Mobile Application Development (LTAT.06.021)
ET
Log in

Mobile Application Development 2022/23 fall

  • Main
  • Lectures
  • Labs
  • Homeworks & Assignments
  • Task submission
  • Extra Materials
  • Best Practices
  • Results
  • Quizes
  • Projects
    • Presentations & Report
    • Teams & Topics
    • Grading & Submission

Table of contents

  • 1. Receiving SMS Broadcasts
  • 2. Displaying SMS-s in a ListView
  • 3. RecyclerView and Custom Adapter
  • Other information

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. Use intent.getAction()

In MainActivity, let's add code which registers and unregisters 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
  • 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 HW2).

1.2 Permissions

For this lab, we need the following permission:

  1. android.permission.RECEIVE_SMS Doc
    • allows us to receive SMS_RECEIVED_ACTION broadcasts.
    • Note: there exist similar ones, such as # android.permission.READ_SMS Doc

It allows us to read the SMS messages user has previously received.

  • Add android.permission.RECEIVE_SMS to your manifest file using the <uses-permission> tag.
    • Place them just outside of the <application> tag.
  • After updating AndroidManifest.xml, (re)install the app

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 the permission 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.
    • If you don't have an emulator or friend to SMS you, you can try a service like this to send yourself SMS

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.
  • SmsReceiver will invoke this interface method on every broadcast
  • MainActivity will implements the logic of this interface, and passes the implementation to SmsReceiver.
  • Create an interface named 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()

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)

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.
  • 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:

Tip: to avoid spending time on sending yourself SMS-es, you can update the messagesList to have some pre-filled values, e.g. using mutableListOf("lorem", "ipsum", "dolor")


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 list UI - what if we want to show multiple data items (e.g. contents and SMS time) in several 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)

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>)

SmsAdapter will extend RecyclerView.Adapter. Before continuing, let's see which methods RecyclerView will use for displaying data that we need to override and implement when extending RecyclerView.Adapter.

getItemCount(): Int -- total number of data elements in the dataset
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.
  • 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>()

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:
    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
  • 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)
  • Institute of Computer Science
  • Faculty of Science and Technology
  • University of Tartu
In case of technical problems or questions write to:

Contact the course organizers with the organizational and course content questions.
The proprietary copyrights of educational materials belong to the University of Tartu. The use of educational materials is permitted for the purposes and under the conditions provided for in the copyright law for the free use of a work. When using educational materials, the user is obligated to give credit to the author of the educational materials.
The use of educational materials for other purposes is allowed only with the prior written consent of the University of Tartu.
Terms of use for the Courses environment