๊ฐ์
- ์๋๋ก์ด๋๋ ๋ธ๋ฃจํฌ์ค API๋ฅผ ํตํด ๋ธ๋ฃจํฌ์ค ๊ธฐ๋ฅ์ ์ง์
๊ธฐ๋ฅ
- ์ฃผ๋ณ์ ๋ธ๋ฃจํฌ์ค ๊ธฐ๊ธฐ ๊ฒ์
- ๋ธ๋ฃจํฌ์ค ์ด๋ํฐ ์ฟผ๋ฆฌ
- RFCOMM์ฑ๋ ์ค์
- ๋ค๋ฅธ ๊ธฐ๊ธฐ์ ์ฐ๊ฒฐ
- ๋ค๋ฅธ ๊ธฐ๊ธฐ์ ๋ฐ์ดํฐ ์ฃผ๊ณ ๋ฐ๊ธฐ
- ์ฌ๋ฌ ์ฐ๊ฒฐ ๊ด๋ฆฌ
ํ์ด๋ง
- ๋ธ๋ฃจํฌ์ค ์ง์๊ธฐ๊ธฐ๊ฐ ์๋ก ํต์ ํ๊ธฐ ์ํด์๋ ํ์ด๋ง ํ๋ก์ธ์ค๋ฅผ ์ฌ์ฉํ์ฌ ํต์ ์ฑ๋์ ํ์ฑํด์ผํ๋ค.
- ํ์ด๋ง ๊ณผ์
- ๊ธฐ๊ธฐ1์ ์์ ์ ๊ฒ์ ๊ฐ๋ฅํ๋๋ก ์ค์ (discoverable)
- ๊ธฐ๊ธฐ2๋ discovering์ ํตํด ์ฃผ๋ณ์ ๊ฒ์๊ฐ๋ฅํ ๊ธฐ๊ธฐ๋ฅผ ํ์
- ๊ธฐ๊ธฐ2์์ ๊ธฐ๊ธฐ1์ ๋ฐ๊ฒฌํ๋ฉด ๊ธฐ๊ธฐ1๋ก ํ์ด๋ง ์์ฒญ์ ๋ณด๋ผ ์ ์์
- ๊ธฐ๊ธฐ1์์ ํด๋น์์ฒญ์ ์๋ฝํ๋ฉด ๋ ๊ธฐ๊ธฐ๋ ๋ณด์ํค๋ฅผ ๊ตํํ๊ณ ํด๋น ํค๋ฅผ ์บ์ํ๋ฉฐ bonding์ ์๋ฃํจ
- ์ธ์ ์ด ์๋ฃ๋๋ฉด ํ์ด๋ง ์์ฒญ์ ์์ํ ๊ธฐ๊ธฐ๊ฐ ๊ฒ์๊ฐ๋ฅํ ๊ธฐ๊ธฐ์ ์ฐ๊ฒฐํ ์ฑ๋์ ํด์
- ๊ทธ๋ฌ๋ ์ด๋ฏธ ํค๋ฅผ ๊ตํํ์ฌ ๋ ๊ธฐ๊ธฐ๋ ์ฐ๊ฒฐ๋ ์ํ๋ก ์ ์ง๋๋ฏ๋ก ์๋ก ๋ฒ์๋ด์์๊ณ , ๋ ๊ธฐ๊ธฐ ๋ชจ๋ ์ฐ๊ฒฐ์ ์ญ์ ํ์ง ์์ ๊ฒฝ์ฐ ๋ค์ ์ฐ๊ฒฐ ๊ฐ๋ฅ
๋ธ๋ฃจํฌ์ค ๊ด๋ จ๊ถํ
-
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์ ํตํด ๋ค๋ฅธ ๊ธฐ๊ธฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ตํํ๋ ์ฐ๊ฒฐ ์์
- ๋ธ๋ฃจํฌ์ค ์์ผ์ ํ์ฉํ๊ธฐ ์ํ ์ธํฐํ์ด์ค (TCP
BluetoothServerSocket
- ์ฐ๊ฒฐ์์ฒญ์ ๋ฐ์๋ค์ด๊ธฐ ์ํ ์๋ฒ์์ผ์ ๋ํ๋. (TCP์
ServerSocket
๊ณผ ๋น์ท) - ๋ ๋๋ฐ์ด์ค๊ฐ ์ฐ๊ฒฐ๋๊ธฐ ์ํด์ ๋ฐ๋์ ํ๋์ ๋๋ฐ์ด์ค๋ ์ด ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฒ์์ผ์ ์ด์ด์ผํจ.
- ์ฐ๊ฒฐ์์ฒญ์ด ๋ค์ด์ค๊ณ ํด๋น ์์ฒญ์ด ๋ฐ์๋ค์ฌ์ก๋ค๋ฉด ๋ฐ์ดํฐ ๊ตํ์ ์ํ
BluetoothSocket
์ ๋ฐํํจ.
- ์ฐ๊ฒฐ์์ฒญ์ ๋ฐ์๋ค์ด๊ธฐ ์ํ ์๋ฒ์์ผ์ ๋ํ๋. (TCP์
๋ธ๋ฃจํฌ์ค ์ฐ๊ฒฐ ๊ณผ์
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)์ด์์์ ํ์ฉ๊ฐ๋ฅ
-
manifest.xml์ companion device ์ค์
<uses-feature android:name="android.software.companion_device_setup"/>
-
Device Filter ๋ง๋ค๊ธฐ
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder() // ๋๋ฐ์ด์ค ์ด๋ฆ์ด ์๋ ํจํด๊ณผ ๋ง๋ ๋๋ฐ์ด์ค๋ง ํ์ํ๋๋ก ํํฐ๋ง .setNamePattern(Pattern.compile("My device")) //์๋น์ค UUID๊ฐ ์๋ ํจํด๊ณผ ์ผ์นํ๋ ๋๋ฐ์ด์ค๋ง ํ์ํ๋๋ก ํํฐ๋ง .addServiceUuid(ParcelUuid(UUID(0x123abcL, -1L)), null) .build()
-
AssociationRequest๋ง๋ค๊ธฐ + DeviceFilter์ถ๊ฐ
val pairingRequest: AssociationRequest = AssociationRequest.Builder() // deviceFilter ์ถ๊ฐ .addDeviceFilter(deviceFilter) // ํํฐ์ ๋ง๋ ๋๋ฐ์ด์ค ํ๋๋ฅผ ์ฐพ์ผ๋ฉด ํ์ ์ค๋จ .setSingleDevice(true) .build()
-
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. ๋ธ๋ฃจํฌ์ค ์ด๋ํฐ ์ฌ์ฉ
- ๊ฒ์ ๋นํ๋ ๊ธฐ๊ธฐ์์ 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) }
- ๊ธฐ๊ธฐ ๊ฒ์ ๋ฐ ๊ฒ์ ๊ฒฐ๊ณผ ํ์ธ
- 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) }
- bluetoothAdapter์
3. ๋ธ๋ฃจํฌ์ค ๊ธฐ๊ธฐ ์ฐ๊ฒฐ
- ๋ธ๋ฃจํฌ์ค ์ฐ๊ฒฐ์ ์๋ฒ, ํด๋ผ์ด์ธํธ ๋ฉ์ปค๋์ฆ์ด ์กด์ฌํ๋ค.
- ์ฐ๊ฒฐ๊ณผ์
- ๊ธฐ๊ธฐ A์์ ์๋ฒ์์ผ์ ์ด์ด ์ฐ๊ฒฐ ์์ฒญ์ ์์ ๋๊ธฐํ๋ค.
- ํด๋ผ์ด์ธํธ ๊ธฐ๊ธฐ B์์ ๋ธ๋ฃจํฌ์ค ์์ผ์ ํตํด ๊ธฐ๊ธฐA์ ์๋ฒ ์์ผ์ ์ฐ๊ฒฐ์ ์์ฒญํ๋ค.
- ๊ธฐ๊ธฐA์์ ํด๋น ์์ฒญ์ ๋ฐ์๋ค์ฌ RFCOMM์ฑ๋์ด ํ์ฑ๋๊ณ (์ถ๊ฐ ์ฐ๊ฒฐ์ด ์๋ค๋ฉด) ์๋ฒ ์์ผ์ ๋ซ๋๋ค.
- ๋ธ๋ฃจํฌ์ค ์์ผ์ ํ์ฉํ์ฌ ์๋ก ๊ณ์ ํต์ ํ๋ค.
์๋ฒ ์ฝ๋
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
}
์์
์ด๊ฒ ์ฝ๋๋ฉ์ธ๊ฐ์ ๋ธ๋ฃจํฌ์ค ์ฑํ ์ดํ๋ฆฌ์ผ์ด์ ์์ ๊ฐ ์์๋๋ฐ ์ด๊ฒ ์ข ์๋ ์ฝ๋๋ผ์ ์ค์ตํ ๊ฒธ ๋ค์ ๋ง๋ค์ด๋ดค๋ค.
์ด๋ฒ์ ๊ณต๋ถํ๋ฉด์ ๋ณด๋๊น ๊ณ ์น ๋ถ๋ถ์ด ์๋๊ฒ ๊ฐ์์ ์กฐ๋ง๊ฐ ์๊ฐ๋๋ฉด ์์ ํ ์์ ์ด๋ค.
์ง๊ธ์
- 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
๋๊ธ๋จ๊ธฐ๊ธฐ