Arvutiteaduse instituut
  1. Kursused
  2. 2021/22 sügis
  3. Mobiiliarvutus ja asjade internet (LTAT.06.009)
EN
Logi sisse

Mobiiliarvutus ja asjade internet 2021/22 sügis

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

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

  1. android.permission.RECEIVE_SMS Doc
    • allows us to receive SMS_RECEIVED_ACTION broadcasts.
  2. 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.

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

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:


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)

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

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)
  • Arvutiteaduse instituut
  • Loodus- ja täppisteaduste valdkond
  • Tartu Ülikool
Tehniliste probleemide või küsimuste korral kirjuta:

Kursuse sisu ja korralduslike küsimustega pöörduge kursuse korraldajate poole.
Õppematerjalide varalised autoriõigused kuuluvad Tartu Ülikoolile. Õppematerjalide kasutamine on lubatud autoriõiguse seaduses ettenähtud teose vaba kasutamise eesmärkidel ja tingimustel. Õppematerjalide kasutamisel on kasutaja kohustatud viitama õppematerjalide autorile.
Õppematerjalide kasutamine muudel eesmärkidel on lubatud ainult Tartu Ülikooli eelneval kirjalikul nõusolekul.
Courses’i keskkonna kasutustingimused