Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

안드로이드 앱 만들기 : Nearby Connections 주변 기기와 간단한 통신 처리 방법에 관한 이야기

Billcorea 2023. 5. 30. 22:03
반응형

오늘은 앱을 구현하는 동안 알게 된 Nearby Connections API에 대한 이야기를 정리해 두고자 합니다.  이 기능은 wifi 가 되지 않더라도 주변(10m 반경)에 같은 앱을 사용하는 사용자가 있을 때, 데이터, 텍스트, 이미지 등을 공유하는 기능을 구현하기 위해서 사용됩니다. 

 

https://medium.com/androiddevelopers/two-way-communication-without-internet-nearby-connections-b1185 30cb84d

 

Two-way communication without internet: Nearby Connections (Part 2 of 3)

The Nearby Connections API allows your users to connect with each other even when no internet is available. The API uses Bluetooth and…

medium.com

참고글의 링크 하나를 달아 둡니다.  읽어 보면 도움이 될 듯합니다.   그리고 안드로이드 개발자 매뉴얼은 다음 링크를 참조하였습니다. 

 

https://developers.google.com/nearby/connections/android/get-started?hl=ko 

 

시작하기  |  Nearby Connections  |  Google for Developers

메일링 리스트에 가입하여 Nearby에 관한 소식 및 공지사항을 확인하세요. 이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 시작하기 컬렉션을 사용해 정리하기 내 환경설정을

developers.google.com

 

이제 알아본 내용들을 기초로 하여 구현을 시도해 보겠습니다.  개발자 페이지 내용을 보다 보면 참고할 소스 코드가 github에 올라가 있는 것을 볼 수 있었습니다. 그것을  fork 하여 개인 계정으로 가져온 다음 일단은 그대로 저장해 실행을 해 보기록 했습니다. 

 

https://github.com/nari4169/connectivity-samples/tree/main/NearbyConnectionsWalkieTalkie

 

GitHub - nari4169/connectivity-samples: Multiple samples showing the best practices in connectivity on Android.

Multiple samples showing the best practices in connectivity on Android. - GitHub - nari4169/connectivity-samples: Multiple samples showing the best practices in connectivity on Android.

github.com

여러 개의 예제 소스가 있기는 했지만, 가장 필요한 기능에 유사한 것은 간단한 무선통신(워키토키)을 지원하는 코드가 눈에 들어와 실행을 해 보았습니다. 

 

다만, 원래 코드가 2018년쯤에 만들어진 이후 업데이트가 되지 않아서 최신 환경에 맞지 않는 부분들이 있었고, java 코드로 되어 있어 그대로 일단은 실행이 될 수 있도록 하는 부분만 수정해서 개인 계정에는 commit을 해 두었습니다.  저 링크에 있는 코드는 2023년 현재에도 그대로 실행해 볼 수 있는 코드입니다. 

 

저는 그것을 가지고 와서  kotlin을 수정된 코드를 활용해서 만들고 있는 앱에 적용해 보기로 했습니다.   아무튼 기능 구현을 해 보도록 하겠습니다. 

 

먼저 권한 설정을 시작합니다. 

 

manifest 파일에 다음과 같이 권한을 추가했습니다. 

    <!-- Required for Nearby Connections -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"  />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
        android:usesPermissionFlags="neverForLocation"
        tools:targetApi="s" />

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

    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
        android:usesPermissionFlags="neverForLocation"
        tools:targetApi="s" />

 

이제 기본으로 사용할 연결 작성을 위해서 ConnectionsActivity을 작성해 보겠습니다.  사실 이 코드는 개발자 페이지에 있는 코드를 kotlin을 변환한 코드입니다. java 코드 그대로라고 봐도 무방(?) 합니다. 

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.CallSuper
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.billcorea.barcodevoucher0225.R
import com.billcorea.barcodevoucher0225.database.DBHandler
import com.billcorea.barcodevoucher0225.database.ViewModels
import com.billcorea.barcodevoucher0225.databean.ReceiveMessage
import com.google.android.gms.common.api.Status
import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.connection.AdvertisingOptions
import com.google.android.gms.nearby.connection.ConnectionInfo
import com.google.android.gms.nearby.connection.ConnectionLifecycleCallback
import com.google.android.gms.nearby.connection.ConnectionResolution
import com.google.android.gms.nearby.connection.ConnectionsClient
import com.google.android.gms.nearby.connection.ConnectionsStatusCodes
import com.google.android.gms.nearby.connection.DiscoveredEndpointInfo
import com.google.android.gms.nearby.connection.DiscoveryOptions
import com.google.android.gms.nearby.connection.EndpointDiscoveryCallback
import com.google.android.gms.nearby.connection.Payload
import com.google.android.gms.nearby.connection.PayloadCallback
import com.google.android.gms.nearby.connection.PayloadTransferUpdate
import com.google.android.gms.nearby.connection.Strategy
import com.google.android.gms.tasks.OnFailureListener
import com.google.firebase.auth.FirebaseAuth
import java.util.Locale

/** Nearby Connections에 연결하고 편리한 메서드 및 콜백을 제공하는 클래스입니다. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
abstract class ConnectionsActivity : ComponentActivity() {

    private val dataViewModel : ViewModels by viewModels()

    /**
     * ConnectionsActivity 권한으로 앱에 필요한 모든 권한을 풀링하는 선택적 후크
     *요청합니다.
     *
     * @return 앱이 제대로 작동하는 데 필요한 모든 권한입니다.
     */
    /**
     * 이러한 권한은 Nearby Connections에 연결하기 전에 필요합니다.
     */

    protected open val requiredPermissions: Array<String> = arrayOf(
        Manifest.permission.BLUETOOTH_SCAN,
        Manifest.permission.BLUETOOTH_ADVERTISE,
        Manifest.permission.BLUETOOTH_CONNECT,
        Manifest.permission.ACCESS_WIFI_STATE,
        Manifest.permission.CHANGE_WIFI_STATE,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION,
    )

    private val REQUEST_CODE_REQUIRED_PERMISSIONS = 1

    private val wifiManager: WifiManager by lazy {
        applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
    }

    /**
     * 로깅을 위해 [Status]를 영어로 읽을 수 있는 메시지로 변환합니다.
     *
     * @param status 현재 상태
     * @return 읽을 수 있는 문자열. 예. 파일을 찾을 수 없습니다.
     */
    private fun toString(status: Status): String {
        return String.format(
            Locale.US,
            "[%d]%s",
            status.statusCode,
            if (status.statusMessage != null) status.statusMessage else ConnectionsStatusCodes.getStatusCodeString(
                status.statusCode
            )
        )
    }

    private fun hasPermissions(context: Context?, vararg permissions: String?): Boolean {
        if (context != null) {
            for (permission in permissions) {
                logD("permission=$permission", "")
                if (ActivityCompat.checkSelfPermission(
                        context,
                        permission!!
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    return false
                }
            }
        }
        return true
    }

    /** Nearby Connections에 대한 핸들러입니다.  */
    lateinit var mConnectionsClient: ConnectionsClient

    /** 우리 주변에서 발견한 장치.  */
    private val mDiscoveredEndpoints: MutableMap<String, Endpoint> = HashMap()

    /**
     * 보류 중인 연결이 있는 장치입니다. [ ][.acceptConnection] 또는 [.rejectConnection]을 호출할 때까지 보류 상태로 유지됩니다.
     */
    private val mPendingConnections: MutableMap<String, Endpoint> = HashMap()

    /**
     * 현재 연결된 장치. 광고주의 경우 이는 클 수 있습니다. 발견자에게는
     * 이 맵에는 하나의 항목만 있습니다.
     */
    private val mEstablishedConnections: MutableMap<String, Endpoint?> = HashMap()
    /** 현재 다른 장치에 연결을 시도하고 있으면 'true'를 반환합니다. */
    /**
     * 검색된 장치에 연결을 요청하는 경우 True입니다. 우리가 묻는 동안, 우리는 다른 것을 요청할 수 없습니다
     * 장치.
     */
    protected var isConnecting = false
        private set
    /** 현재 검색 중이면 'true'를 반환합니다. */
    /** 발견하는 경우 참입니다. */
    protected var isDiscovering = false
        private set
    /** 현재 광고 중인 경우 'true'를 반환합니다. */
    /** 광고하는 경우 참입니다. */
    protected var isAdvertising = false
        private set

    /** 다른 장치에 대한 연결을 위한 콜백.  */
    private val mConnectionLifecycleCallback: ConnectionLifecycleCallback =
        object : ConnectionLifecycleCallback() {
            override fun onConnectionInitiated(endpointId: String, connectionInfo: ConnectionInfo) {
                logD(
                    String.format(
                        "onConnectionInitiated(endpointId=%s, endpointName=%s)",
                        endpointId, connectionInfo.endpointName
                    ),
                    ""
                )
                val endpoint = Endpoint(endpointId, connectionInfo.endpointName)
                mPendingConnections[endpointId] = endpoint
                this@ConnectionsActivity.onConnectionInitiated(endpoint, connectionInfo)
            }

            override fun onConnectionResult(endpointId: String, result: ConnectionResolution) {
                logD(
                    String.format(
                        "onConnectionResponse(endpointId=%s, result=%s)",
                        endpointId,
                        result
                    ),
                    ""
                )

                // We're no longer connecting
                isConnecting = false
                if (!result.status.isSuccess) {
                    logW(
                        String.format(
                            "Connection failed. Received status %s.",
                            toString(result.status)
                        )
                    )
                    onConnectionFailed(mPendingConnections.remove(endpointId))
                    return
                }
                connectedToEndpoint(mPendingConnections.remove(endpointId))
            }

            override fun onDisconnected(endpointId: String) {
                if (!mEstablishedConnections.containsKey(endpointId)) {
                    logW("Unexpected disconnection from endpoint $endpointId")
                    return
                }
                disconnectedFromEndpoint(mEstablishedConnections[endpointId])
            }
        }

    /** 다른 장치에서 우리에게 보낸 페이로드(데이터 바이트)에 대한 콜백.  */
    private val mPayloadCallback: PayloadCallback = object : PayloadCallback() {
        override fun onPayloadReceived(endpointId: String, payload: Payload) {
            logD(
                String.format("onPayloadReceived(endpointId=%s, payload=%s)", endpointId, payload),
                ""
            )
            onReceive(mEstablishedConnections[endpointId], payload)
        }

        override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {
            logD(
                String.format(
                    "onPayloadTransferUpdate(endpointId=%s, update=%s)", endpointId, update
                ),
                ""
            )
        }
    }

    /** 활동이 처음 생성될 때 호출됩니다.  */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mConnectionsClient = Nearby.getConnectionsClient(this)
    }

    /** 활동이 사용자에게 표시될 때 호출됩니다. */
    override fun onStart() {
        super.onStart()
        // 이건 33 일때만 적용 가능
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) {
            logD("SDK=${Build.VERSION.SDK_INT}","")
            requiredPermissions.plus(Manifest.permission.NEARBY_WIFI_DEVICES)
        }
        if (!hasPermissions(applicationContext, *requiredPermissions)) {
            requestPermissions.launch(requiredPermissions)
        }
        // startLocalOnlyHotspot()
    }

    private fun startLocalOnlyHotspot() {
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.NEARBY_WIFI_DEVICES
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            return
        }
        wifiManager.startLocalOnlyHotspot(
            object : WifiManager.LocalOnlyHotspotCallback() {
                override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
                    super.onStarted(reservation)

                }

                override fun onFailed(reason: Int) {
                    super.onFailed(reason)
                    Toast.makeText(
                        applicationContext,
                        "Error Local Only Hotspot: $reason",
                        Toast.LENGTH_SHORT,
                    ).show()
                }

                override fun onStopped() {
                    super.onStopped()

                }
            },
            null,
        )
    }

    private  val requestPermissions = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()) { permissions ->

        var chkE = false ;
        permissions.entries.forEach {
            logD("DEBUG", "${it.key} = ${it.value}")
            if (!it.value) {
                chkE = true
            }
        }
        if (chkE) {
            MaterialDialog(this).show {
                icon(R.mipmap.ic_barcode_voucher_round)
                title(R.string.titleGetPermission)
                message(R.string.msgGetPermissonsNon)
                positiveButton(R.string.OK) {
                    finish()
                }
            }
        }
    }

    /**
     * 장치를 광고 모드로 설정합니다. 검색 모드에서 다른 장치로 브로드캐스트합니다.
     * [.onAdvertisingStarted] 또는 [.onAdvertisingFailed] 중 하나가 한 번 호출됩니다.
     * 이 모드에 성공적으로 진입했는지 확인했습니다.
     */
    protected fun startAdvertising() {
        isAdvertising = true
        val localEndpointName = name
        val advertisingOptions = AdvertisingOptions.Builder()
        advertisingOptions.setStrategy(strategy!!)
        mConnectionsClient
            .startAdvertising(
                localEndpointName,
                serviceId,
                mConnectionLifecycleCallback,
                advertisingOptions.build()
            )
            .addOnSuccessListener {
                logV("Now advertising endpoint $localEndpointName")
                onAdvertisingStarted()
            }
            .addOnFailureListener(
                object : OnFailureListener {
                    override fun onFailure(e: Exception) {
                        isAdvertising = false
                        logW("startAdvertising() failed.", e)
                        onAdvertisingFailed()
                    }
                })
    }

    /** Stops advertising.  */
    protected fun stopAdvertising() {
        isAdvertising = false
        mConnectionsClient.stopAdvertising()
    }

    /** Called when advertising successfully starts. Override this method to act on the event.  */
    protected fun onAdvertisingStarted() {}

    /** Called when advertising fails to start. Override this method to act on the event.  */
    protected fun onAdvertisingFailed() {}

    /**
     * 원격 엔드포인트와의 보류 중인 연결이 생성될 때 호출됩니다. [연결 정보] 사용
     * 연결에 대한 메타데이터(예: 수신 대 발신 또는 인증 토큰). 만약에
     * 연결을 계속하려면 [.acceptConnection]을 호출합니다. 그렇지 않으면,
     * [.rejectConnection]을 호출합니다.
     */
    protected open fun onConnectionInitiated(
        endpoint: Endpoint?,
        connectionInfo: ConnectionInfo?
    ) {
    }

    /** Accepts a connection request.  */
    protected fun acceptConnection(endpoint: Endpoint) {
        mConnectionsClient
            .acceptConnection(endpoint.id, mPayloadCallback)
            .addOnFailureListener { e -> logW("acceptConnection() failed.", e) }
    }

    /** Rejects a connection request.  */
    protected fun rejectConnection(endpoint: Endpoint) {
        mConnectionsClient
            .rejectConnection(endpoint.id)
            .addOnFailureListener { e -> logW("rejectConnection() failed.", e) }
    }

    /**
     * 장치를 검색 모드로 설정합니다. 이제 광고 모드에서 장치를 수신합니다. 어느 하나
     * [.onDiscoveryStarted] 또는 [.onDiscoveryFailed]는 일단 찾으면 호출됩니다.
     * 이 모드에 성공적으로 진입한 경우 출력됩니다.
     */
    protected fun startDiscovering() {
        isDiscovering = true
        mDiscoveredEndpoints.clear()
        val discoveryOptions = DiscoveryOptions.Builder()
        discoveryOptions.setStrategy(strategy!!)
        mConnectionsClient
            .startDiscovery(
                serviceId,
                object : EndpointDiscoveryCallback() {
                    override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
                        logD(
                            String.format(
                                "onEndpointFound(endpointId=%s, serviceId=%s, endpointName=%s)",
                                endpointId, info.serviceId, info.endpointName
                            ),
                            ""
                        )
                        if (serviceId == info.serviceId) {
                            val endpoint = Endpoint(endpointId, info.endpointName)
                            mDiscoveredEndpoints[endpointId] = endpoint
                            onEndpointDiscovered(endpoint)
                        }
                    }

                    override fun onEndpointLost(endpointId: String) {
                        logD(String.format("onEndpointLost(endpointId=%s)", endpointId), "")
                    }
                },
                discoveryOptions.build()
            )
            .addOnSuccessListener { onDiscoveryStarted() }
            .addOnFailureListener(
                object : OnFailureListener {
                    override fun onFailure(e: Exception) {
                        isDiscovering = false
                        logW("startDiscovering() failed.", e)
                        onDiscoveryFailed()
                    }
                })
    }

    /** Stops discovery.  */
    protected fun stopDiscovering() {
        isDiscovering = false
        mConnectionsClient.stopDiscovery()
    }

    /** Called when discovery successfully starts. Override this method to act on the event.  */
    protected fun onDiscoveryStarted() {}

    /** Called when discovery fails to start. Override this method to act on the event.  */
    protected fun onDiscoveryFailed() {}

    /**
     * Called when a remote endpoint is discovered. To connect to the device, call [ ][.connectToEndpoint].
     */
    protected open fun onEndpointDiscovered(endpoint: Endpoint?) {}

    /** Disconnects from the given endpoint.  */
    protected fun disconnect(endpoint: Endpoint) {
        mConnectionsClient.disconnectFromEndpoint(endpoint.id)
        mEstablishedConnections.remove(endpoint.id)
    }

    /** Disconnects from all currently connected endpoints.  */
    protected fun disconnectFromAllEndpoints() {
        for (endpoint in mEstablishedConnections.values) {
            mConnectionsClient.disconnectFromEndpoint(endpoint!!.id)
        }
        mEstablishedConnections.clear()
    }

    /** Resets and clears all state in Nearby Connections.  */
    protected fun stopAllEndpoints() {
        mConnectionsClient.stopAllEndpoints()
        isAdvertising = false
        isDiscovering = false
        isConnecting = false
        mDiscoveredEndpoints.clear()
        mPendingConnections.clear()
        mEstablishedConnections.clear()
    }

    /**
     * Sends a connection request to the endpoint. Either [.onConnectionInitiated] or [.onConnectionFailed] will be called once we've found out
     * if we successfully reached the device.
     */
    protected fun connectToEndpoint(endpoint: Endpoint) {
        logV("Sending a connection request to endpoint $endpoint")
        // Mark ourselves as connecting so we don't connect multiple times
        isConnecting = true

        // Ask to connect
        mConnectionsClient
            .requestConnection(name, endpoint.id, mConnectionLifecycleCallback)
            .addOnFailureListener { e ->
                logW("requestConnection() failed.", e)
                isConnecting = false
                onConnectionFailed(endpoint)
            }
    }

    private fun connectedToEndpoint(endpoint: Endpoint?) {
        logD(String.format("connectedToEndpoint(endpoint=%s)", endpoint), "")
        mEstablishedConnections[endpoint!!.id] = endpoint
        onEndpointConnected(endpoint)
    }

    private fun disconnectedFromEndpoint(endpoint: Endpoint?) {
        logD(String.format("disconnectedFromEndpoint(endpoint=%s)", endpoint), "")
        mEstablishedConnections.remove(endpoint!!.id)
        onEndpointDisconnected(endpoint)
    }

    /**
     * Called when a connection with this endpoint has failed. Override this method to act on the
     * event.
     */
    protected open fun onConnectionFailed(endpoint: Endpoint?) {}

    /** Called when someone has connected to us. Override this method to act on the event.  */
    protected open fun onEndpointConnected(endpoint: Endpoint?) {}

    /** Called when someone has disconnected. Override this method to act on the event.  */
    protected open fun onEndpointDisconnected(endpoint: Endpoint?) {}

    /** Returns a list of currently connected endpoints.  */
    protected val discoveredEndpoints: Set<Endpoint>
        get() = HashSet(mDiscoveredEndpoints.values)

    /** Returns a list of currently connected endpoints.  */
    protected val connectedEndpoints: Set<Endpoint?>
        get() = HashSet(mEstablishedConnections.values)

    /**
     * 현재 연결된 모든 엔드포인트에 [페이로드]를 보냅니다.
     *
     * @param payload 보내려는 데이터.
     */
    protected fun send(payload: Payload) {
        send(payload, mEstablishedConnections.keys)
    }

    fun send(payload: Payload, endpoints: Set<String>) {
        mConnectionsClient
            .sendPayload(ArrayList(endpoints), payload)
            .addOnFailureListener { e -> logW("sendPayload() failed.", e) }
    }

    /**
     * 우리와 연결된 누군가가 우리에게 데이터를 보냈습니다. 이벤트에 대해 작동하도록 이 메서드를 재정의합니다.
     *
     * @param endpoint 발신자.
     * @param payload 데이터.
     */
    protected open fun onReceive(endpoint: Endpoint?, payload: Payload?) {}

    /** Returns the client's name. Visible to others when connecting.  */
    protected abstract val name: String

    /**
     * 서비스 ID를 반환합니다. 이것은 이 연결에 대한 작업을 나타냅니다. 발견할 때,
     * 연결을 고려하기 전에 광고주가 동일한 서비스 ID를 가지고 있는지 확인합니다.
     */
    protected abstract val serviceId: String

    /**
     * 다른 장치에 연결하는 데 사용하는 전략을 반환합니다. 동일한 전략을 사용하는 장치만
     * 및 서비스 ID는 검색 시 나타납니다. Stragies는 들어오고 나가는 수를 결정합니다.
     * 동시에 연결이 가능하며 사용 가능한 대역폭은 얼마입니까?
     */
    protected abstract val strategy: Strategy?
    @CallSuper
    protected open fun logV(msg: String) {
        Log.e(Constants.TAG, msg)
    }

    @CallSuper
    protected open fun logD(msg: String, s: String) {
        Log.e(Constants.TAG, String.format("%s %s", msg, s))
    }

    @CallSuper
    protected open fun logW(msg: String) {
        Log.e(Constants.TAG, msg)
    }

    @CallSuper
    protected open fun logW(msg: String, e: Throwable) {
        Log.e(Constants.TAG, msg, e)
    }

    @CallSuper
    protected open fun logE(msg: String, e: Throwable) {
        Log.e(Constants.TAG, msg, e)
    }

    protected open fun doSendNearBy(auth: FirebaseAuth) {}

    protected open fun doAdvertSendNearBy() {}

    /** Represents a device we can talk to.  */
    protected class Endpoint(val id: String, val name: String) {

        override fun equals(other: Any?): Boolean {
            if (other is Endpoint) {
                return id == other.id
            }
            return false
        }

        override fun hashCode(): Int {
            return id.hashCode()
        }

        override fun toString(): String {
            return String.format("Endpoint{id=%s, name=%s}", id, name)
        }
    }

}

영문 코멘트는 번역기를 통해서 일부 번역을 해 보았습니다. 

 

오늘은 여기까지 기술해 보겠습니다. 다음 이야기는 테스트 중에 발견된 일부 사항을 수정한 다음 계속해 보겠습니다.

 

** 어제 확인이 미처 되지 못했던 부분에 대한 확인을 오늘에야 끝냈습니다.  일단, 권한 기술하는 부분에서 android 13에 맞게 기술이 되지 않았던 부분이 확인이 되었습니다.  2023.05.31 이전에 보셨다면 다시 확인해 보셔야 할 것 같습니다. 

 

아무튼 이렇게 기술해서 정리를 하고 나니 앱에서 상호 접속 하는 부분등이 정리가 되는 것을 확인할 수 있었습니다.

 

neayby 샘플 화면

 

연결이 되고 나면 오른쪽 상단에  개발자 페이지에 제공된 nearby 공식 이미지를 사이즈만 조금 줄여서 표시가 되도록 하였더니, 노출이 됩니다. 연결되었다는 메시지도 나오고요.

 

 

잠깐 동안의 영상을 통해서 접속이 잘 되는 지도 확인해 볼 수 있었습니다. 

 

이렇게 작성된 예제 코드는 아무튼 위에 있는 github을 참고해 보시도록 하시면 되겠습니다.    오늘도 즐 코딩 하세요.

 

 p.s : 한 가지 정리 중인 부분은 payload에 byte 문자열을 보내는 것을 테스트해보고 있는 데, 이미지를 byte 문자열로 해서 보내려고 했더니  아주 작은 사이즈 이미지 말고는 불가능해 보입니다.   그래서 다음번에는 file로 저장해서 전달하는 기능을 구현해 보고 정리를 해 보도록 하겠습니다.

 

반응형