Institute of Computer Science
  1. Courses
  2. 2020/21 fall
  3. Mobile Computing and Internet of Things (LTAT.06.009)
ET
Log in

Mobile Computing and Internet of Things 2020/21 fall

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

Table of contents

  • 1. Receiving phonecall Broadcasts
  • 2. Displaying calls in a ListView
  • 3. RecyclerView and Custom Adapter
  • Other information

Lab 4

In this lab we will use:

  • Broadcast receivers to receive incoming phonecalls
  • Adapters, ListView & RecyclerView to show a list of calls that is kept up to date.

1. Receiving Phonecall Broadcasts

  • Start a new Android Studio Project with a Blank Activity.

1.1 Creating a BroadcastReceiver

  • Create a separate class that extends BroadcastReceiver ( call it CallReceiver, for example).
    • Override the onReceive() method. Make the method body log the action of the Intent argument. Use intent.getAction()

In MainActivity, let's add code which registers and unregisters our CallReceiver.

  • Create a new instance of your CallReceiver class.
  • Then, create a new IntentFilter, this specifies which kind of broadcasts you are interested in receiving. Since we want to receive call-related events, we will use the action TelephonyManager.ACTION_PHONE_STATE_CHANGED 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 CallReceiver, 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 two permissions:

  1. android.permission.READ_PHONE_STATE
    • allows us to receive ACTION_PHONE_STATE_CHANGED broadcasts and check the state Extra attached to them.
  2. android.permission.READ_CALL_LOG
    • allows us to read the Extra containing the caller number from the same phone state changed broadcasts.
  • Add them to your manifest file using the <uses-permission> tag. It should be placed 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.

  • Try to run the application, make a phonecall to the device and verify that you can see the action in LogCat.

You will probably receive 2 events when the call is made, and another 2 when the call is declined, meaning you will see duplicate action values. This is due to the specifics of the framework for the TelephonyManager broadcast with these 2 permissions.

1.3 Handling Intent Extras in CallReceiver

Now let's implement the handling of the event (in CallReceiver.onReceive() )

  • The TelephonyManager.ACTION_PHONE_STATE_CHANGED may contain two different Extras:
    1. phone state (ringing, idle, in call)
    2. incoming number.
  • Read the documentation of TelephonyManager to find the exact values (keys) of these extras. Obtain their values using Intent.getExtra(.. ), Intent.hasExtra() is also useful.

Try to log the value of the state and number (if available)


2. Displaying calls in a list

We will now add some UI to our app, so that new incoming calls are immediately shown in a list. Think of it as a simplified "call log" app.

2.1 Storing call data in a list

First, we will start storing the calls in a List of Strings.

  • In MainActivity, create an empty list of type ArrayList<String>
    • Make sure it is defined a class property (member variable), not as a local variable declared within the scope of a function (such as onCreate()).
  • Second, create a function addPhoneNumberToList(phoneNumber: String) in MainActivity. It should append the number to the list.

Finally, let's make the CallReceiver invoke that function every time a call comes in with a phone number. Since we want to access a method of MainActivity from CallReceiver, let's add MainActivity as a class property to CallReceiver, and create a constructor that sets the value of this property.

  • In Kotlin, this can be done with very short syntax:
    class CallReceiver(var mainActivity: MainActivity) : BroadcastReceiver() { .. }
  • In Java, the above code corresponds to something like this: (Show Code)
public class JavaCallReceiver extends BroadcastReceiver {
        MainActivity mainActivity;
        public CallReceiver(MainActivity mainActivity) {
            this.mainActivity = mainActivity;
        }
        public MainActivity getMainActivity(){
             return mainActivity;
        }
        public void setMainActivity(MainActivity activity) {
             this.mainActivity = activity;
        }
    }

Now, you should be able to call mainActivity.addPhoneNumberToList(number) from the receiver.

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 addPhoneNumberToList(), to notify the adapter that the data has changed by calling adapter.notifyDatasetChanged() after adding the new list element.
    • To access the adapter from addPhoneNumberToList(), make sure you are defining it at the class-level.

The application should now be able to handle incoming calls and visually present the number 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.

  • First we have to add an additional library to our project. Open your build.gradle file (Module: app), and add this line to the dependencies { } block:
    implementation 'androidx.recyclerview:recyclerview:1.1.0'

After adding the dependency, make sure you sync the Gradle project (you should see a notification appear after changing the gradle file)!


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 call in our case.

  • Create a new class called CallViewHolder
    • 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 CallViewHolder(val item: View) : RecyclerView.ViewHolder(item)

Now let's implement the Adapter for RecyclerView

  • Create a new class called CallAdapter
  • 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>)

CallAdapter 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 CallAdapter extend RecyclerView.Adapter.
    • You have to provide the type of the ViewHolder the adapter will use, we will use the one we just created (CallViewHolder).
      • class CallAdapter(var numbers: ArrayList<String>): RecyclerView.Adapter<CallViewHolder>()

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 CallViewHolder.
    • CallViewHolder 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 CallViewHolder 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 make some calls, 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

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