In this lab we will:
- use broadcast receivers to receive incoming phonecalls
- use adapters and ListView to show a list of calls that is kept up to date
1. Receiving Phonecalls
1.1 Create a BroadcastReceiver for incoming phone call events
- Create a separate class that extends BroadcastReceiver and override the onReceive() method (Call it CallReceiver, for example). Leave onReceive() empty for now.
- In MainActivity, add code which registers and unregisters an instance of CallReceiver.
- Since we want to receive call-related events, we have to specify the action PHONE_STATE_CHANGED to our IntentFilter ( TelephonyManager.ACTION_PHONE_STATE_CHANGED )
- You should decide when to unregister the receiver based on where you registered it. E.g., if you register in onCreate but unregister in onPause(), you will get unexpected behaviour since the receiver is not re-registered if the app is briefly sent into the background. Registering in onCreate() and unregistering in onDestroy is one reasonable approach().
1.2 Permissions
The ACTION_PHONE_STATE_CHANGED
broadcast requires two permissions:
android.permission.READ_PHONE_STATE
android.permission.READ_CALL_LOG
- Add them to your manifest file using the <uses-permission> tag. It should be placed outside of the <application> tag.
- As discussed in lecture 4, Dangerous permissions require the user to explicitly give the permission through UI. For now, let's grant these permissions from Android 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 the above permissions.
1.3 Handling Intent Extras in CallReceiver
Now let's implement the handling of the event (in CallReceiver.onReceive() )
- Add code which logs the action of the received intent using
intent.getAction()
- 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. All of them have the same action value.
- The TelephonyManager.ACTION_PHONE_STATE_CHANGED may contain two different Extras - the phone state (ringing, idle, in call) and the incoming number. Read the documentation to find the exact values (keys) of these extras.
- Obtain the extras 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 MutableList<String>, create it as a class field (member variable), as opposed to a variable declared within the scope of a function (such as onCreate()).
- Second, create a function "addPhoneNumberToList(phoneNumber: String)" in MainActivity. This should update the contents of the list.
- Finally, make the CallReceiver call 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 it as a class property, and create a constructor that accepts MainActivity.
- 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:
public class JavaCallReceiver extends BroadcastReceiver { MainActivity mainActivity; public JavaCallReceiver(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 layout file. Let's 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, and as the 3rd argument pass list objet we created earlier. - Set the ListView to use the Adapter by calling
numbers_listview.adapter = myAdapterName
- In your addPhoneNumberToList(), you should call adapter.notifyDatasetChanged() after adding the new list element.
- To access the adapter from addPhoneNumberToList(), you have to declare it as a class field.
The application should now be able to handle incoming calls and log the number
3. Custom Adapter for ListView
Aboce, we used a pre-defined View and Adapter to display the text items. 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?
For this, we can implement our own Adapter, by extending BaseAdapter. When extending BaseAdapter, we have to implement the following methods that the ListView will use to fetch data for display:
- getItem(position) : String -- the data object at given position index
- getItemId(position): Long -- unique id of object at position
- getCount(): Int -- total number of data elements
- getView(position, convertView, parent) : View -- the View object to show for a item in the dataset
- Create a new class CustomAdapter, make it extend BaseAdapter.
- Add a constructor parameter of type List<String> called objects, set it as a class property.
- Hint:
CustomArrayAdapter(var objects: List<String>) : BaseAdapter()
- Hint:
- Implement the above mentioned methods. For getItem and getCount, you can directly return the element from the list or the list size. for ItemId, you can return the elements hashcode,
objects[index].hashCode().toLong()
- For getView(..) you have to return a View object. One of the arguments is convertView - this may be null or contain an old object view object that can be re-used. Alternatively, you can inflate a view based on some XML like so:
val view: View if (convertView == null) { val layoutInflater = LayoutInflater.from(parent?.context) view = layoutInflater.inflate(R.layout.custom_list_item, parent, false) } else{ view = convertView }
- In the above code R.layout.custom_list_item is a new Layout XML, you have to create it. Create a simple horizontal LinearLayout that contains 1 TextView
- Still inside the getView() of CustomAdapter, set the content of the TextView :
view.findViewById<TextView>(R.id.number_textview).text = item
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