Android 에서  socket을 이용한 2인용 게임 개발중이다

 

소켓을 이용해서 메시지를 두번 보낼 때 빠진 메시지가 있어 Log를 검토해 보았다

 

game code

if (tempPoint.y < 0 || tempPoint.y > 500) {
    isPaused = true
    gameData.resetData()
    sendMessageToClientViaSocket("SERVER_STOPPED_GAME")
    messageCallback.onGameStateMessageFromThread(GameState.STOPPED)
    if(tempPoint.y <0) {
        sendMessageToClientViaSocket("SERVER_WIN")
        messageCallback.onGameWinnerFromThread(true)
    } else {
        sendMessageToClientViaSocket("CLIENT_WIN")
        messageCallback.onGameWinnerFromThread(false)
    }
    return
}

 

sendMesssage

fun sendMessageToClientViaSocket(message: String): Unit {
    try{
        if (connectedSocket != null && connectedSocket?.isConnected == true) {
            CoroutineScope(Dispatchers.IO).launch {
                outputStream?.write(message.toByteArray())
            }
        }
    } catch(e:Exception) {
        Log.e(">>>>","sendMessageToClientViaSocket@serverSocketThread exception : ${e.message}")
    }
}

 

Client의 SocketThread에서 Log를 보면 간간히 한번씩 두개의 Message가 합해져 전달 되고 이것 때문에 정확한 상태 전달이 이루어 지지 않았다

 

concatenated Log

2024-04-25 13:03:07.409 32518-395   >>>>                    com.example.lamb0693.p2papp          I  ClientThread ReceivedMessage : SERVER_STOPPED_GAMESERVER_WIN
2024-04-25 13:03:07.410 32518-395   >>>>                    com.example.lamb0693.p2papp          I  onOtherMessageReceivedFromServerViaSocket : SERVER_STOPPED_GAMESERVER_WIN

 

원래 이렇게 되는 경우가 TCP전달에 있을 수 있다고 하는 것 같다

 

코드를 수정했다

message를 보낼때 끝에 "\n"을 delimeter 용으로 넣고

 

fun sendMessageToClientViaSocket(message: String): Unit {
    try{
        if (connectedSocket != null && connectedSocket?.isConnected == true) {
            CoroutineScope(Dispatchers.IO).launch {
                val messageWithLF = message + "\n"
                outputStream?.write(messageWithLF.toByteArray())
            }
        }
    } catch(e:Exception) {
        Log.e(">>>>","sendMessageToClientViaSocket@serverSocketThread exception : ${e.message}")
    }
}

 

client 의 Socket Thread에서는 

합쳐져서 올 가능성에 대비해서

message를 delimeter를 기준으로 split하고

split된 message 하나 하나를 처리해 주었다

 

try{
    val bytesRead = inputStream?.read(buffer)
    if (bytesRead != null && bytesRead > 0) {
        val receivedMessage = String(buffer, 0, bytesRead)

        val messages = receivedMessage.split("\n")
        for (msg in messages){
            if(msg.isNotBlank()){
                if(msg.startsWith("GAME_DATA")){
                    messageCallback.onGameDataReceivedFromServerViaSocket(msg)
                } else {
                    Log.i(">>>>",  "ClientThread ReceivedMessage : $msg")
                    when(msg) {
                        "SERVER_STARTED_GAME" -> messageCallback.onGameStateFromServerViaSocket(GameState.STARTED)
                        "SERVER_PAUSED_GAME" -> messageCallback.onGameStateFromServerViaSocket(GameState.PAUSED)
                        "SERVER_RESTARTED_GAME" -> messageCallback.onGameStateFromServerViaSocket(GameState.STARTED)
                        "SERVER_STOPPED_GAME" -> messageCallback.onGameStateFromServerViaSocket(GameState.STOPPED)
                        "SERVER_WIN" -> messageCallback.onGameWinnerFromServerViaSocket(true)
                        "CLIENT_WIN" -> messageCallback.onGameWinnerFromServerViaSocket(false)
                        else -> messageCallback.onOtherMessageReceivedFromServerViaSocket(msg)
                    }
                }
            }
        }

    } else {
        isRunning = false
    }

 

이렇게 바꾸니 게임이 오류가 발생하거나 빠지는 정보 없이 진행되었다

 

전 회 까지 소켓을 이용한 연결을 만들었으니

이제 UI를 이용해서 메시지를 보내기 위한 작업을 해 보자

 

1. Socket button 활성화

    override fun onCapabilitiesChanged(
        network: Network,
        networkCapabilities: NetworkCapabilities
    ) {
        ..................................
        if(clientSocketThread == null){
            try{
                clientSocketThread = ClientSocketThread(this@MainActivity,
                    InetSocketAddress(peerIpv6, 8888), this@MainActivity)
                clientSocketThread?.also{
                    it.start()
                    runOnUiThread{
                        bindMain.btnSendViaSocket.isEnabled = true
                    }
                }
            } catch(e : Exception){
                Log.e(">>>>", "clientSocket : ${e.message}")
            }
        }
    }

 

    severSocketThread에서도 같은 처리를 해 주었다

 

2.  ThreadMessageCallback interface 

Socket Thread에서 받은 내용을 main Thread에서 chantting message를 update 하기 위해 전달용  interface를 만들었다

 

interface ThreadMessageCallback {
    fun onMessageReceivedFromThread(message : String)
}

 

 

3. SocketThread 수정

1) main Thread에서 message를 보내기 위한 method를 추가하고, 

 

2) ThreadMessageCallback을 이용해 받은 message를 MainActivity에 전달하였다

 

3) ClientThread도 같은 code를 구현

...................................

class ServerSocketThread(private val context: Context, private val messageCallback: ThreadMessageCallback) : Thread(){
    private var serverSocket: ServerSocket? = null
    private var clientSocket : Socket? = null

    private var outputStream: OutputStream? = null
    private var inputStream : InputStream? = null
    private var isRunning = true
    override fun run() {
        Log.i(">>>>", "ServerSocketTrhead Thread Started")

        try {
            serverSocket = ServerSocket(8888)

            serverSocket?.also { serverSocket1 ->
                clientSocket = serverSocket1.accept()
                Log.i(">>>>" , "server socket ; Accepted  clientSocket = $clientSocket")
                clientSocket?.also {
                    inputStream = it.getInputStream()
                    outputStream = it.getOutputStream()

                    sendMessage("hello from server through socket")

                    while(isRunning){
                        val buffer = ByteArray(1024)
                        val bytesRead = inputStream?.read(buffer)
                        if (bytesRead != null && bytesRead > 0) {
                            val receivedMessage = String(buffer, 0, bytesRead)
                            Log.i(">>>>",  "ReceivedMessage : $receivedMessage")
                            messageCallback.onMessageReceivedFromThread(receivedMessage)
                            if(receivedMessage == "quit") isRunning = false
                        }
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            outputStream?.close()
            inputStream?.close()
            clientSocket?.close()
            serverSocket?.close()
        }
        Log.i(">>>>", "Server Thread terminating...")
    }

    private fun sendMessage(message: String): Unit {
        try{
            val strMessage = "server : $message << via socket"
            outputStream?.write(strMessage.toByteArray())
        } catch(e:Exception) {
            Log.e(">>>>","sendMessage in socket thread : ${e.message}")
        }

    }

    fun sendMessageFromMainThread(message : String) {
        CoroutineScope(Dispatchers.IO).launch {
            sendMessage(message)
        }
    }
}

 

3. MainActivity 와 Trehad간의 소통을 위해 code 수정

 

1) btnSendViaSocket을 활성화 하고

2) ThreadMessageCallback 을 이용해서 MainActivity에 들어오는 chatting message를 보여 주었다

 

class MainActivity : AppCompatActivity(), ThreadMessageCallback {
   .............................  
    @RequiresApi(Build.VERSION_CODES.S)
    fun initUIListener() {
        .....................
        bindMain.btnSendViaSocket.setOnClickListener{
            if(bindMain.editMessage.text.isEmpty()) return@setOnClickListener
            sendMessageViaSocket(bindMain.editMessage.text.toString())
            bindMain.editMessage.text.clear()
        }
    }

    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {
       ..............................
        connectivityManager= getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        initViewModel()
        initUIListener()
        initWifiAwareManager()
        checkPermission()
        bindMain.btnSendViaSocket.isEnabled = false
    }

    .........................

    // 이 button은 뽑아내도 되지만 비교를 위해 남겨 두었다
    fun sendMessageViaSession(message : String){
        if(asServer!!) {
            val strToSend = "server : $message"
            viewModel.publishDiscoverySession.value?.sendMessage(
                viewModel.peerHandle.value!!,101, strToSend.toByteArray(Charsets.UTF_8))
        } else {
            val strToSend = "client : $message"
            viewModel.subscribeDiscoverySession.value?.sendMessage(
                viewModel.peerHandle.value!!,101, strToSend.toByteArray(Charsets.UTF_8))
        }
        val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + message
        bindMain.tvChattingArea.text = strDisplay
    }

    private fun sendMessageViaSocket(message : String){
        if(asServer!!) {
            val strToSend = "server : $message"
            serverSocketThread?.sendMessageFromMainThread(message)
        } else {
            val strToSend = "client : $message"
            clientSocketThread?.sendMessageFromMainThread(message)
        }
        val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + message
        bindMain.tvChattingArea.text = strDisplay
    }

    override fun onMessageReceivedFromThread(message: String) {
        Log.i(">>>>", "from thread : $message")
        val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + message
        bindMain.tvChattingArea.text = strDisplay
    }

    override fun onBackPressed() {
         ...........................
    }
}

 

4. App Test

소켓을 통해 전달 된 message가 chatting 창에 들어 왔다

 

이제 Session을 이용한 message 전달을 위한 code는 제거해도 될 듯하다

 

하고 보니 내용은 WifiAware Session을 만들어 서로 연결하고

연결에서 IPV6주소를 얻고

WifiAwareSession을 바탕으로 Socket 연결을 한다 는 내용인 것 같다.

 

아직 연결이 끊기거나, 범위를 벗어나든지, 다시 연결 되든지 이런 상황에 대해 test 해보고  수정할 것이  남은 것 같다.

 

 

전 회에 이어

 

Wifi Aware를 이용한 DiscoverySession으로 연결된 network를 바탕으로 Socket 연결을 만들어 두 Anrdroid 디바이스 간의 고속의 안정적인 연결을 만들어 내는 과정이다

 

Developer Page에 나와 있는 내용을 따라 가보자

 

1. Server side OnMessageReceived 수정

 이전회 까지의 연결에서 Subscriber로 부터의 message가 오면 onMessageReceived() 가 호출 되고 

override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
	// peerHandled을 set
    viewModel.setPeerHandle(peerHandle)
    
    // 이제 이것은 필요 없긴 하다
    val receivedMessage = String(message, Charsets.UTF_8)
    Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
    val strDispay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
    bindMain.tvChattingArea.text = strDispay

    // serverSocketThread가 없으면 server socket 활성화를 시작한다
    if(viewModel.serverSocketThread.value != null) initServerSocket()
    
    // client에 message를 보내고 client는 아래 message를 받으면 ClientSocket을 만든다
    sendMessageViaSession("hello from server")
}

 

   peerHandle 을 Global 변수에 set 한 후 ServerSocket을 준비 한다

   한편으로는 client(subscriber) 에 session을 이용해 message를 보내고

   이 메시지를 받은 client는 onMessageReceived()에서 Socket을 만들고 sever socket에 connect  할 수 있게 된다.

 

2. ServerSocketThread 준비

일반적인 socket 연결 에서와 비슷한 Thread를 준비 했다

port 번호는 8888 로 임의로 설정했다

class ServerSocketThread(private val context: Context) : Thread(){
private var serverSocket: ServerSocket? = null

    private var outputStream: OutputStream? = null
    private var inputStream : InputStream? = null
    private var isRunning = true
    override fun run() {
        Log.i(">>>>", "ServerSocketTrhead Thread Started")

        try {
            // 포트는 임의로 8888로 했다
            serverSocket = ServerSocket(8888)
            serverSocket?.also { serverSocket1 ->
                val clientSocket : Socket? = serverSocket1.accept()
                Log.i(">>>>" , "server socket ; Accepted  clientSocket = $clientSocket")
                clientSocket?.also {
                    inputStream = it.getInputStream()
                    outputStream = it.getOutputStream()

                    sendMessage("hello from server through socket")

                    while(isRunning){
                        val buffer = ByteArray(1024)
                        val bytesRead = inputStream?.read(buffer)
                        if (bytesRead != null && bytesRead > 0) {
                            val receivedMessage = String(buffer, 0, bytesRead)
                            Log.d(">>>>",  "ReceivedMessage : $receivedMessage")
                            // 종료 메시지
                            if(receivedMessage == "quit") isRunning = false
                        }
                    }

                    outputStream?.close()
                    inputStream?.close()
                    clientSocket.close()

                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            serverSocket?.close()
        }
        Log.i(">>>>", "Server Thread terminating...")
    }

    fun sendMessage(message: String): Unit {
        outputStream?.write(message.toByteArray())
    }
}

 

3.  Network 생성 및 Callback 등록

ConnectivityManager.requestNetworkCallback()을 이용해 Network를 생성하고 Callback 을 등록

 

onAvailable() callback 에서 ServerSocketThread를 실행시킴

 

class MainActivity : AppCompatActivity() {
    
    ..........

    private lateinit var connectivityManager : ConnectivityManager
    private var serverSocketThread : ServerSocketThread? =  null
    private var clientSocketThread : ClientSocketThread? =  null

    ............
    
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {
        ..............

        connectivityManager= getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        initViewModel()
        initUIListener()
        initWifiAwareManager()
        checkPermission()
    }

    @SuppressLint("MissingPermission")
    fun setWifiAwareSession(wifiAwareSession: WifiAwareSession?){

        .........

        if(asServer!!) {
            ............
            viewModel.wifiAwareSession.value?.publish(config, object : DiscoverySessionCallback() {
                ...............
                @RequiresApi(Build.VERSION_CODES.Q)
                override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
                    viewModel.setPeerHandle(peerHandle)
                    
                    //이부분은 필요 없긴 하다 이제
                    val receivedMessage = String(message, Charsets.UTF_8)
                    Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
                    val strDispay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
                    bindMain.tvChattingArea.text = strDispay

                    // serverSocketThread가 null 이면 serverSocket을 만들고 실행
                    if(serverSocketThread == null) initServerSocket()
                    // client는 아래 message를 받으면 ClientSocket을 만든다
                    sendMessageViaSession("hello from server")
                }
                ..............
            }, null)
        } else {
            .........
        }
    }

    
    @RequiresApi(Build.VERSION_CODES.Q)
    fun initServerSocket(){
        if(asServer == null || asServer==false) return
        Log.i(">>>>", "init serversocket")

        if(viewModel.publishDiscoverySession.value == null
            || viewModel.peerHandle.value == null) return

        // WifiAwareNetworkSpecifier 생성
        val networkSpecifier = WifiAwareNetworkSpecifier.Builder(
            viewModel.publishDiscoverySession.value!!,
            viewModel.peerHandle.value!!)
            .setPskPassphrase("12340987")
            .build()
        Log.i(">>>>", "init serversocket $networkSpecifier")

        // WifiAware 를 이용 하는 NetworkRequest 생성
        val myNetworkRequest = NetworkRequest.Builder()
            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
            .setNetworkSpecifier(networkSpecifier)
            .build()
        Log.i(">>>>", "init serversocket $myNetworkRequest")

        // 콜백 만들고 등록
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                super.onAvailable(network)
                Log.i(">>>>", "NetworkCallback onAvailable")
                Toast.makeText(this@MainActivity, "Socket network availabe", Toast.LENGTH_LONG).show()

                // ServerSocketThread가 만들어 져 있지 않으면
                // ServerSocketThread를 만들고 시작시킴
                try{
                    if(serverSocketThread == null) {
                        serverSocketThread = ServerSocketThread(this@MainActivity)
                        serverSocketThread?.start()
                    }
                } catch ( e : Exception){
                    Log.e(">>>>", "starting socket thread exception : ${e.message}")
                }
            }
            override fun onCapabilitiesChanged(
                network: Network,
                networkCapabilities: NetworkCapabilities
            ) {
                super.onCapabilitiesChanged(network, networkCapabilities)
                Log.i(">>>>", "NetworkCallback onCapabilitiesChanged network : $network")
                Log.i(">>>>", "NetworkCapabilities : $networkCapabilities")
            }
            override fun onLost(network: Network) {
                super.onLost(network)
                Log.i(">>>>", "NetworkCallback onLost")
            }
        }

        connectivityManager.requestNetwork(myNetworkRequest, networkCallback)
    }

    .............
}

 

 

4. Client Side의 onMessageReceived() 수정

위의 과정중 server에서 ServerSocket 시작 후 Session을 이용해 메시지를 보내게 되고

onMessageReceived()에서 clientSocket을 준비한다

@SuppressLint("MissingPermission")
fun setWifiAwareSession(wifiAwareSession: WifiAwareSession?){
    ...........

    if(asServer!!) {
        .........
    } else {
        .........
        viewModel.wifiAwareSession.value?.subscribe(config, object : DiscoverySessionCallback() {
            .............
            @RequiresApi(Build.VERSION_CODES.Q)
            override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
                viewModel.setPeerHandle(peerHandle)
                
                //마찬가지 이제 필요 없다
                val receivedMessage = String(message, Charsets.UTF_8)
                Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
                val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
                bindMain.tvChattingArea.text = strDisplay
                
                // clientSocketThread가 없으면 실행 시킨다
                if(clientSocketThread ==null) connectToServerSocket()
            }
            ............
        }, null)
    }
}

 

5. ClientSocketThread 준비

일반적인 client 용 socket thread 와 비슷하다

 

class ClientSocketThread(val context: Context, private val host : InetSocketAddress) : Thread() {
    private lateinit var socket : Socket
    private var outputStream: OutputStream? = null
    private var inputStream : InputStream? = null
    private var isRunning = true
    override fun run() {
        Log.i(">>>>", "Client Thread Started")

        try {
            socket = Socket()
            socket.connect(host, 10000)
            Log.i(">>>>" , "client socket ; connected to server = $socket")

            outputStream = socket.getOutputStream()
            inputStream = socket.getInputStream()
            // Send message to the server (group owner)
            sendMessage("hello from client through socket")

            while(isRunning){
                val buffer = ByteArray(1024)
                val bytesRead = inputStream?.read(buffer)
                if (bytesRead != null && bytesRead > 0) {
                    val receivedMessage = String(buffer, 0, bytesRead)
                    // Handle the received message
                    Log.d(">>>>",  "ReceivedMessage : $receivedMessage")
                    
                    //탈출구
                    if(receivedMessage == "quit") isRunning = false
                }
            }

            outputStream?.close()
            inputStream?.close()
            socket.close()
        } catch (e: SocketTimeoutException) {
            // Handle timeout exception
            e.printStackTrace()
        } catch (e: Exception) {
            // Handle other exceptions
            e.printStackTrace()
        }

        Log.i(">>>>", "Client Thread terminating...")
    }

    fun sendMessage(message: String): Unit {
        val strMessage : String  = "" + message
        outputStream?.write(strMessage.toByteArray())
    }
}

 

6. connectToServerSocket() 구현

 

서버의 경우와 내용이 거의 비슷하긴 하지만 이번에는

override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
        )  Callback 이 실행 되어야 비로소 Network 정보(server의 IPV6 Address 포함) 를 알게 되고

 

그것을 이용해 InetSocketAddress를 만들어 ClientSocketThread를 만들고 실행시킨다, 포트는 서버와 같은 포트를 설정

 

@RequiresApi(Build.VERSION_CODES.Q)
fun connectToServerSocket(){
    if(asServer == null || asServer==true) return
    Log.i(">>>>", "init client socket")

    if(viewModel.subscribeDiscoverySession.value == null
        || viewModel.peerHandle.value == null) return

    // NetworkSpectifer를 만들고
    val networkSpecifier = WifiAwareNetworkSpecifier.Builder(
        viewModel.subscribeDiscoverySession.value!!,
        viewModel.peerHandle.value!!
    )
    .setPskPassphrase("12340987")
    .build()

    Log.i(">>>>", "connecting to server socket $networkSpecifier")

	// NetworkRequest를 만든 후
    val myNetworkRequest = NetworkRequest.Builder()
        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
        .setNetworkSpecifier(networkSpecifier)
        .build()
    Log.i(">>>>", "connecting to server socket $myNetworkRequest")
    
    // 해당 Callback을 만들고
    val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            super.onAvailable(network)
            Log.i(">>>>", "NetworkCallback onAvailable")
            Toast.makeText(this@MainActivity, "Socket network availabe", Toast.LENGTH_LONG)
                .show()
        }

        override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
        ) {
            super.onCapabilitiesChanged(network, networkCapabilities)
            Log.i(">>>>", "NetworkCallback onCapabilitiesChanged network : $network")
            Log.i(">>>>", "NetworkCapabilities : $networkCapabilities")

            val peerAwareInfo = networkCapabilities.transportInfo as WifiAwareNetworkInfo
            val peerIpv6 = peerAwareInfo.peerIpv6Addr

            if(clientSocketThread == null){
                try{
                    clientSocketThread = ClientSocketThread(this@MainActivity, InetSocketAddress(peerIpv6, 8888))
                    clientSocketThread?.start()
                } catch(e : Exception){
                    Log.e(">>>>", "clientSocket : ${e.message}")
                }
            }
        } 
        override fun onLost(network: Network) {
            super.onLost(network)
            Log.i(">>>>", "NetworkCallback onLost")
        }
    }
    
    // connectivityManager를 이용해 등록하고 실행
    connectivityManager.requestNetwork(myNetworkRequest,
        networkCallback as ConnectivityManager.NetworkCallback
    )
}

 

7.    종료시에는

      위의 Socket connection이 종료 되어도 WifiAware Network는 살아 있어서,  현 상태에서 다시 Socket Connection을 연결 할 수도 있다.  Socket network connection이 종료되면  connectivityManager.unregisterNetworkCallback(netwokrCallback) 을 이용해 현재 등록된 networkCallback을 해제 해 주어야 한다.   해제를 안하고 다시 netwokrkCallback을 다시 만들어서 등록을 하니 등록이 안 되는 것 같았다.

8 . 실행 결과

Log를 보면 Socket이 잘 연결되고

양 측 Thread가 정상적으로 시작 되었다. 현재는 Disconnect로 끝 내는 수 밖에 없고,  다음에 메시지 보내는 기능을 추가 하면 "quit" message로 종료 시킬 수 있다

onCapabilitiesChanged, 나  onAvailable이 여러번 호출 되기도 해서  현재 Thread가 생성 되어 있는지 확인 하는 것은 꼭 추가 되어 있어야 할 듯 하다.

 

1) server side

2024-03-25 00:35:34.512 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onAvailable

2024-03-25 00:35:34.548 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onCapabilitiesChanged network : 718
2024-03-25 00:35:34.548 21097-22418 >>>>                    com.example.wifi_aware_test          I  ServerSocketTrhead Thread Started
2024-03-25 00:35:34.549 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCapabilities : [ Transports: WIFI_AWARE Capabilities: NOT_METERED&NOT_RESTRICTED&TRUSTED&NOT_VPN&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED LinkUpBandwidth>=1Kbps LinkDnBandwidth>=1Kbps TransportInfo: <AwareNetworkInfo: IPv6=/fe80::87:56ff:fec7:c02a%aware_data0, port=0, transportProtocol=-1> SignalStrength: 1]
2024-03-25 00:35:34.552 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onCapabilitiesChanged network : 718
2024-03-25 00:35:34.552 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCapabilities : [ Transports: WIFI_AWARE Capabilities: NOT_METERED&NOT_RESTRICTED&TRUSTED&NOT_VPN&VALIDATED&NOT

2024-03-25 00:35:35.561 21097-22418 >>>>                    com.example.wifi_aware_test          I  server socket ; Accepted  clientSocket = Socket[address=/fe80::87:56ff:fec7:c02a%aware_data0,port=49654,localPort=8888]
2024-03-25 00:35:35.563 21097-22418 >>>>                    com.example.wifi_aware_test          D  ReceivedMessage : hello from client through socket

 

2) client side

2024-03-25 00:31:15.208 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onAvailable

2024-03-25 00:31:15.255 21097-22358 >>>>                    com.example.wifi_aware_test          I  Client Thread Started
2024-03-25 00:31:15.257 21097-22358 TcpOptimizer            com.example.wifi_aware_test          D  TcpOptimizer-ON
2024-03-25 00:31:15.261 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCallback onCapabilitiesChanged network : 717
2024-03-25 00:31:15.261 21097-22356 >>>>                    com.example.wifi_aware_test          I  NetworkCapabilities : [ Transports: WIFI_AWARE Capabilities: NOT_METERED&NOT_RESTRICTED&TRUSTED&NOT_VPN&VALIDAT

2024-03-25 00:31:16.279 21097-22358 >>>>                    com.example.wifi_aware_test          I  client socket ; connected to server = Socket[address=/fe80::c3:4dff:febf:b76c%aware_data0,port=8888,localPort=38700]
2024-03-25 00:31:16.283 21097-22358 >>>>                    com.example.wifi_aware_test          D  ReceivedMessage : hello from server through socket

 

8 다음에는

전송 button을 활성화 시켜서 서로 메시지를 보내고,  UI에 message를 나타내도록 추가해 보자

이전 회에 알아본 내용을 실제로 코드로 구현 해 보자

 

1. ViewModel에 observer 추가

    1) ViewModel의 observer에 publishDiscoverSession, subscribeSession에 대한 observer 를 추가하고

    2) btnSendViaSession 을 disable 및 enable 시키는 부분 구현

private fun initViewModel(){
    viewModel = ViewModelProvider(this)[MainViewModel::class.java]

    .................

        viewModel.publishDiscoverySession.observe(this){
            var strDisplay = "publishDiscoverySession : "
            strDisplay += it?.toString() ?: "null"
            bindMain.tvPublishDiscoverySession.text = strDisplay
            bindMain.btnConnect.isEnabled = (
                viewModel.publishDiscoverySession.value == null
                    && viewModel.subscribeDiscoverySession.value == null)
            bindMain.btnSendViaSession.isEnabled = (
                    viewModel.publishDiscoverySession.value != null
                            || viewModel.subscribeDiscoverySession.value != null)
        }

        viewModel.subscribeDiscoverySession.observe(this){
            var strDisplay = "subscribeDiscoverySession : "
            strDisplay += it?.toString() ?: "null"
            bindMain.tvSubscribeDiscoverySession.text = strDisplay
            bindMain.btnConnect.isEnabled = (
                    viewModel.publishDiscoverySession.value == null
                            && viewModel.subscribeDiscoverySession.value == null)
            bindMain.btnSendViaSession.isEnabled = (
                    viewModel.publishDiscoverySession.value != null
                            || viewModel.subscribeDiscoverySession.value != null)
        }
}

 

2. setWifiAwareSession 구현

이전 회의 내용 그대로 순서대로 구현 하였다

fun setWifiAwareSession(wifiAwareSession: WifiAwareSession?){

    Log.i(">>>>", "setting wifiAwareSession")
    if(wifiAwareSession == null) Log.i(">>>>", "wifiAwareSession null")
    removeCurrentWifiAwareSession()
    viewModel.setWifiAwareSession(wifiAwareSession)

    if(asServer == null || bindMain.editServiceName.text.isEmpty()) return

    if(asServer!!) {
        val config: PublishConfig = PublishConfig.Builder()
            .setServiceName(bindMain.editServiceName.text!!.toString())
            .setTtlSec(0)
            .build()
        viewModel.wifiAwareSession.value?.publish(config, object : DiscoverySessionCallback() {
            override fun onPublishStarted(session: PublishDiscoverySession) {
                Log.i(">>>>", "onPublishStarted... $session")
                viewModel.setPublishDiscoverySession(session)
            }
            @RequiresApi(Build.VERSION_CODES.Q)
            override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
                viewModel.setPeerHandle(peerHandle)
                val receivedMessage = String(message, Charsets.UTF_8)
                Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
                val strDispay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
                bindMain.tvChattingArea.text = strDispay
                //initServerSocket()
            }
            override fun onSessionTerminated() {
                Log.i(">>>>", "onSessionTerminated")
                removeCurrentWifiAwareSession()
                Toast.makeText(this@MainActivity, "fail to connect to server", Toast.LENGTH_SHORT).show()
                super.onSessionTerminated()
            }
            override fun onServiceLost(peerHandle: PeerHandle, reason: Int) {
                super.onServiceLost(peerHandle, reason)
                Log.i(">>>>", "onServiceLost $peerHandle, $reason")
            }
        }, null)
    } else {
        val config: SubscribeConfig = SubscribeConfig.Builder()
            .setServiceName(bindMain.editServiceName.text!!.toString())
            .setTtlSec(0)
            .build()
        viewModel.wifiAwareSession.value?.subscribe(config, object : DiscoverySessionCallback() {
            override fun onSubscribeStarted(session: SubscribeDiscoverySession) {
                Log.i(">>>>", "onSubscribeStarted... $session")
                viewModel.setSubscribeDiscoverySession(session)
            }
            override fun onServiceDiscovered(
                peerHandle: PeerHandle,
                serviceSpecificInfo: ByteArray,
                matchFilter: List<ByteArray>
            ) {
                Log.i(">>>>", "onServiceDiscovered... $peerHandle, $serviceSpecificInfo")
                val messageToSend = "hello...connected"
                viewModel.setPeerHandle(peerHandle)
                sendMessageViaSession(messageToSend)
                Toast.makeText(this@MainActivity, "Connected to server", Toast.LENGTH_SHORT).show()
            }
            @RequiresApi(Build.VERSION_CODES.Q)
            override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
                viewModel.setPeerHandle(peerHandle)
                val receivedMessage = String(message, Charsets.UTF_8)
                Log.i(">>>>", "onMessageReceived...$peerHandle, $receivedMessage")
                val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + receivedMessage
                bindMain.tvChattingArea.text = strDisplay
                //connectToServerSocket()
            }
            override fun onSessionTerminated() {
                removeCurrentWifiAwareSession()
                Toast.makeText(this@MainActivity, "fail to connect to server", Toast.LENGTH_SHORT).show()
                super.onSessionTerminated()
            }
            override fun onServiceLost(peerHandle: PeerHandle, reason: Int) {
                super.onServiceLost(peerHandle, reason)
                Log.i(">>>>", "onServiceLost $peerHandle, $reason")
            }
        }, null)
    }   
    
    fun sendMessageViaSession(message : String){
        if(asServer!!) {
            val strToSend = "server : $message"
            viewModel.publishDiscoverySession.value?.sendMessage(
                viewModel.peerHandle.value!!,101, strToSend.toByteArray(Charsets.UTF_8))
        } else {
            val strToSend = "client : $message"
            viewModel.subscribeDiscoverySession.value?.sendMessage(
                viewModel.peerHandle.value!!,101, strToSend.toByteArray(Charsets.UTF_8))
        }
        val strDisplay = bindMain.tvChattingArea.text.toString() + "\n" + message
        bindMain.tvChattingArea.text = strDisplay
    }

 

3. 실행 결과

두개의 device로 실행 시켜 보았다.

하나는 Galaxy S9으로 이전 글에서 살펴 본 최소한의 사양은 만족하지만, 권장 사양은 만족하지 못하지만 일단 정상적인 작동은 하는 것으로 확인 되었다

 

4.  전송 버튼 작동 시키기

전송 버튼에 리스너를 달고 message를 서로 보내 보았다

@RequiresApi(Build.VERSION_CODES.S)
fun initUIListener() {
    ..........
    bindMain.btnSendViaSession.setOnClickListener{
        if(bindMain.editMessage.text.isEmpty()) return@setOnClickListener
        sendMessageViaSession(bindMain.editMessage.text.toString())
        bindMain.editMessage.text.clear()
    }
}

 

이제 서로 서로 메시지를 주고 받을 수 있게 되었다

 

정상 작동 한다

 

5. Socket을 통한 연결

앞에 살펴 보았던 개발자 페이지 내용을 보면 이 방법은 안정적이지 못하고(데이터의 전달이 안 되는 경우도 있다라고 했다), 256 byte까지의 데이터만 전달 가능하다

 

따라서 만들어진 Session을 이용한 Socket 연결 방법이 있다고 하여 다음 회에는 그 것을 구현 해 보겠다

 

서비스 게시(Publish) 및 서비스 구독(Subscribe) 에 대한 내용이다

 

Developer page에 설명되어 있는 과정은 다음과 같다

 

1) 한 쪽에서 WifiAwareSession.publish()를 이용해서 서비스를 게시

     게시에 성공하면 onPublishStarted() 콜백 메서드가 호출됩니다.

     여기서 PublishDiscoverySession을 얻을 수 있다

 

2) 구독자는 WifiAwareSession.subscribe() 를 이용해서 구독 시도  연결되면

    onSubscribeStarted() 콜백이 실행되고 SubscribeDiscoverySession을 얻을 수 있다

    게시자와 연결되면 시스템은 onServiceDiscovered() 콜백 메서드를 실행합니다.

    이 콜백에서 상대방과 연결된  PeerHandle을 얻음

 

3) 구독자는 SubscribeDiscoverySession.sendMessage(peerHandel, .. )을 이용해 Publisher에 Message를 보내고

 

4) 게시자는 onMessageReceived(peerHandle: PeerHandle, message: ByteArray) 을 이용해 상대방의 peerHandle을 얻는다

 

5) 게시자도 PublishDiscoverySession.sendMessage(peerHandle, ....) 을 통해 상대방에 메시지를 보낼 수 있다

 

 

연결이 되면 간단한 메시지를 주고 받는 것은 가능 해 진다

 

그러나....

 

참고: 메시지는 전송되지 않을 수 있고 (또는 순서에 맞지 않거나 두 번 이상 전송될 수 있음) 길이가 약 255바이트로 제한되므로 일반적으로 간단한 메시징에 사용됩니다. 정확한 길이 제한을 확인하려면 getMaxServiceSpecificInfoLength()를 호출합니다. 고속 양방향 통신을 위해서는 앱에서 대신 연결을 생성해야 합니다.

 

상당히 황당한 내용이다.

 

일단 다음회에 PublishDiscoverySession 과 SubscribeDiscoverSession,  PeerHandle을 얻는 과정을 구현해서 메시지를 주고 받아 보고

 

그 다음 마지막으로 Session연결을 바탕으로 한 Socket을 이용한 두 디바이스간의 연결을 생성해 보겠다

 

 

 

이전 회에 이어 WifiAwareSession을 얻는 과정이다

 

1.  ViewModel 초기화 및 observer 추가

WifiAwareSession의 instance 값을 화면에서 볼 수 있도록 변수에 대한 observer를 설정했다

더불어 connect 와 disconnect 버튼도 활성화 및 비활성화 하도록  했다

class MainActivity : AppCompatActivity() {

    lateinit var bindMain : ActivityMainBinding

    ...........

    lateinit var viewModel : MainViewModel

    private fun initViewModel(){
        viewModel = ViewModelProvider(this)[MainViewModel::class.java]

        viewModel.wifiAwareSession.observe(this){
            var strDisplay = "wifiAwareSession : "
            strDisplay += it?.toString() ?: "null"
                bindMain.tvWifiAwareSession.text = strDisplay
            // session이 있으면 연결 버튼 disable
            bindMain.btnConnect.isEnabled = (it==null)
            bindMain.btnDisconnect.isEnabled = (it!=null)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        .............

        initViewModel()

        initWifiAwareManager()

        checkPermission()

    }
}

 

 

2. Session 가져 오기

Developer page의 내용은 다음과 같다

 

Wi-Fi Aware 사용을 시작하려면 앱에서 attach()를 호출하여 WifiAwareSession를 가져와야 합니다. 이 메서드는 다음을 실행합니다.

  • Wi-Fi Aware 하드웨어를 켭니다.
  • Wi-Fi Aware 클러스터에 참여하거나 클러스터를 형성합니다.
  • 생성된 모든 검색 세션의 컨테이너 역할을 하는 고유한 네임스페이스를 사용하여 Wi-Fi Aware 세션을 만듭니다.

앱이 성공적으로 연결되면 시스템은 onAttached() 콜백을 실행합니다. 이 콜백은 앱이 모든 추가 세션 작업에 사용해야 하는 WifiAwareSession 객체를 제공합니다. 앱은 이 세션을 사용하여 서비스를 게시하거나 서비스를 구독할 수 있습니다.

앱은 attach()를 한 번만 호출해야 합니다. 앱에서 attach()를 여러 번 호출하는 경우 앱은 호출마다 고유한 네임스페이스를 가진 다른 세션을 수신합니다. 이는 복잡한 시나리오에서 유용할 수 있지만 일반적으로는 피해야 합니다.

 

3. Callback class 작성

WifiAwareManager.attach() 를 이용하여  WifiAwareSession을 가져오기 위해 Callback 함수를 작성

 

onAttached() method가 실행될 때 parameter 로 WifiAwareSession을 얻을 수 있다

새로운 session이 생기면 먼저 removeCurrentWifiAwareSession을 이용해 모든 연결을 닫고 변수를 초기화 해 주고 난뒤

seWifiAwareSession method를 시행 시킴

class CustomAttachCallback(private val activity: MainActivity) : AttachCallback() {
    override fun onAttachFailed() {
        super.onAttachFailed()
        Log.i(">>>>", "onAttachFailed")
    }

    override fun onAttached(session: WifiAwareSession?) {
        super.onAttached(session)
        Log.i(">>>>", "onAttached")
        session?.let{
            Log.i(">>>>", "onAttached session : $it")
            activity.removeCurrentWifiAwareSession()
            activity.setWifiAwareSession(it)
        }
    }

    override fun onAwareSessionTerminated() {
        super.onAwareSessionTerminated()
        Log.i(">>>>", "onAwareSessionTerminated")
    }
}

 

4. Button들에 Listener를 달아 주었다

attach() 기능을 실행 전 server or client 인지,  만들거나 접속할 서비스의 이름이 설정되었는지 확인 해 주었다.

class MainActivity : AppCompatActivity() {

    lateinit var viewModel : MainViewModel

    private var asServer : Boolean? = null

    @RequiresApi(Build.VERSION_CODES.S)
    fun initUIListener() {
        bindMain.btnConnect.setOnClickListener{
            if(asServer == null) {
                Toast.makeText(this, "server or client를 선택하세요", Toast.LENGTH_LONG).show()
                return@setOnClickListener
            }
            if(bindMain.editServiceName.text.isEmpty()) {
                Toast.makeText(this, "service 이름을 입력하세요", Toast.LENGTH_LONG).show()
                return@setOnClickListener
            }
            attach()
        }
        bindMain.btnDisconnect.setOnClickListener{
            removeCurrentWifiAwareSession()
        }
        bindMain.rbServer.setOnClickListener{
            asServer = true
        }
        bindMain.rbClient.setOnClickListener{
            asServer = false
        }
    }

    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {

        ...............
        initViewModel()
        initUIListener()
        initWifiAwareManager()
        checkPermission()

    }

}

 

5.  WifiAwareSession을 구하기 - attach()

attach button을 누르면 AttachCallback을 등록하고

CustomAttachCallback의 onAttach()에서 WifiAwareSession의 instance를 구해

일단 ViewModel내의 wifiAwareSesssion 변수에 등록 해 주었다

 


class MainActivity : AppCompatActivity() {

    ..............

    private val customAttachCallback=  CustomAttachCallback(this)
    private var asServer : Boolean? = null

    ...............
    
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {
        ..........

        initViewModel()
        initUIListener()
        initWifiAwareManager()
        checkPermission()
    }

    fun setWifiAwareSession(wifiAwareSession: WifiAwareSession?){
        Log.i(">>>>", "setting wifiAwareSession")
        if(wifiAwareSession == null) Log.i(">>>>", "wifiAwareSession null")
        removeCurrentWifiAwareSession()
        // wifiAwareSession에 값 등록
        viewModel.setWifiAwareSession(wifiAwareSession)
        //......  다음에 계속
    }

    // AttachCallback을 등록하고 attach 기능을 시도
    @RequiresApi(Build.VERSION_CODES.S)
    fun attach(){
        wifiAwareManager.attach(customAttachCallback, null)
    }

    // 새로운 session을 얻으면 이전 session을 모두 없앰
    fun removeCurrentWifiAwareSession(){
        try{
            viewModel.publishDiscoverySession.value?.close()
            viewModel.setPublishDiscoverySession(null)
            viewModel.subscribeDiscoverySession.value?.close()
            viewModel.setSubscribeDiscoverySession(null)
            viewModel.wifiAwareSession.value?.close()
            viewModel.setWifiAwareSession(null)
            viewModel.setPeerHandle(null)
        } catch (e: Exception) {
            Log.e(">>>>", "removeWifiAwareSession : ${e.message}")
        }
    }

    .............
}

 

5.  wifi 상태 변화에 따른 대비 추가

wifAwareBroadastReceiver에 내용을 추가 했다

내용을 추가하면 wifi가 끊어 졌다 연결되면 자동으로 재 연결 된다.

class WifiAwareBroadcastReceiver(
    private val activity: MainActivity,
    private val wifiAwareManager: WifiAwareManager,
    private val wifiAwareSession: WifiAwareSession?,
) : BroadcastReceiver() {
    @SuppressLint("MissingPermission")
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onReceive(context: Context?, intent: Intent?) {
        if(wifiAwareManager.isAvailable){
            Log.i(">>>>", "wifiAwareManager is available")
            activity.attach()
        } else {
            wifiAwareSession.let {
                activity.removeCurrentWifiAwareSession()
            }
            Log.e(">>>>", "wifiAwareManager is not available")
        }
    }
}

 

6. 결과

Connect button을 누르면 

2024-03-24 15:51:16.105 14239-14239 >>>>                    com.example.wifi_aware_test          I  onAttached
2024-03-24 15:51:16.105 14239-14239 >>>>                    com.example.wifi_aware_test          I  onAttached session : android.net.wifi.aware.WifiAwareSession@6c37598
2024-03-24 15:51:16.106 14239-14239 >>>>                    com.example.wifi_aware_test          I  setting wifiAwareSession

 

Disconnect Button을 누르면 

2024-03-24 15:52:06.835 14239-14239 WifiAwareManager        com.example.wifi_aware_test          V  disconnect()

 

예상과 다르게 onAwareSessionTerminated()는 불리지 않았다.  이 건 다음에 쓸 일이 있다

 

Device에도 정상적으로 wifiAwareSession이 표시되어 있다

1. Device가 Wi-fi Aware 를 지원하는 지 확인  및 해당 BroadcastReceiver 작성

(1) WifiAwareManager를 구하는 방법

context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)

 

(2) WifiAware 가 사용 가능한지에 대한 Receiver 작성

- ACESS_WIFI_STATE 에 대한 permission체크가 필요하다고 되어있지만 Suppress 시켰다. check 하면 missing permission으로 작동되지 않는 것 같았다.

 

물론 이전 회에 필요한 Permission을 추가하고 앱 시작시에 permisson 체크는 해 두어야 한다.

class WifiAwareBroadcastReceiver(
    private val activity: MainActivity,
    private val wifiAwareManager: WifiAwareManager,
    private val wifiAwareSession: WifiAwareSession?,
) : BroadcastReceiver() {
    @SuppressLint("MissingPermission")
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onReceive(context: Context?, intent: Intent?) {
        if(wifiAwareManager.isAvailable){
            Log.i(">>>>", "wifiAwareManager is available")
            //activity.attach()  나중에 추가할 내용
        } else {
        	//나중에 추가할 내용
//            wifiAwareSession.let {
//                activity.removeCurrentWifiAwareSession()
//            }
            Log.e(">>>>", "wifiAwareManager is not available")
        }
    }
}

 

(3) BroadcastReceiver를 등록

class MainActivity : AppCompatActivity() {

    ..........

    private var wifiAwareReceiver: WifiAwareBroadcastReceiver? = null
    private lateinit var intentFilter : IntentFilter
    private lateinit var wifiAwareManager : WifiAwareManager

    ...........

    override fun onCreate(savedInstanceState: Bundle?) {
        ..........
        initWifiAwareManager()
        
        checkPermssion()
    }

    private fun initWifiAwareManager(){
        wifiAwareManager = getSystemService(Context.WIFI_AWARE_SERVICE) as WifiAwareManager
        Log.i(">>>>", "wifiAwareManager : $wifiAwareManager")
        intentFilter = IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED)
        wifiAwareReceiver = WifiAwareBroadcastReceiver(this, wifiAwareManager, viewModel.wifiAwareSession.value )
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun onResume() {
        registerReceiver(wifiAwareReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
        super.onResume()
    }
}

 

 

(4) 결과

 

이제 앱을 작동 시키고 Wifi 를 껐다 켜면 다음과 같이 Receiver의 정상 작동을 확인 할 수 있다

 

그렇지만 앱 시작시에 자동으로 isAvaialble() 이 수신되지는 않아서, 여기에 초기화 과정을 넣어서는 안될 듯 하다

 

2024-03-24 11:50:58.574 29281-29281 >>>>                    com.example.wifi_aware_test          E  wifiAwareManager is not available
2024-03-24 11:50:58.677  1016-2349  SemIWCMonitor.File      system_server                        E  Foreground package,:  com.example.wifi_aware_test(10539)
2024-03-24 11:51:04.620 29281-29281 >>>>                    com.example.wifi_aware_test          I  wifiAwareManager is available

 

다음의 구글의 개발자 페이지를 참고해서 제작해 보았다.

 

 

 

Wi-Fi Aware 개요  |  Connectivity  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Wi-Fi Aware 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Wi-Fi Aware 기능을 사용하면 Android 8.0 (API

developer.android.com

 

 

1. Project 만들기

개발자 페이지에 "Wi-Fi Aware 기능을 사용하면 Android 8.0 (API 수준 26) 이상을 실행하는 기기 간에 다른 유형의 연결 없이 서로를 검색하고 직접 연결할 수 있습니다" 라고 되어있다

 

SDK 31이상의 Device에만 적용되는 API도 있다고 되어있다

 

Android 12 (API 수준 31)에서는 Wi-Fi Aware가 다음과 같이 개선되었습니다.

  • Android 12 (API 수준 31) 이상을 실행하는 기기에서는 onServiceLost() 콜백을 사용하여 서비스가 중지되거나 범위를 벗어나서 앱이 검색된 서비스를 손실했을 때 알림을 받을 수 있습니다.
  • Wi-Fi Aware 데이터 경로 설정이 간소화되었습니다. 이전 버전에서는 L2 메시징을 사용하여 시작자의 MAC 주소를 제공하므로 지연 시간이 발생했습니다. Android 12 이상을 실행하는 기기에서는 응답자(서버)가 모든 피어를 수락하도록 구성할 수 있습니다. 즉, 초기화자의 MAC 주소를 미리 알 필요가 없습니다. 이렇게 하면 데이터 경로 가져오기가 빨라지고 단 한 번의 네트워크 요청으로 여러 지점 간 링크를 사용할 수 있습니다.
  • Android 12 이상에서 실행되는 앱은 WifiAwareManager.getAvailableAwareResources() 메서드를 사용하여 현재 사용 가능한 데이터 경로 수를 가져오고 세션을 게시하고 구독 세션을 구독할 수 있습니다. 이렇게 하면 앱에서 원하는 기능을 실행하는 데 사용 가능한 리소스가 충분한지 확인할 수 있습니다.

내가 가진 테스트용 안드로이드 기계중 하나가 갤럭시 S9라 SDK 31은 지원이 되지는 않는다.  그렇지만 테스트 용으로 일단  Minimum SDK  26(Oreo, Android 8.0)으로 프로젝트를 생성 했다

 

2. Permission 추가 및 체크

(1) Permisson 추가

개발자 페이지에 적혀 있는 것 말고 추가로  더 필요한 내용이 있어 추가 했다

 

    <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.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <uses-feature
        android:name="android.hardware.wifi.direct"
        android:required="true" />

    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />

 

(2) Permission check 및 획득

실제 앱 속에서 Permission check는 두가지만 하면 정상 작동했다

   나머지는 추가하면 앱 시작시 확인도 되지 않고, 오히려 중간 중간 Permission check 상황에서 permission Denied로 체크 되어 앱이 정상 작동하지 않는 결과를 보여 주었다

 

private val permissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) {
            permissions -> val granted = permissions.entries.all {
        it.value
    }
        if(granted) Log.i("permission Launcher>>>>", "all permission granted in permission Launcher")
        else {
            Log.e("permission Launcher >>>>", "not all of permission granted in permission Launcher ")
        }
    }

    private fun checkPermission(){
        val statusCoarseLocation = ContextCompat.checkSelfPermission(this,
            "android.permission.ACCESS_COARSE_LOCATION")
        val statusFineLocation = ContextCompat.checkSelfPermission(this,
            "android.permission.ACCESS_FINE_LOCATION")

        val shouldRequestPermission = statusCoarseLocation != PackageManager.PERMISSION_GRANTED
                || statusFineLocation != PackageManager.PERMISSION_GRANTED

        if (shouldRequestPermission) {
            Log.d(">>>>", "One or more Permission Denied, Starting permission Launcher")
            permissionLauncher.launch(
                arrayOf(
                    "android.permission.ACCESS_COARSE_LOCATION",
                    "android.permission.ACCESS_FINE_LOCATION",
                )
            )
        } else {
            Log.d(">>>>", "All Permission Permitted, No need to start permission Launcher")
        }
    }

 

3. 상태를 표시할 변수 만들기

Test 용이라 네트워크 연결 중에 필요한 몇개의 변수가 있는데, 현재 변수의 연결 상태를 쉽게 알고, 디버깅에 참고 하기 위해 변수의 상태를 앱에 표시해 주려고 한다

그래서 ViewModel 과 Observer를 등록해 사용하였다.  실제 프로그램에서는 필요가 없을 것으로 생각된다.  Sample 용이라. 

 

(1) View Model

 


class MainViewModel : ViewModel() {

    private val _wifiAwareSession = MutableLiveData<WifiAwareSession>()
    val wifiAwareSession : LiveData<WifiAwareSession> get() = _wifiAwareSession

    private val _peerHandle = MutableLiveData<PeerHandle>()
    val peerHandle : LiveData<PeerHandle> get() = _peerHandle

    private val _publishDiscoverySession = MutableLiveData<PublishDiscoverySession>()
    val publishDiscoverySession : LiveData<PublishDiscoverySession> get()= _publishDiscoverySession

    private val _subscribeDiscoverySession = MutableLiveData<SubscribeDiscoverySession>()
    val subscribeDiscoverySession : LiveData<SubscribeDiscoverySession> get()= _subscribeDiscoverySession

    init {
        _wifiAwareSession.value = null
        _peerHandle.value = null
        _publishDiscoverySession.value = null
        _subscribeDiscoverySession.value = null
    }

    fun setWifiAwareSession(session: WifiAwareSession?){
        _wifiAwareSession.value = session
    }

    fun setPeerHandle(handle: PeerHandle?){
        _peerHandle.value = handle
    }

    fun setPublishDiscoverySession(session: PublishDiscoverySession?){
        _publishDiscoverySession.value = session
    }

    fun setSubscribeDiscoverySession(session: SubscribeDiscoverySession?){
        _subscribeDiscoverySession.value = session
    }

}

4. User Interface를 생성

위의 변수 상태를 포함한 User Interface 를 생성했다

원래 ServerSocketThread, ClientSocketThred도 ViewModel에 추가 시키려고 했으나, Thread를 상속한 class는 ViewModel 의 observer를 쓰기가 까다로워서 뺐다., UI에는 남아 있다

 

 

다음회 부터 본격적인 시작을 해 보자

Wi-Fi 를 이용한 P to P 연결을 이용해 두 개의 안드로이드 디바이스를 Socket으로 연결해 보고자 한다

 

사실 Internet을 통한 디바이스 간의 연결이 거의 대중화 되어 있기도 하고, 블루투스를 통한 연결이 널리 알려져 있어 Wi-Fi를 이용한 두 디바이스의 연결이 큰 의미가 있을지는 의문이긴 하다.  게다가 안드로이드 디바이스 사이의 연결만 지원 가능하니 더욱 사용 용도가 적을 것 같기도 하다

 

목표는 Wi-Fi 를 이용한  P2P 연결을 이용한 2인용 App을 만들어 보고자 한다

 

일단 두 디바이스 간의 소켓 연결을 만들어 내고, 간단하게 메시지를 주고받는 과정까지 기록을 남겨 보기로 한다

메시지를 주고 받을 수 있으면 파일 전송및 수신, 게임 개발등 다른 방향으로 쉽게 확장 가능하리라 생각된다.

 

Android 개발자 페이지를 보면 다음과 같은 2가지 형식이 있다

 

1) Wi-Fi Direct를 이용한 P2P 연결을 이용

 

2) Wi-Fi Aware 를 이용한 Socket 연결

 

Wi-Fi Direct를 이용한 연결은 개발자 페이지를 보면 간단한 예제가 있어서, 참고로 해서 어느 정도 구현이 가능했고,

Wifi Aware를 이용한 연결은 개발자 페이지의 설명도 자세하지 않고, 참고할 만한 예제가 개발자 페이지 및 웹 상에도 검색이 불가능 하여 상당한 시행 착오 후 연결에 성공하였다.

 

개인적인 느낌으로는 Wi-Fi Aware를 이용한 연결이 관리가 조금 더 쉬운 것 같았다.

 

다음 회에 먼저 Wi-Fi Aware를 이용한 연결을 구현해 보려고 한다

 

 

 

 

 

+ Recent posts