In this lab, you will learn how to use Android Studio to create simple Android applications. You will also learn:

  1. The basics of Kotlin for Android
  2. XML layouts
  3. Event-driven programming with buttons and inputs.
  4. Network (Socket) programming in Android
  5. WiFi Direct programming with android

Prerequisites

Wi-Fi Direct (P2P) allows devices with the appropriate hardware to connect directly to each other via Wi-Fi without an intermediate access point

Popular uses are

The WifiP2pManager class provides methods to enable you to interact with the Wi-Fi hardware on your device to do things like discover and connect to peers. The following actions are available:

Method

Description

initialize()

Registers the application with the Wi-Fi framework. Call this before calling any other Wi-Fi P2P method.

connect()

Starts a peer-to-peer connection with a device with the specified configuration.

cancelConnect()

Cancels any ongoing peer-to-peer group negotiation.

requestConnectInfo()

Requests a device's connection information.

createGroup()

Creates a peer-to-peer group with the current device as the group owner.

removeGroup()

Removes the current peer-to-peer group.

requestGroupInfo()

Requests peer-to-peer group information.

discoverPeers()

Initiates peer discovery.

requestPeers()

Requests the current list of discovered peers.

Listener interface

Associated actions

Description

WifiP2pManager.ActionListener

connect(), cancelConnect(), createGroup(), removeGroup(), and discoverPeers()

The ActionListener is an interface that defines 2 functions onSuccess and onFailure that tells you if the call completed successfully or not

WifiP2pManager.ChannelListener

initialize()

This listener is an interface that notifies when the framework channel is lost

WifiP2pManager.ConnectionInfoListener

requestConnectInfo()

This listener is an interface that notifies you when information on the connection is available (If the group was formed, address of the GO, if the device is the GO, )

WifiP2pManager.GroupInfoListener

requestGroupInfo()

This is an interface that notifies when the group information is available (Get network ID name and password, and whether this device is the GO )

WifiP2pManager.PeerListListener

requestPeers()

This is an interface that calls notifies on when the list of devices has updated

The Wi-Fi P2P APIs define intents that are broadcast when certain Wi-Fi P2P events happen, such as when a new peer is discovered or when a device's Wi-Fi state changes. You can register to receive these intents in your application by creating a broadcast receiver that handles these intents:

Intent

Description

WIFI_P2P_CONNECTION_CHANGED_ACTION

Broadcast when the state of the device's Wi-Fi connection changes.

WIFI_P2P_PEERS_CHANGED_ACTION

Broadcast when you call discoverPeers(). You will usually call requestPeers() to get an updated list of peers if you handle this intent in your application.

WIFI_P2P_STATE_CHANGED_ACTION

Broadcast when Wi-Fi P2P is enabled or disabled on the device.

WIFI_P2P_THIS_DEVICE_CHANGED_ACTION

Broadcast when a device's details have changed, such as the device's name.

Objective

The objective of this app is to create an echo server. An echo server is one that receives an input from a client, reverses the message and sends the message back to the client. This app will make use of multiple activities to maintain logical separation of concerns

Steps

  1. Create a new project named EchoServerWifiDirect

At the END of the lab, the file structure should look like this:

app/src/main
    -  java+kotlin/com/example/echoserverwifidirect
        - chatlist
        -  ChatListAdapter.kt
        - models
        -  ContentModel.kt
        - network
        -  Client.kt
        -  NetworkMessageInterface.kt
        -  Server.kt
        - peerlist
        -  PeerListAdapter.kt
        -  PeerListAdapterInterface.kt
        - wifidirect
        -  WifiDirectInterface.kt
        -  WifiDirectManager.kt
        PermissionActivity.kt(This is the usual MainActivity)
        CommunicationActivity.kt
    -  res/layout
        - activity_communication.xml
        - activity_permission.xml
        - chat_item.xml
        - peer_item.xml

The logic of the application takes into consideration all possible states.

App Logic

User Interface: Wireframe

The first section of this lab deals with permissions. We'd create an Activity (Page/Screen) that's dedicated to permissions as the app will not function without it.

Permissions not granted

Permissions

In general, the permissions your app is requesting should be listed in the AndroidManifest.xml file

Request permission to use the Wi-Fi hardware on the device and declare your application to have the correct minimum SDK version in the Android manifest:

AndroidManifext.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" >

    <!-- Add these lines below -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission
        android:name="android.permission.NEARBY_WIFI_DEVICES"
        tools:targetApi="s" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <!-- Add the lines above -->
    <application
    REDACTED FOR BREVITY

Requesting Permissions

Request the permissions of the app. Rename the MainActivity to PermissionActivity by right-clicking > refactor > rename.

The permissions should be checked every time the application is launched in the onCreate. If the app does not have all the permissions, then we should request them from the OS

class MainActivity : AppCompatActivity() {

    /// The [requestCode] variable acts as an identifier for the app that's requesting the permissions.
    private val requestCode = 1234

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_permission)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        // If we do not have our permissions, we need to request them. Create an array of the permissions we want,
        // then send a request to the android OS
        if (!hasAllPermissions()){
            var perm = arrayOf(
                Manifest.permission.ACCESS_WIFI_STATE,
                Manifest.permission.CHANGE_WIFI_STATE,
                Manifest.permission.INTERNET,
                Manifest.permission.ACCESS_FINE_LOCATION,
            )
            if (SDK_INT >= 33){
                // Android 13 (API 33) requires the NEARBY_WIFI_DEVICES permission
                perm +=Manifest.permission.NEARBY_WIFI_DEVICES
            }

            ActivityCompat.requestPermissions(this, perm, requestCode)

        } else {
            // If i do have permissions, then i can navigate to the next page
            navigateToNextPage()
        }
    }
}

Implement the hasAllPermissions function

//This is a function inside PermissionActivity
private fun hasAllPermissions():Boolean{
    var perm = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
            checkSelfPermission(Manifest.permission.ACCESS_WIFI_STATE) == PackageManager.PERMISSION_GRANTED &&
            checkSelfPermission(Manifest.permission.CHANGE_WIFI_STATE) == PackageManager.PERMISSION_GRANTED &&
            checkSelfPermission(Manifest.permission.INTERNET) == PackageManager.PERMISSION_GRANTED
    if (SDK_INT >= 33){
        // If we're running on android SDK 33 or higher, we also need the NEARBY_WIFI_DEVICES permission
        perm = perm && checkSelfPermission(Manifest.permission.NEARBY_WIFI_DEVICES) == PackageManager.PERMISSION_GRANTED
    }
    return perm
}

Handling permissions

Handle the callback from the android OS when permissions are granted or denied

   /// This function is called by the OS itself after the user interacts with the permissions popups.
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        when(requestCode){
            this.requestCode -> {
                if (hasAllPermissions()){
                    navigateToNextPage()
                }
            }
            else -> {
                super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            }
        }
    }

Handling the edge case

If a user repeatedly denies the permission to the application, They may be required to manually grant it in the application settings. This would involve the user moving away from the app and then coming back to it. With our current setup, we have no way of capturing this edge case. To solve that lets override the onResume function.

// This is a function inside the PermissionActivity
override fun onResume() {
    super.onResume()
    if (hasAllPermissions()){
        navigateToNextPage()
    }
}

The onResume function runs whenever the app is resumed from the background. And thus, with this implementation, will be able to properly redirect the user when the app's permissions are granted using the settings.

Implement the functionality to go to settings

// This is a function inside PermissionActivity
    fun goToSettings(view: View) {
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
        val uri = Uri.fromParts("package", packageName, null)
        intent.setData(uri)
        startActivity(intent)
    }

Implement the functionality to navigate to the next page

// This is a function inside PermissionActivity
    private fun navigateToNextPage(){
        print("I should go to the nex page")
    }

UI Implementation

Tasks

  1. Rename the activity_main.xml to activity_permission.xml. (And make the change to the line in the onCreate)
  2. Implement the design
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_margin="32dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".PermissionActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="No Permissions"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.4" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:gravity="center"
        android:text="Unfortunately you have not granted the necessary permissions to use this app. Please grant these settings manually using the button below!"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="64dp"
        android:onClick="goToSettings"
        android:text="Go to Settings"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.526"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />


</androidx.constraintlayout.widget.ConstraintLayout>

Now that our permission handling is out of the way, we can focus on the remaining logic. The next step in the process is to handle changes to WifiDirect.

Consider the following questions

  1. Can you create a socket if the Wifi adapter is off?
  2. can you communicate without being connected to a WiFi Direct group?
  3. How does a device find nearby devices?
  4. What happens if a device wants to start a new group with itself as the GO?

In this section, we cater for all of those scenarios in the app.

Create a new activity

  1. Create a new activity called CommunicationActivity. (right click folder with kotlin files > new > empty views activity)
  2. Modify the code in PermissionActivity to navigate to this activity once the conditions are satisfied
    private fun navigateToNextPage(){
        val i = Intent(this,CommunicationActivity::class.java)
        i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
        startActivity(i)
    }

Handling WiFi Direct

While there are many ways to handle WiFi Direct, we're going to leverage BroadcastReceivers to automatically receive the information from the operating system.

  1. Create a new folder(package) inside the app called wifidirect
  2. Create a new class called WifiDirectManager in that folder
  3. Create a new interface called WifiDirectInterface in that folder

The WifiDirectManager class will contain the code necessary to perform all WiFi direct operations like

The WifiDirectInterface class will list functions that its implementing member MUST implement. The idea here is that the core functionality will be implemented in the CommunicationActivity. i.e. CommunicationActivity will implement WifiDirectInterface

Define the interface

The information that we're interested in from wifiDirect are as follows

The interface will be defined as follows

interface WifiDirectInterface {
    fun onWiFiDirectStateChanged(isEnabled:Boolean)
    fun onPeerListUpdated(deviceList: Collection<WifiP2pDevice>)
    fun onGroupStatusChanged(groupInfo: WifiP2pGroup?)
    fun onDeviceStatusChanged(thisDevice: WifiP2pDevice)
}

Implement the Broadcast Receiver

The broadcast receiver is responsible for listening to events from the operating system and performing actions accordingly.

Implement the WifiDirectManager class as follows

/// This [WifiDirectManager] class is a [BroadcastReceiver] that listens for events fired from the
/// operating system and relays the relevant information using the [WiFiDirectInterface] to the
/// [CommunicationActivity]
class WifiDirectManager(
    private val manager: WifiP2pManager,
    private val channel: WifiP2pManager.Channel,
    private val iFaceImpl: WifiDirectInterface
):BroadcastReceiver() {
    var groupInfo: WifiP2pGroup? = null

    @SuppressLint("MissingPermission")
    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
                val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)
                val isWifiP2pEnabled = state == WifiP2pManager.WIFI_P2P_STATE_ENABLED
                iFaceImpl.onWiFiDirectStateChanged(isWifiP2pEnabled)
                Log.e("WFDManager","The WiFi direct adapter state has changed to $isWifiP2pEnabled")
            }

            WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
                manager.requestPeers(channel) { peers: WifiP2pDeviceList? ->
                    peers?.deviceList?.let { iFaceImpl.onPeerListUpdated(it) }
                    Log.e("WFDManager","The peer listing has changed")
                }
            }
            WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
                val wifiP2pInfo = when{
                    Build.VERSION.SDK_INT >= 33 -> intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO, WifiP2pInfo::class.java)!!
                    else -> @Suppress("DEPRECATION") intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO)!!
                }
                val tmpGroupInfo = when{
                    !(wifiP2pInfo.groupFormed)->null
                    Build.VERSION.SDK_INT >= 33 -> intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP, WifiP2pGroup::class.java)!!
                    else -> @Suppress("DEPRECATION") intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)!!
                }
                if (groupInfo != tmpGroupInfo){
                    groupInfo = tmpGroupInfo
                    Log.e("WFDManager","The group status has changed")
                    iFaceImpl.onGroupStatusChanged(groupInfo)
                }


            }
            WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
                val thisDevice = when{
                    Build.VERSION.SDK_INT >= 33 -> intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE, WifiP2pDevice::class.java)!!
                    else -> @Suppress("DEPRECATION") intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE)!!
                }
                Log.e("WFDManager","The device status has changed")

                iFaceImpl.onDeviceStatusChanged(thisDevice)
            }
        }
    }

    @SuppressLint("MissingPermission")
    fun createGroup(){
        manager.createGroup(channel, object : ActionListener {
            override fun onSuccess() {
                Log.e("WFDManager","Successfully created a group with myself as the GO")
            }

            override fun onFailure(reason: Int) {
                Log.e("WFDManager","An error occurred while trying to create a group")
            }

        })
    }

    @SuppressLint("MissingPermission")
    fun connectToPeer(peer: WifiP2pDevice) {
        val config = WifiP2pConfig()
        config.deviceAddress = peer.deviceAddress
        manager.connect(channel, config, object : ActionListener {
            override fun onSuccess() {
                Log.e("WFDManager","Successfully attempted to connect to a peer '${peer.deviceName}'")
            }

            override fun onFailure(reason: Int) {
                Log.e("WFDManager","An error occurred while trying to connect to a peer '${peer.deviceName}'")
            }

        })
    }

    @SuppressLint("MissingPermission")
    fun discoverPeers(){
        manager.discoverPeers(channel, object : ActionListener {
            override fun onSuccess() {
                Log.e("WFDManager","Successfully attempted to discover peers")
            }

            override fun onFailure(reason: Int) {
                Log.e("WFDManager","An error occurred while trying to discover peers")
            }
        })
    }

    fun disconnect(){
        manager.removeGroup(channel, object : ActionListener {
            override fun onSuccess() {
                Log.e("WFDManager","Successfully disconnected from the group")
            }
            override fun onFailure(reason: Int) {
                Log.e("WFDManager","An error occurred while trying to disconnect from the group")
            }

        })
    }
}

Integrate the WifiDirect classes

Inside CommunicationActivity add the following to the onCreate

    val manager: WifiP2pManager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
    val channel = manager.initialize(this, mainLooper, null)
    wfdManager = WifiDirectManager(manager, channel, this)

Implement the WifiDirect Interface in CommunicationActivity

class CommunicationActivity : AppCompatActivity(), WifiDirectInterface{

Implement the members

    override fun onWiFiDirectStateChanged(isEnabled: Boolean) {
        var text = "There was a state change in the WiFi Direct. Currently it is "
        text = if (isEnabled){
            "$text enabled!"
        } else {
            "$text disabled! Try turning on the WiFi adapter"
        }

        val toast = Toast.makeText(this, text, Toast.LENGTH_SHORT)
        toast.show()
        updateUI()
    }

   override fun onPeerListUpdated(deviceList: Collection<WifiP2pDevice>) {
        val toast = Toast.makeText(this, "Updated listing of nearby WiFi Direct devices", Toast.LENGTH_SHORT)
        toast.show()
        updateUI()
    }

    override fun onGroupStatusChanged(groupInfo: WifiP2pGroup?) {
        val text = if (groupInfo == null){
            "Group is not formed"
        } else {
            "Group has been formed"
        }
        val toast = Toast.makeText(this, text , Toast.LENGTH_SHORT)
        toast.show()

    }

    override fun onDeviceStatusChanged(thisDevice: WifiP2pDevice) {
        val toast = Toast.makeText(this, "Device parameters have been updated" , Toast.LENGTH_SHORT)
        toast.show()
    }

Implement the UI

The logic for the UI for this page is quite complex and follows the following algorithm

    IF the WFD adapter is off (disalbed) THEN
        SHOW the UI that says we need to turn on the adapter
    ELSE IF there is NO WFD connection THEN
        SHOW options that would allow the user to either CREATE A GROUP or DISCOVER NEARBY GROUPS
    ELSE IF there are nearby groups THEN
        SHOW elibigble devices in list
    ELSE IF there is a Wfd connection
        SHOW chat interface

Let's handle the first scenario. i.e. Displaying an appropriate interface if the adapter is enabled/disabled

The WifiDirectManager class alerts us on whenever our adapter state has changed from enabled to disabled and vice versa. We can use the onWiFiDirectStateChanged method to handle this.

private var wfdAdapterEnabled = false

add the following line in the onWifiDirectStateChanged method wfdAdapterEnabled = isEnabled

Implement the updateUI call

        val wfdAdapterErrorView:ConstraintLayout = findViewById(R.id.clWfdAdapterDisabled)
        wfdAdapterErrorView.visibility = if (!wfdAdapterEnabled) View.VISIBLE else View.GONE

The definition of the constraint layout in the activity_communication.xml is as follows

<androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/clWfdAdapterDisabled"
        android:padding="32dp"
        android:visibility="gone"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <TextView
            android:id="@+id/tvWfdError"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="WiFi Direct - Error"
            android:textSize="18sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="It seems your WiFi Direct Adapter is off. Please ensure that you turn ON WiFI in your settings!"
            android:textSize="11sp"
            android:gravity="center"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tvWfdError" />


    </androidx.constraintlayout.widget.ConstraintLayout>
  1. Dont have a connection
  2. Discovered peers
  3. Have a connection

Handling when we don't have a connection

Create a new instance variable called wfdHasConnection

Modify the onGroupStatusCHanged implementation. We're going to say that we have a connection if groupInfo is not null wfdHasConnection = groupInfo != null

Modify the updateUI function and add the following

        val wfdNoConnectionView:ConstraintLayout = findViewById(R.id.clNoWifiDirectConnection)
        wfdNoConnectionView.visibility = if (wfdAdapterEnabled && !wfdHasConnection) View.VISIBLE else View.GONE

The constraint Layout for this will be as follows

<androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/clNoWifiDirectConnection"
        android:padding="32dp"
        android:visibility="gone"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <TextView
            android:id="@+id/tvWfdDisconnected"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="WiFi Direct - Disconnected"
            android:textSize="18sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btnCreateGroup"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="62dp"
            android:layout_marginEnd="50dp"
            android:onClick="createGroup"
            android:text="Create Group"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintVertical_chainStyle="packed"
            app:layout_constraintEnd_toStartOf="@+id/btnDiscover"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tvWfdDisconnected" />

        <Button
            android:id="@+id/btnDiscover"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="62dp"
            android:onClick="discoverNearbyPeers"
            android:text="Discover Nearby"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/btnCreateGroup"
            app:layout_constraintTop_toBottomOf="@+id/tvWfdDisconnected" />


        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rvPeerListing"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="48dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btnCreateGroup" />
    </androidx.constraintlayout.widget.ConstraintLayout>

Handling peer list updates

Add a state variable hasDevices

private var hasDevices = false

Update the onPeerListUpdated function

        hasDevices = deviceList.isNotEmpty()

Update the updateUI function

        val rvPeerList: RecyclerView= findViewById(R.id.rvPeerListing)
        rvPeerList.visibility = if (wfdAdapterEnabled && !wfdHasConnection && hasDevices) View.VISIBLE else View.GONE

Implement the PeerListAdapterInterface

interface PeerListAdapterInterface {
    fun onPeerClicked(peer:WifiP2pDevice)
}

Implement the PeerListAdapter

class PeerListAdapter(private val iFaceImpl:PeerListAdapterInterface): RecyclerView.Adapter<PeerListAdapter.ViewHolder>() {
    private val peersList:MutableList<WifiP2pDevice> = mutableListOf()

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
        val descriptionTextView: TextView = itemView.findViewById(R.id.descriptionTextView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.peer_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val peer = peersList[position]

        holder.titleTextView.text = peer.deviceName
        holder.descriptionTextView.text = peer.deviceAddress

        holder.itemView.setOnClickListener {
            iFaceImpl.onPeerClicked(peer)
        }
    }

    override fun getItemCount(): Int {
        return peersList.size
    }

    @SuppressLint("NotifyDataSetChanged")
    fun updateList(newPeersList:Collection<WifiP2pDevice>){
        peersList.clear()
        peersList.addAll(newPeersList)
        notifyDataSetChanged()
    }
}

Add the adapter as an instance member to the COmmunicationActivity class

private var peerListAdapter:PeerListAdapter? = null

Add the following code at the end of the onCreate

        peerListAdapter = PeerListAdapter(this)
        val rvPeerList: RecyclerView= findViewById(R.id.rvPeerListing)
        rvPeerList.adapter = peerListAdapter
        rvPeerList.layoutManager = LinearLayoutManager(this)

The completed source code