๊ฐœ์š”

  • ์•ˆ๋“œ๋กœ์ด๋“œ๋Š” ๋ธ”๋ฃจํˆฌ์Šค API๋ฅผ ํ†ตํ•ด ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๋Šฅ์„ ์ง€์›

๊ธฐ๋Šฅ

  • ์ฃผ๋ณ€์˜ ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ ๊ฒ€์ƒ‰
  • ๋ธ”๋ฃจํˆฌ์Šค ์–ด๋Œ‘ํ„ฐ ์ฟผ๋ฆฌ
  • RFCOMM์ฑ„๋„ ์„ค์ •
  • ๋‹ค๋ฅธ ๊ธฐ๊ธฐ์— ์—ฐ๊ฒฐ
  • ๋‹ค๋ฅธ ๊ธฐ๊ธฐ์™€ ๋ฐ์ดํ„ฐ ์ฃผ๊ณ ๋ฐ›๊ธฐ
  • ์—ฌ๋Ÿฌ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ

ํŽ˜์–ด๋ง

  • ๋ธ”๋ฃจํˆฌ์Šค ์ง€์›๊ธฐ๊ธฐ๊ฐ€ ์„œ๋กœ ํ†ต์‹ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํŽ˜์–ด๋ง ํ”„๋กœ์„ธ์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ†ต์‹  ์ฑ„๋„์„ ํ˜•์„ฑํ•ด์•ผํ•œ๋‹ค.
  • ํŽ˜์–ด๋ง ๊ณผ์ •
    1. ๊ธฐ๊ธฐ1์€ ์ž์‹ ์„ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ •(discoverable)
    2. ๊ธฐ๊ธฐ2๋Š” discovering์„ ํ†ตํ•ด ์ฃผ๋ณ€์˜ ๊ฒ€์ƒ‰๊ฐ€๋Šฅํ•œ ๊ธฐ๊ธฐ๋ฅผ ํƒ์ƒ‰
    3. ๊ธฐ๊ธฐ2์—์„œ ๊ธฐ๊ธฐ1์„ ๋ฐœ๊ฒฌํ•˜๋ฉด ๊ธฐ๊ธฐ1๋กœ ํŽ˜์–ด๋ง ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Œ
    4. ๊ธฐ๊ธฐ1์—์„œ ํ•ด๋‹น์š”์ฒญ์„ ์ˆ˜๋ฝํ•˜๋ฉด ๋‘ ๊ธฐ๊ธฐ๋Š” ๋ณด์•ˆํ‚ค๋ฅผ ๊ตํ™˜ํ•˜๊ณ  ํ•ด๋‹น ํ‚ค๋ฅผ ์บ์‹œํ•˜๋ฉฐ bonding์„ ์™„๋ฃŒํ•จ
    5. ์„ธ์…˜์ด ์™„๋ฃŒ๋˜๋ฉด ํŽ˜์–ด๋ง ์š”์ฒญ์„ ์‹œ์ž‘ํ•œ ๊ธฐ๊ธฐ๊ฐ€ ๊ฒ€์ƒ‰๊ฐ€๋Šฅํ•œ ๊ธฐ๊ธฐ์— ์—ฐ๊ฒฐํ•œ ์ฑ„๋„์„ ํ•ด์ œ
    6. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋ฏธ ํ‚ค๋ฅผ ๊ตํ™˜ํ•˜์—ฌ ๋‘ ๊ธฐ๊ธฐ๋Š” ์—ฐ๊ฒฐ๋œ ์ƒํƒœ๋กœ ์œ ์ง€๋˜๋ฏ€๋กœ ์„œ๋กœ ๋ฒ”์œ„๋‚ด์—์žˆ๊ณ , ๋‘ ๊ธฐ๊ธฐ ๋ชจ๋‘ ์—ฐ๊ฒฐ์„ ์‚ญ์ œํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋‹ค์‹œ ์—ฐ๊ฒฐ ๊ฐ€๋Šฅ

๋ธ”๋ฃจํˆฌ์Šค ๊ด€๋ จ๊ถŒํ•œ

  • API 31(์•ˆ๋“œ๋กœ์ด๋“œ 12) ์ด์ƒ

    ๊ถŒํ•œ ์ข…๋ฅ˜ ์„ค๋ช…
    BLUETOOTH_SCAN ๋Ÿฐํƒ€์ž„ ์ฃผ๋ณ€ ๊ธฐ๊ธฐ ํƒ์ƒ‰ ๊ธฐ๋Šฅ ํ™œ์šฉ์‹œ ํ™•์ธ (discovering)
    BLUETOOTH_ADVERTISE ๋Ÿฐํƒ€์ž„ ํ˜„์žฌ ๊ธฐ๊ธฐ๋ฅผ ๋‹ค๋ฅธ ๋ธ”๋ฃจํˆฌ์Šค์—์„œ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•˜๋Š” ๊ฒฝ์šฐ (discoverable ์„ค์ •)
    BLUETOOTH_CONNECT ๋Ÿฐํƒ€์ž„ ์ด๋ฏธ ํŽ˜์–ด๋ง๋œ ๋ธ”๋ฃจํˆฌ์Šค์™€ ํ†ต์‹ ํ•˜๋Š” ๊ฒฝ์šฐ ๋ธ”๋ฃจํˆฌ์Šค ํ™œ์„ฑํ™”ํ• ๋•Œ๋„ ํ•„์š”
    ACCESS_FINE_LOCATION ๋Ÿฐํƒ€์ž„ ์•ฑ์—์„œ ํƒ์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ ์œ„์น˜๋ฅผ ์–ป๋Š” ๊ฒฝ์šฐ

    ์•ˆ๋“œ๋กœ์ด๋“œ 6.0 (API ๋ ˆ๋ฒจ 23) ์ดํ›„๋ถ€ํ„ฐ๋Š” ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์ž‘์—…์ด ์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ์œ ์ถ”ํ•  ์ˆ˜ ์žˆ๋Š” ์ž ์žฌ์ ์ธ ์œ„ํ—˜์ด ์žˆ๋‹ค๊ณ  ๊ฐ„์ฃผ๋˜์–ด, ์œ„์น˜ ๊ถŒํ•œ์„ ์š”๊ตฌํ•œ๋‹ค.

    ex) ์‹ค์ œ๋กœ ๋„์„œ๊ด€์— ์žˆ๋Š” ๋ธ”๋ฃจํˆฌ์Šค ๋น„์ฝ˜์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ๋ณ€์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ชฉ์ ์„ ๊ฒธํ•˜๋ฏ€๋กœ ์‚ฌ์šฉ์ž์˜ ๋ฌผ๋ฆฌ์  ์œ„์น˜๋ฅผ ํ™œ์šฉํ•˜๋Š” ์˜ˆ์‹œ์ด๋‹ค.

    android:usesPermissionFlags ์†์„ฑ์„ BLUETOOTH_SCAN ๊ถŒํ•œ ์„ ์–ธ์— ์ถ”๊ฐ€ํ•˜๊ณ  ํ•ด๋‹น ์†์„ฑ์„ neverForLoaction์œผ๋กœ ์„ค์ •ํ•จ์œผ๋กœ์จ ์œ„์น˜๋ฅผ ํ™œ์šฉ์„ ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๊ฒƒ์„ ๊ฐ•ํ•˜๊ฒŒ ์ฃผ์žฅํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค๋งŒ ์ด ๊ฒฝ์šฐ ๋ช‡๋ช‡ BLE๋น„์ฝ˜์ด ์Šค์บ”๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค.

  • ์•ˆ๋“œ๋กœ์ด๋“œ 11 ์ดํ•˜ (API 30 ์ดํ•˜)

    ๊ถŒํ•œ ์ข…๋ฅ˜ ์„ค๋ช…
    BLUETEOOTH ๋Ÿฐํƒ€์ž„ classic, ble ํ†ต์‹  ์ˆ˜ํ–‰์‹œ ํ™•์ธ. ์—ฐ๊ฒฐ, ์—ฐ๊ฒฐ์ˆ˜๋ฝ, ๋ฐ์ดํ„ฐ ์ „์†ก ์‹œ ํ•„์š”
    ACCESS_FINE_LOCATION ๋Ÿฐํƒ€์ž„ ๊ธฐ๊ธฐ ํƒ์ƒ‰์‹œ ํ•„์š”
  • ๊ธฐํƒ€ ๊ถŒํ•œ

    ๊ถŒํ•œ ์ข…๋ฅ˜ ์„ค๋ช…
    BLUETOOTH_ADMIN ์ผ๋ฐ˜ ์ฃผ๋ณ€ ๊ธฐ๊ธฐ ์™€ ํŽ˜์–ด๋ง์„ ํ•˜๊ฑฐ๋‚˜ ๋ธ”๋ฃจํˆฌ์Šค๋ฅผ ์ผœ๊ณ  ๋„๋Š” ์ž‘์—… ๋“ฑ ๋ธ”๋ฃจํˆฌ์Šค ์„ค์ •์„ ์ œ์–ดํ• ๋•Œ (๊ทธ์น˜๋งŒ ๋ธ”๋ฃจํˆฌ์Šค ์„ค์ •์„ ์ œ์–ดํ•˜๋Š” ๋ถ€๋ถ„์€ ์‚ฌ์šฉ์ž๋ฅผ ์„ค์ •์œผ๋กœ ๋ณด๋‚ด๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•˜๋Š”๊ฒƒ๊ฐ™๋‹ค.)
    ACCESS_BACKGROUND_LOCATION ๋Ÿฐํƒ€์ž„ ์•ฑ์ด ์„œ๋น„์Šค๋ฅผ ์ง€์›ํ•˜๊ณ  ์•ˆ๋“œ๋กœ์ด๋“œ 10,11(API 29,30)์—์„œ ์‹คํ–‰๋˜๋Š” ๊ฒฝ์šฐ ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ ๊ฒ€์ƒ‰์— ํ•„์š” (๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์œ„์น˜ ์ •๋ณด๋ฅผ ์š”์ฒญํ•˜๋Š” ๊ฒฝ์šฐ)

๊ด€๋ จ ํด๋ž˜์Šค ๋ฐ ์ธํ„ฐํŽ˜์ด์Šค

  • BluetoothAdapter
    • ๋ชจ๋“  ๋ธ”๋ฃจํˆฌ์Šค ์ƒํ˜ธ์ž‘์šฉ์˜ ์ง„์ž…์  ์—ญํ• 
    • ์ฃผ๋ณ€๊ธฐ๊ธฐ ์Šค์บ”, ํŽ˜์–ด๋ง๋œ ๊ธฐ๊ธฐ ๋ชฉ๋ก ํ™•์ธ, MAC์ฃผ์†Œ๋ฅผ ํ†ตํ•ด BluetoothDeivce ์ธ์Šคํ„ด์Šคํ™”, ์„œ๋ฒ„ ์†Œ์ผ“ ์ƒ์„ฑ ๋“ฑ๋“ฑ
      //onCreate์—์„œ ์„ ์–ธ
      val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
      val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.getAdapter()
    
  • BluetoothDevice
    • ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ๋ฅผ ์ถ”์ƒํ™”ํ•œ ํด๋ž˜์Šค
    • BluetoothSocket์„ ํ™œ์šฉํ•˜์—ฌ ํ•ด๋‹น ๊ธฐ๊ธฐ์™€ ์—ฐ๊ฒฐ์„ ์š”์ฒญ ๊ฐ€๋Šฅ
    • ๋””๋ฐ”์ด์Šค์˜ ์ด๋ฆ„, mac์ฃผ์†Œ, class, ์—ฐ๊ฒฐ ์ƒํƒœ ๋“ฑ ํ™•์ธ ๊ฐ€๋Šฅ
    • ์ž„์˜๋กœ ์ธ์Šคํ„ด์Šคํ™” ์•ˆ๋จ
  • BluetoothSocket
    • ๋ธ”๋ฃจํˆฌ์Šค ์†Œ์ผ“์„ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค (TCP Socket์ด๋ž‘ ๋น„์Šท)
    • InputStream, OutputStream์„ ํ†ตํ•ด ๋‹ค๋ฅธ ๊ธฐ๊ธฐ์™€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตํ™˜ํ•˜๋Š” ์—ฐ๊ฒฐ ์š”์†Œ
  • BluetoothServerSocket
    • ์—ฐ๊ฒฐ์š”์ฒญ์„ ๋ฐ›์•„๋“ค์ด๊ธฐ ์œ„ํ•œ ์„œ๋ฒ„์†Œ์ผ“์„ ๋‚˜ํƒ€๋ƒ„. (TCP์˜ ServerSocket๊ณผ ๋น„์Šท)
    • ๋‘ ๋””๋ฐ”์ด์Šค๊ฐ€ ์—ฐ๊ฒฐ๋˜๊ธฐ ์œ„ํ•ด์„œ ๋ฐ˜๋“œ์‹œ ํ•˜๋‚˜์˜ ๋””๋ฐ”์ด์Šค๋Š” ์ด ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์†Œ์ผ“์„ ์—ด์–ด์•ผํ•จ.
    • ์—ฐ๊ฒฐ์š”์ฒญ์ด ๋“ค์–ด์˜ค๊ณ  ํ•ด๋‹น ์š”์ฒญ์ด ๋ฐ›์•„๋“ค์—ฌ์กŒ๋‹ค๋ฉด ๋ฐ์ดํ„ฐ ๊ตํ™˜์„ ์œ„ํ•œ BluetoothSocket์„ ๋ฐ˜ํ™˜ํ•จ.

๋ธ”๋ฃจํˆฌ์Šค ์—ฐ๊ฒฐ ๊ณผ์ •


1. ์‚ฌ์ „์ž‘์—…


๋ธ”๋ฃจํˆฌ์Šค ๊ถŒํ•œ ์„ค์ •

  • manifest.xml
<manifest>
    <!-- ์•ˆ๋“œ๋กœ์ด๋“œ 11 ์ดํ•˜์—์„œ ํ•„์š”ํ•œ ๊ถŒํ•œ -->
    <uses-permission android:name="android.permission.BLUETOOTH"
                     android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
                     android:maxSdkVersion="30" />

    <!-- ์ฃผ๋ณ€๊ธฐ๊ธฐ ์Šค์บ” ๊ธฐ๋Šฅ์ด ์กด์žฌํ• ๋•Œ๋งŒ ์ถ”๊ฐ€
		    ๋งŒ์•ฝ ์Šค์บ” ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉ์ž์˜ ๋ฌผ๋ฆฌ์  ์œ„์น˜ ๋„์ถœ์— ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด 
		    android:usesPermissionFloags="neverForLocation"
		    ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฌผ๋ฆฌ์  ์œ„์น˜๋ฅผ ๋„์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š”๊ฒƒ์„ ๊ฐ•๋ ฅํ•˜๊ฒŒ ์„ ์–ธํ•œ๋‹ค.
          -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

    <!-- ํ˜„์žฌ ๊ธฐ๊ธฐ๋ฅผ ๋‹ค๋ฅธ ๊ธฐ๊ธฐ์—์„œ ์Šค์บ” ๊ฐ€๋Šฅํ•˜๊ฒŒ(discoverable) ์„ค์ •ํ•  ๋•Œ๋งŒ ์„ ์–ธ -->
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

    <!-- ์ด๋ฏธ ํŽ˜์–ด๋ง๋œ ๊ธฐ๊ธฐ์™€ ์—ฐ๊ฒฐ ๊ธฐ๋Šฅ์ด ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ ์„ ์–ธ -->
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <!-- ์ฃผ๋ณ€ ๊ธฐ๊ธฐ ์Šค์บ” ๊ธฐ๋Šฅ์ด ์กด์žฌํ•˜๊ณ , ํ•ด๋‹น ์Šค์บ” ๊ฒฐ๊ณผ๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์˜ ๋ฌผ๋ฆฌ์  ์œ„์น˜๋ฅผ ๋„์ถœํ•ด๋‚ด๋Š” ๊ฒฝ์šฐ ์„ ์–ธ
			    '๋‚˜๋Š” ์Šค์บ”๋งŒ ํ•˜๋ฉด๋œ๋‹ค' ๋ผ๋ฉด ์ด ๊ถŒํ•œ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ƒ๋‹จ BLUETOOTH_SCAN ๊ถŒํ•œ์— ํ”Œ๋ž˜๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.
     -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    ...
</manifest>
  • ๋Ÿฐํƒ€์ž„ ๊ถŒํ•œ ์š”์ฒญ (MainActivity.kt)
private lateinit var bluetoothEnableSettingLauncher:ActivityResultLauncher<Intent>

private val bluetoothPermissions = mutableListOf<String>(/*๋Ÿฐํƒ€์ž„ ๊ถŒํ•œ ์ถ”๊ฐ€*/)

override fun onCreate(savedInstanceState: Bundle?) {
	...
	
	bluetoothPermissionLauncher =
	            registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
	                //๊ฑฐ๋ถ€๋‹นํ•œ ๊ถŒํ•œ ๋ฆฌ์ŠคํŠธ
	                val deniedList = result.filter { !it.value }.map { it.key }
									
	                if (deniedList.isNotEmpty()) {
			                //๊ฑฐ๋ถ€๋‹นํ•œ ๊ถŒํ•œ์ค‘ ์„ค๋ช…์ด ํ•„์š”ํ•œ ๊ถŒํ•œ๊ณผ ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ถŒํ•œ์„ ๋ถ„๋ฆฌ
	                    val map = deniedList.groupBy { permission ->
	                        if (shouldShowRequestPermissionRationale(permission)) "DENIED" else "EXPLAINED"
	                    }
	                    //์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ๊ฑฐ๋ถ€๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ๊ฒฝ์šฐ ๊ถŒํ•œ์— ๋Œ€ํ•œ ์„ค๋ช…์ด ํ•„์š”
	                    map["DENIED"]?.let {
	                        explainBluetoothConnectPermission()
	                    }
	                }
	            }
	     
	 //๊ถŒํ•œ ์š”์ฒญ ์‹œ์ž‘
	requestBluetoothPermission()
            
}      

//๋ธ”๋ฃจํˆฌ์Šค ๊ถŒํ•œ ์š”์ฒญ ๋กœ์ง
private fun requestBluetoothPermission() {
    val notGrantedPermissionList = mutableListOf<String>()

    for (permission in bluetoothPermissions) {

        val result = ContextCompat.checkSelfPermission(this, permission)
        if (result == PackageManager.PERMISSION_GRANTED) continue
        notGrantedPermissionList.add(permission)
        if (shouldShowRequestPermissionRationale(permission)) explainBluetoothConnectPermission()

    }
    if (notGrantedPermissionList.isNotEmpty()) {
        bluetoothPermissionLauncher.launch(notGrantedPermissionList.toTypedArray())
  }
}

//์‚ฌ์šฉ์ž์—๊ฒŒ ๊ถŒํ•œ์— ๋Œ€ํ•ด ์„ค๋ช…
private fun explainBluetoothConnectPermission() {
    Toast.makeText(
        this,
        "์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ํ•ด๋‹น ๊ถŒํ•œ์ด ํ•„์š”ํ•จ์„ ์„ค๋ช…",
        Toast.LENGTH_SHORT
    ).show()
}
		


๋ธ”๋ฃจํˆฌ์Šค๋ฅผ ํ•„์ˆ˜๋กœ ์„ค์ •ํ•˜๊ธฐ

  • ์ด๊ฑฐ ํ•ด๋‘๋ฉด ๋งˆ์ผ“์—์„œ ๋ธ”๋ฃจํˆฌ์Šค๋ฅผ ์ง€์›ํ•˜๋Š” ๊ธฐ๊ธฐ๋งŒ ํ•ด๋‹น ์•ฑ์„ ๋ณผ ์ˆ˜ ์žˆ์Œ

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


๋ธ”๋ฃจํˆฌ์Šค ์ง€์› ๊ธฐ๊ธฐ์ธ์ง€ ํ™•์ธ

  • <uses-feature โ€ฆ android:required=false/>์ธ ๊ฒฝ์šฐ๋งŒ ํ•„์š”. (๋ธ”๋ฃจํˆฌ์Šค๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๊ธฐ์—์„œ๋„ ํ•ด๋‹น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ œ๊ณตํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋ฏ€๋กœ ์„ค์ •์„ ํ™•์ธํ•ด์•ผํ•œ๋‹ค.)
  • BluetoothAdapter๊ฐ€ null์ธ ๊ฒฝ์šฐ ํ•ด๋‹น ๊ธฐ๊ธฐ๋Š” ๋ธ”๋ฃจํˆฌ์Šค๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค.

      if (bluetoothAdapter == null) {
      // ๋ธ”๋ฃจํˆฌ์Šค๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋””๋ฐ”์ด์Šค
      }
    


๋ธ”๋ฃจํˆฌ์Šค๊ฐ€ ํ™œ์„ฑํ™” ๋˜์–ด์žˆ๋Š”์ง€ ํ™•์ธ + ํ™œ์„ฑํ™” ์š”์ฒญ

  • ๋””๋ฐ”์ด์Šค ๋ธ”๋ฃจํˆฌ์Šค ์„ค์ •์—์„œ ๋ธ”๋ฃจํˆฌ์Šค ์‚ฌ์šฉ์ด ํ™œ์„ฑํ™” ๋˜์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • BluetoothAdapter.isEnabled ๊ฐ€ false์ด๋ฉด ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์ด๋‹ค
  • ActivityResultLauncher๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ธ”๋ฃจํˆฌ์Šค ํ™œ์„ฑํ™”๋ฅผ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋‹ค.
private lateinit var bluetoothEnableSettingLauncher:ActivityResultLauncher<Intent>
...

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

    val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
    val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter

    bluetoothEnableSettingLauncher=registerForActivityResult(ActivityResultContracts.StartActivityForResult()){result->
        if (result.resultCode == RESULT_OK) {
            Toast.makeText(this, "Bluetooth enabled", Toast.LENGTH_SHORT).show()
        } else if (result.resultCode == RESULT_CANCELED) {
            Toast.makeText(this, "bluetooth not enable", Toast.LENGTH_SHORT).show()
        }
    }
    setContent {
        PlaygroundForAndroidTheme {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colorScheme.background
            ) {
...

                //๋ธ”๋ฃจํˆฌ์Šค ํ™œ์„ฑํ™” ์ƒํƒœ ํ™•์ธ -> ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ๋ผ๋ฉด ์„ค์ • ํ™”๋ฉด์œผ๋กœ ์ด๋™
                if (bluetoothAdapter?.isEnabled == false) {
		                val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
		                //BLUETOOTH_CONNECT(api31์ด์ƒ), ๋˜๋Š” BLUETOOTH(api 30์ดํ•˜) ๊ฐ€ ํ—ˆ์šฉ ์•ˆ๋˜์–ด์žˆ์„๋•Œ ํ˜ธ์ถœํ•˜๋ฉด ํ„ฐ์ง€๋‹ˆ๊นŒ ์ฃผ์˜!
										bluetoothEnableLauncher.launch(enableBtIntent)
                }
            }
        }
    }
}


2. ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ ์ฐพ๊ธฐ(discovering,scaning,inquiring)


(์‹œ์ž‘ํ•˜๊ธฐ ์ „) ํŽ˜์–ด๋ง๋œ ๊ธฐ๊ธฐ ์ฟผ๋ฆฌ

discovering์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „ ์ด๋ฏธ ํŽ˜์–ด๋ง ๋œ ๊ธฐ๊ธฐ ๋ชฉ๋ก์„ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ถˆ๋Ÿฌ์™€์„œ ํŽ˜์–ด๋ง์„ ์›ํ•˜๋Š” ๊ธฐ๊ธฐ๊ฐ€ ์ด๋ฏธ ํŽ˜์–ด๋ง ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach { device ->
   val deviceName = device.name
   val deviceHardwareAddress = device.address // MAC address
}


๋ฐฉ๋ฒ•1. Companion Device Manager API** (CDM API) ํ™œ์šฉ(๊ถŒ์žฅ)

  • Android 8.0 (API 26)์ด์ƒ์—์„œ ํ™œ์šฉ๊ฐ€๋Šฅ
  1. manifest.xml์— companion device ์„ค์ •

     <uses-feature android:name="android.software.companion_device_setup"/>
    
  2. Device Filter ๋งŒ๋“ค๊ธฐ

     val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
             // ๋””๋ฐ”์ด์Šค ์ด๋ฆ„์ด ์•„๋ž˜ ํŒจํ„ด๊ณผ ๋งž๋Š” ๋””๋ฐ”์ด์Šค๋งŒ ํƒ์ƒ‰ํ•˜๋„๋ก ํ•„ํ„ฐ๋ง
             .setNamePattern(Pattern.compile("My device"))
             //์„œ๋น„์Šค UUID๊ฐ€ ์•„๋ž˜ ํŒจํ„ด๊ณผ ์ผ์น˜ํ•˜๋Š” ๋””๋ฐ”์ด์Šค๋งŒ ํƒ์ƒ‰ํ•˜๋„๋ก ํ•„ํ„ฐ๋ง
             .addServiceUuid(ParcelUuid(UUID(0x123abcL, -1L)), null)
             .build()
    
  3. AssociationRequest๋งŒ๋“ค๊ธฐ + DeviceFilter์ถ”๊ฐ€

     val pairingRequest: AssociationRequest = AssociationRequest.Builder()
             // deviceFilter ์ถ”๊ฐ€
             .addDeviceFilter(deviceFilter)
             // ํ•„ํ„ฐ์— ๋งž๋Š” ๋””๋ฐ”์ด์Šค ํ•˜๋‚˜๋ฅผ ์ฐพ์œผ๋ฉด ํƒ์ƒ‰ ์ค‘๋‹จ
             .setSingleDevice(true)
             .build()
    
  4. deviceManager.associate()๋กœ ๋””๋ฐ”์ด์Šค ํƒ์ƒ‰ ๋ฐ ์‹œ์Šคํ…œ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋„์šฐ๊ธฐ

    • Android 13 (API 33) ์ด์ƒ ๊ธฐ๊ธฐ

        val deviceManager =
          requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
                
        private lateinit var bluetoothScanLauncher: ActivityResultLauncher<IntentSenderRequest> 
              
        override fun onCreate(){
        	bluetoothScanLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {result->
        	//์œ ์ €๊ฐ€ ํŽ˜์–ด๋ง ํ•˜๊ณ  ์‹ถ์€ ๋””๋ฐ”์ด์Šค ์„ ํƒ
                            Log.d(TAG,"bluetoothScan finish")
                            if(result.resultCode==Activity.RESULT_OK){
                                val device = if (Build.VERSION.SDK_INT >= 33) result.data?.getParcelableExtra(
                                    BluetoothDevice.EXTRA_DEVICE,
                                    BluetoothDevice::class.java
                                ) else result.data?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                                Log.d(TAG,"Request pairing device name : ${device?.name}")
                                //ํ•ด๋‹น ๋””๋ฐ”์ด์Šค์— ํŽ˜์–ด๋ง ์š”์ฒญ 
                                device?.createBond()
                            }else{
                                Log.d(TAG,"User cancel")
                            }
        	}
        }
              
        val executor: Executor =  Executor { it.run() }
              
        deviceManager.associate(pairingRequest,
            executor,
            object : CompanionDeviceManager.Callback() {
            // ๋””๋ฐ”์ด์Šค ํƒ์ƒ‰ ์„ฑ๊ณต์‹œ ํ˜ธ์ถœ๋จ
            override fun onAssociationPending(intentSender: IntentSender) {
        		    // ์œ ์ €์—๊ฒŒ ์‹œ์Šคํ…œ ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ๋„์›Œ์„œ ์›ํ•˜๋Š” ๊ธฐ๊ธฐ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๊ฒŒํ•œ๋‹ค.
                val intentSenderRequest = IntentSenderRequest.Builder(intentSender).build()
                        bluetoothScanLauncher.launch(intentSenderRequest)
                }
            }
              
            override fun onAssociationCreated(associationInfo: AssociationInfo) {
                // ์—ฐ๊ฒฐ ์ƒ์„ฑ์‹œ ํ˜ธ์ถœ๋จ
            }
              
            override fun onFailure(errorMessage: CharSequence?) {
                // ํƒ์ƒ‰ ์‹คํŒจ
             }
        })
      
    • ์•ˆ๋“œ๋กœ์ด๋“œ 12L (API 32) ์ดํ•˜ ๊ธฐ๊ธฐ

        val deviceManager =
              requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
                    
        private lateinit var bluetoothScanLauncher: ActivityResultLauncher<IntentSenderRequest> 
              
        override fun onCreate(){
        	bluetoothScanLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {result->
        	//์œ ์ €๊ฐ€ ํŽ˜์–ด๋ง ํ•˜๊ณ  ์‹ถ์€ ๋””๋ฐ”์ด์Šค ์„ ํƒ
                            Log.d(TAG,"bluetoothScan finish")
                            if(result.resultCode==Activity.RESULT_OK){
                                val device = if (Build.VERSION.SDK_INT >= 33) result.data?.getParcelableExtra(
                                    BluetoothDevice.EXTRA_DEVICE,
                                    BluetoothDevice::class.java
                                ) else result.data?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                                Log.d(TAG,"Request pairing device name : ${device?.name}")
                                //ํ•ด๋‹น ๋””๋ฐ”์ด์Šค์— ํŽ˜์–ด๋ง ์š”์ฒญ 
                                device?.createBond()
                            }else{
                                Log.d(TAG,"User cancel")
                            }
        	}
        }
              
        deviceManager.associate(pairingRequest,
            object : CompanionDeviceManager.Callback() {
                // ๋””๋ฐ”์ด์Šค ๋ฐœ๊ฒฌ์‹œ ํ˜ธ์ถœ๋˜๊ณ , ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹œ์Šคํ…œ ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ๋„์›€
                override fun onDeviceFound(chooserLauncher: IntentSender) {
                    val intentSenderRequest = IntentSenderRequest.Builder(intentSender).build()
                        bluetoothScanLauncher.launch(intentSenderRequest)
                }
              
                override fun onFailure(error: CharSequence?) {
                    // ํƒ์ƒ‰ ์‹คํŒจ์‹œ ํ˜ธ์ถœ
                }
            }, null)
      

    โ—๏ธ API์ˆ˜์ค€์— ๋”ฐ๋ผ ํŠน์ • API ์ˆ˜์ค€์—์„œ๋Š” ํƒ์ƒ‰๋œ ๊ธฐ๊ธฐ๊ฐ€ ์—†๋‹ค๋ฉด ์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค. (์•ˆ๋“œ๋กœ์ด๋“œ 12(API31) ์ˆ˜์ค€ ํ…Œ์ŠคํŠธ๊ธฐ๊ธฐ์—์„œ ์ด ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ๋‹ค.) ( ๊ด€๋ จํ•ด์„œ ์ •๋ฆฌํ•œ ๋‚ด์šฉ(stackOverflow) )


๋ฐฉ๋ฒ•2. ๋ธ”๋ฃจํˆฌ์Šค ์–ด๋Œ‘ํ„ฐ ์‚ฌ์šฉ

  1. ๊ฒ€์ƒ‰ ๋‹นํ•˜๋Š” ๊ธฐ๊ธฐ์—์„œ discoverable์„ค์ •
    • ๋น„์ฝ˜์ด๋‚˜ ๋‹ค๋ฅธ ์—ฌ๋Ÿฌ ๋ธ”๋ฃจํˆฌ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๊ธฐ๋“ค๊ณผ ๋‹ค๋ฅด๊ฒŒ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ธฐ๊ธฐ๋Š” ํ•ญ์ƒ discoverableํ•˜์ง€ ์•Š๋‹ค.
    • ๋”ฐ๋ผ์„œ ์ž์‹ ์˜ ๊ธฐ๊ธฐ๊ฐ€ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฒ€์ƒ‰๋˜๊ณ  ์‹ถ๋‹ค๋ฉด ๊ธฐ๊ธฐ๋ฅผ discoverableํ•˜๊ฒŒ ์„ค์ •ํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค.
    • ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด discoverable ์„ค์ •์„ ์œ„ํ•œ ์‹œ์Šคํ…œ ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋ณ€๊ฒฝ๋œ discoverable์„ค์ • ์ƒํƒœ(์‹œ์Šคํ…œ ๋‹ค์ด์–ผ๋กœ๊ทธ ๊ฒฐ๊ณผ)๋Š” BroadcastReceiver๋กœ ํ™•์ธ๊ฐ€๋Šฅ
     private lateinit var bluetoothDiscoverableLauncher: ActivityResultLauncher<Intent>
        
     override fun onCreate(){
        
     	bluetoothDiscoverableLauncher =
     	                it.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
        	
     	//discoverable ํ—ˆ์šฉ ์š”์ฒญ
     	setBluetoothDiscoverable()
     }
        
     fun setBluetoothDiscoverable() {
         val discoverableIntent: Intent =
             Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
     		        //ํƒ์ƒ‰ ํ—ˆ์šฉ์‹œ๊ฐ„์„ 30์ดˆ๋กœ ์„ค์ • (default๋Š” 2๋ถ„,์ตœ๋Œ€ 1์‹œ๊ฐ„๊นŒ์ง€)
     		        //์‹œ๊ฐ„์„ 0์œผ๋กœ ์„ค์ • ์‹œ ํ•ญ์ƒ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ์ฃผ์˜!!
                 putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 30)
             }
         bluetoothScanLauncher.launch(discoverableIntent)
     }
    
  2. ๊ธฐ๊ธฐ ๊ฒ€์ƒ‰ ๋ฐ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ™•์ธ
    • bluetoothAdapter์˜ startDiscovery() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ฃผ๋ณ€ ๊ธฐ๊ธฐ ํƒ์ƒ‰์„ ์‹œ์ž‘ํ•œ๋‹ค.
    • ํ•ด๋‹น ํ•จ์ˆ˜๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฉฐ ์ผ๋ฐ˜์ ์œผ๋กœ 12์ดˆ๋™์•ˆ ์Šค์บ”์ด ์‹œ์ž‘๋œ๋‹ค.
    • ํƒ์ƒ‰๋œ ๊ธฐ๊ธฐ๋Š” BroadcastReceiver๋ฅผ ํ†ตํ•ด ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
     val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
     val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.getAdapter()
        
     // ํƒ์ƒ‰๋œ ๊ฒฐ๊ณผ๋ฅผ ์ˆ˜์‹ ํ•˜๊ธฐ ์œ„ํ•œ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋ฆฌ์‹œ๋ฒ„ ์ƒ์„ฑ
     private val receiver = object : BroadcastReceiver() {
        
        override fun onReceive(context: Context, intent: Intent) {
            val action: String = intent.action
            when(action) {
                BluetoothDevice.ACTION_FOUND -> {
                    // ํƒ์ƒ‰๋œ ๊ธฐ๊ธฐ ์ •๋ณด
                    val device: BluetoothDevice =
                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                    val deviceName = device.name
                    val deviceHardwareAddress = device.address // MAC address
                }
            }
        }
     }
        
     override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // ํƒ์ƒ‰๋œ ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ ์ •๋ณด๋ฅผ ๋ฐ›๊ธฐ ์œ„ํ•œ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ํ•„ํ„ฐ ์ƒ์„ฑ
        val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
        // ์•กํ‹ฐ๋น„ํ‹ฐ์— ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋ฆฌ์‹œ๋ฒ„ ๋“ฑ๋ก
        registerReceiver(receiver, filter)
           
        ...
        // ์›ํ•˜๋Š” ์‹œ๊ธฐ์— ํƒ์ƒ‰ ์‹œ์ž‘
        bluetoothAdapter.startDiscovery()
     }
        
     override fun onDestroy() {
        super.onDestroy()
        ...
        
        // ์•กํ‹ฐ๋น„ํ‹ฐ ์ข…๋ฃŒ์‹œ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋ฆฌ์‹œ๋ฒ„ ํ•ด์ œ!
        unregisterReceiver(receiver)
     }
    


3. ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ ์—ฐ๊ฒฐ

  • ๋ธ”๋ฃจํˆฌ์Šค ์—ฐ๊ฒฐ์€ ์„œ๋ฒ„, ํด๋ผ์ด์–ธํŠธ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์กด์žฌํ•œ๋‹ค.
  • ์—ฐ๊ฒฐ๊ณผ์ •
    1. ๊ธฐ๊ธฐ A์—์„œ ์„œ๋ฒ„์†Œ์ผ“์„ ์—ด์–ด ์—ฐ๊ฒฐ ์š”์ฒญ์„ ์ˆ˜์‹  ๋Œ€๊ธฐํ•œ๋‹ค.
    2. ํด๋ผ์ด์–ธํŠธ ๊ธฐ๊ธฐ B์—์„œ ๋ธ”๋ฃจํˆฌ์Šค ์†Œ์ผ“์„ ํ†ตํ•ด ๊ธฐ๊ธฐA์˜ ์„œ๋ฒ„ ์†Œ์ผ“์— ์—ฐ๊ฒฐ์„ ์š”์ฒญํ•œ๋‹ค.
    3. ๊ธฐ๊ธฐA์—์„œ ํ•ด๋‹น ์š”์ฒญ์„ ๋ฐ›์•„๋“ค์—ฌ RFCOMM์ฑ„๋„์ด ํ˜•์„ฑ๋˜๊ณ  (์ถ”๊ฐ€ ์—ฐ๊ฒฐ์ด ์—†๋‹ค๋ฉด) ์„œ๋ฒ„ ์†Œ์ผ“์„ ๋‹ซ๋Š”๋‹ค.
    4. ๋ธ”๋ฃจํˆฌ์Šค ์†Œ์ผ“์„ ํ™œ์šฉํ•˜์—ฌ ์„œ๋กœ ๊ณ„์† ํ†ต์‹ ํ•œ๋‹ค.


์„œ๋ฒ„ ์ฝ”๋“œ

  • listenUsingRfcommWithServiceRecord(String name,UUID uuid)๋ฅผ ํ†ตํ•ด BluetoothServerSocket์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
    • name:String : SDP(Service Discovery Protocol) ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค์— ์“ฐ์ด๋Š” ์ด๋ฆ„, ์ž„์˜๋กœ ๊ฒฐ์ •
    • uuid:UUID(Universally Unique Identifier) : ์—ฐ๊ฒฐ์„ ์š”์ฒญํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ์˜ uuid๊ฐ€ ํ•ด๋‹น uuid์™€ ๊ฐ™์•„์•ผ ์—ฐ๊ฒฐ ์ˆ˜๋ฝ ๊ฐ€๋Šฅ
private var serverSocket: BluetoothServerSocket? = null
private var connectSocket: BluetoothSocket? = null

private val myUUID =
        ParcelUuid(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
private val NAME = "BluetoothChat" 
    
...
    
//์„œ๋ฒ„ ์†Œ์ผ“ ์—ฌ๋Š” ์‹œ์ ์— ์ถ”๊ฐ€
CoroutineScope(Dispatchers.Main).launch {
		//์„œ๋ฒ„ ์†Œ์ผ“ ์—ด๊ธฐ
    val res = async { service.openServerSocket() }.await()
    navController.popBackStack()
    if (res) {
        //์„ฑ๊ณต์ ์œผ๋กœ ์—ฐ๊ฒฐ๋์„๋•Œ UI๊ฐฑ์‹  ๋“ฑ ๋กœ์ง
    } else {
        //์—ฐ๊ฒฐ ์‹คํŒจ์‹œ UI๊ฐฑ์‹  ๋“ฑ ๋กœ์ง
    }
}

...

suspend fun openServerSocket() = withContext(Dispatchers.IO) {

        serverSocket?.close()
        connectSocket?.close()

        try {
            serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, myUUID.uuid)
            Log.d(TAG, "open ServerSocket : $serverSocket")
            //accept()๋Š” ์—ฐ๊ฒฐ๋ ๋•Œ๊นŒ์ง€ block ์‹œ์ผฐ๋‹ค๊ฐ€ ์—ฐ๊ฒฐ์‹œ ์ดํ›„ ํ†ต์‹ ์— ํ•„์š”ํ•œ BluetoothSocket์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
		        //๊ธฐ๋ณธ์ ์œผ๋กœ blocking call์ด๋ฏ€๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰ํ•ด์•ผํ•จ
            connectSocket = serverSocket?.accept()
						
						//์—ฐ๊ฒฐ ์„ฑ๊ณต์‹œ ๋กœ์ง
            connectSocket?.also {
		            //์„œ๋ฒ„์†Œ์ผ“ ๋‹ซ๊ธฐ
                serverSocket?.close()
                val device = it.remoteDevice
                Log.d(TAG, "connect success.\n connected with $device")
								
								/*TODO : ํ†ต์‹ 
								* ์—ฐ๊ฒฐ ์ดํ›„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๋กœ์ง์„ ์—ฌ๊ธฐ์„œ ์‹คํ–‰ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ์ด์–ด์ง)
								*/

            }
        } catch (e: IOException) {
            Log.e(TAG, "open ServerSocket fail : $e")
            return@withContext false
        }
        return@withContext true
    }
    


ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ

  • createRfcommSocketToServiceRecord(UUID)์„ ํ†ตํ•ด BluetootoSocket๊ฐ€์ ธ์˜ค๊ธฐ
private var serverSocket: BluetoothServerSocket? = null
private var connectSocket: BluetoothSocket? = null

private val myUUID =
        ParcelUuid(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
private val NAME = "BluetoothChat"

suspend fun requestConnect(address: String) = withContext(Dispatchers.IO) {

        connectSocket?.close()
        serverSocket?.close()
				
				//์—ฐ๊ฒฐ์„ ์›ํ•˜๋Š” ๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ ํ˜ธ์ถœ
        val device = bluetoothAdapter.getRemoteDevice(address)
        //bluetoothSocket๊ฐ€์ ธ์˜ค๊ธฐ
        connectSocket =
            device.createRfcommSocketToServiceRecord(myUUID.uuid)
            
        try {
		        //connectํ•จ์ˆ˜๋กœ ์—ฐ๊ฒฐ ์š”์ฒญ ์‹œ์ž‘.
		        //blocking call์ด๋ฏ€๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰ํ•ด์•ผํ•จ
		        //์—ฐ๊ฒฐ ์‹คํŒจ, ๋˜๋Š” ํƒ€์ž„์•„์›ƒ(12์ดˆ)์‹œ IOException๋ฐœ์ƒ
            connectSocket?.connect()
            _connectedDevice.value = connectSocket?.remoteDevice
            Log.d(TAG, "connect success.\n connected with $device")

            /*TODO : ํ†ต์‹ 
						* ์—ฐ๊ฒฐ ์ดํ›„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๋กœ์ง์„ ์—ฌ๊ธฐ์„œ ์‹คํ–‰ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ์ด์–ด์ง)
						*/

        } catch (e: IOException) {
            Log.e(TAG, "connect fail : $e")
            return@withContext false
        }
        
        //async๋กœ ํ˜ธ์ถœ์‹œ ์—ฐ๊ฒฐ ์„ฑ๊ณต,์‹คํŒจ์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜
        return@withContext true
    }


4. ๋ธ”๋ฃจํˆฌ์Šค ๋ฐ์ดํ„ฐ ์ „์†ก

  • ์—ฐ๊ฒฐ๋œ ๋ธ”๋ฃจํˆฌ์Šค ์†Œ์ผ“์˜ InputStream, OutputStream ์†Œ์ผ“์„ ํ†ตํ•ด ์—ฐ๊ฒฐ๋œ ๊ธฐ๊ธฐ์™€ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฝ์„๋•Œ๋Š” read(byte[]) ํ•จ์ˆ˜, ์“ธ๋•Œ๋Š” write(byte[]) ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.


์ƒ๋Œ€ ๊ธฐ๊ธฐ๊ฐ€ ๋ณด๋‚ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ธฐ

//๋‹ค๋ฅธ ์ฝ”๋ฃจํ‹ด์Šค์ฝ”ํ”„์™€ ๋…๋ฆฝ์ ์œผ๋กœ ์—ฐ๊ฒฐ์„ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ์—ฐ๊ฒฐ์šฉ ์ฝ”๋ฃจํ‹ด์Šค์ฝ”ํ”„ ์ƒ์„ฑ
private val connectingScope = CoroutineScope(Job() + Dispatchers.IO)
//ํ•˜๋‚˜์˜ connecting๋งŒ ์œ ์ง€ํ•˜๋ ค๊ณ  ํ• ๋•Œ ํ•ด๋‹น job์„ ์ €์žฅ
//์—ฌ๋Ÿฌ๊ฐœ์˜ job์„ ๊ด€๋ฆฌํ•˜๋ ค๊ณ  ํ•œ๋‹ค๋ฉด map<String,Job>ํƒ€์ž…์œผ๋กœ ์„ ์–ธ ํ›„ ์ €์žฅํ•ด์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ
private var connectingJob:Job?=null

...
//listening์„ ์‹œ์ž‘ํ•˜๊ณ  ์‹ถ์€ ์œ„์น˜์—์„œ ํ˜ธ์ถœ
//connectSocket = bluetoothSocket
connectSocket?.{
	//๊ธฐ์กด์— ์—ฐ๊ฒฐ์ค‘์ด์—ˆ๋‹ค๋ฉด ํ•ด๋‹น ์—ฐ๊ฒฐ ์ทจ์†Œ
	connectingJob?.cancel()
	//์ƒˆ๋กœ์šด ์—ฐ๊ฒฐ ์‹œ์ž‘
	connectingJob=connectingScope.launch{ listenMessage(it) }
}

private suspend fun listenMessage(connectSocket: BluetoothSocket) =
        withContext(Dispatchers.IO) {
            var numBytes: Int
            val buffer = ByteArray(1024)
            val inputStream = connectSocket.inputStream

						//๋ฌดํ•œ ๋ฃจํ”„๋ฅผ ํ†ตํ•ด ๊ณ„์†ํ•ด์„œ ๋ฉ”์‹œ์ง€ listening
            while (true) {
                numBytes = try {
                    inputStream.read(buffer)
                } catch (e: IOException) {
                    Log.d(TAG, "Input Stream was disconnected", e)
                    break
                }

                if (numBytes > 0) {
                    _messageFlow.emit(String(buffer, 0, numBytes))
                    Log.d(TAG, "listenMessage : $buffer")
                }
            }
        }

override fun onDestroy(owner: LifecycleOwner) {
    activity?.unregisterReceiver(receiver)
    connectingScope.cancel()
}


์ƒ๋Œ€์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๊ธฐ

fun requestSendMessage(text:String){
	viewModelScope.launch {
	    bluetoothService?.sendMessage(text.toByteArray())
	}
}

...

suspend fun sendMessage(msg: ByteArray) = withContext(Dispatchers.IO) {
    val outputStream = connectSocket?.outputStream
    try {
        outputStream?.write(msg)
    } catch (e: IOException) {
        Log.e(TAG, "Error occurred when sending data", e)
    }
}


5. ์—ฐ๊ฒฐ ์ข…๋ฃŒ

fun finishConnect(): Boolean { //์ข…๋ฃŒ ์„ฑ๊ณต์‹œ true, ์‹คํŒจ์‹œ false ๋ฐ˜ํ™˜
    try {
		    //์—ฐ๊ฒฐ์ž‘์—… ์ข…๋ฃŒ
		    connectingJob?.cancel()
        connectedDevice.value = null
        //๋ธ”๋ฃจํˆฌ์Šค ์†Œ์ผ“ ์ข…๋ฃŒ
        connectSocket?.close()
    } catch (e: IOException) {
        Log.e(TAG, "Could not close the connect socket", e)
        return false
    }
    return true
}


์˜ˆ์‹œ

์ด๊ฒŒ ์ฝ”๋“œ๋žฉ์ธ๊ฐ€์— ๋ธ”๋ฃจํˆฌ์Šค ์ฑ„ํŒ… ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์˜ˆ์ œ๊ฐ€ ์žˆ์—ˆ๋Š”๋ฐ ์ด๊ฒŒ ์ข€ ์˜›๋‚ ์ฝ”๋“œ๋ผ์„œ ์‹ค์Šตํ• ๊ฒธ ๋‹ค์‹œ ๋งŒ๋“ค์–ด๋ดค๋‹ค.

BluetoothChat

์ด๋ฒˆ์— ๊ณต๋ถ€ํ•˜๋ฉด์„œ ๋ณด๋‹ˆ๊นŒ ๊ณ ์น  ๋ถ€๋ถ„์ด ์žˆ๋Š”๊ฒƒ ๊ฐ™์•„์„œ ์กฐ๋งŒ๊ฐ„ ์‹œ๊ฐ„๋‚˜๋ฉด ์ˆ˜์ •ํ•  ์˜ˆ์ •์ด๋‹ค.

์ง€๊ธˆ์€

  • java->kotlink
  • Thread->Coroutine,flow
  • xml -> Compose

์ด ์ •๋„๋ฅผ ๋ฐ˜์˜ํ•ด๋’€๊ณ , ๋‹ค์Œ์œผ๋กœ๋Š” CompanionDeviceManager API๋กœ ์ด์ „ํ•ด ๋ณผ ์ƒ๊ฐ์ด๋‹ค.

์ฐธ๊ณ 

Bluetooth overview ย |ย  Connectivity ย |ย  Android Developers

Set up Bluetooth ย |ย  Connectivity ย |ย  Android Developers

Find Bluetooth devices ย |ย  Connectivity ย |ย  Android Developers

Connect Bluetooth devices ย |ย  Connectivity ย |ย  Android Developers

Transfer Bluetooth data ย |ย  Connectivity ย |ย  Android Developers

Companion device pairing ย |ย  Connectivity ย |ย  Android Developers

Bluetooth permissions ย |ย  Connectivity ย |ย  Android Developers

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ