참고
https://developer.android.com/develop/ui/views/layout/webapps/webview?hl=ko
WebView에서 웹 앱 빌드 | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. WebView에서 웹 앱 빌드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. WebView를 사용하여 웹 애플리케이
developer.android.com
BLE 사용 방식 순서
1. 권한 설정
2. BLE 기능 검사 및 Bluetooth 사용 요청
3. BLE Scan
4. BLE Connect with GATT + Data transfer(Write/Read)
<< 내용설명
- Central 역할의 휴대폰 앱 구현
- 최상단에 BLE Tutorial App 유지
- SCAN START 버튼 클릭시 Scan 된 블루투스 기기들 전부 UI 표현
- Close 버튼 누를시 스캔창 닫힘
- 라디오 버튼 누르고 Connect 시 커넥트 On
- Text 라인에 타이핑 후
- Send Data 버튼으로 전송
- Recieve Data 버튼으로 읽기
1. 권한 설정
2. BLE 기능 검사 및 Bluetooth 사용 요청
3. BLE Scan
4. BLE Connect with GATT + Data transfer(Write/Read)
AndroidManifest.xml
하단의 코드 추가
<!-- 이전 기기에서 레거시 Bluetooth 권한 요청 -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />-->
<!-- <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 10(API 레벨 29) 또는 Android 11 인 경우, 백그라운드에서 Bluetooth 을 하려면 해당 위치 권한이 필요함.-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Central 역할 ( Server )
앱이 Bluetooth 기기를 검색하는 경우에만 필요합니다.
앱이 Bluetooth 검색 결과를 물리적 위치 정보를 유도하는 데 사용하지 않는 경우,
<a href="#assert-never-for-location">앱이 물리적 위치를 유도하지 않는다고 강하게 주장할 수 있습니다</a>. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- <!– BLE Peripheral 역할 ( Client )-->
<!-- 앱이 Bluetooth 기기에서 검색 가능하도록 만드는 경우에만 필요합니다. –>-->
<!-- <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />-->
<!-- BLE Central 및 Peripheral ( 이미 페어링된 기기와 통신 )
앱이 이미 페어링된 Bluetooth 기기와 통신하는 경우에만 필요합니다. -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- BLE Central 역할 다른 git Code
BLE 스캔을 위해 위치 권한이 필요합니다. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- <!– 앱에 블루투스 클래식이 필요하다고 표시하는 방법–>-->
<!-- <uses-feature android:name="android.hardware.bluetooth" android:required="true"/>-->
<!-- 앱에 저전력 블루투스를 사용하는 경우-->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
MainActivity.kt
하단의 코드 추가
import android.content.pm.PackageManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
// // 블루투스 클래식 기능이 핸드폰에 있는지 확인
// val bluetoothAvailable = packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
// 1. 기기의 BLE 지원 여부 확인
val bluetoothLEAvailable = packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)
if (!bluetoothLEAvailable){
Log.e(" - MainActivity", "기기의 BLE 지원 여부 확인 : $bluetoothLEAvailable")
//TODO: 현재기기 사용불가 에러 핸들링 표시 필요
}
}
1. 권한 설정
2. BLE 기능 검사 및 Bluetooth 사용 요청
3. BLE Scan
4. BLE Connect with GATT + Data transfer(Write/Read)
activity.xml
하단의 코드 추가
-- 아무것도 없는 상태 빈 화면
MainActivity.kt
하단의 코드 추가
코드설명 :
현재 공식문서에 있는 함수는 지원종료 예정 함수이므로
ActivityResultLauncher, ActivityResultContracts 라이브러리 를 이용함
Launcher 로 블루투스 활성화 요청 ( Deny / Allow )
Contracts 로 요청에 대한 응답을 받을 Callback 함수 등록
package com.example.rssreader
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Intent
import android.util.Log
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import android.app.Activity
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
class MainActivity : ComponentActivity() {
// 1. ActivityResultLauncher를 클래스의 멤버 변수로 선언합니다.
private lateinit var enableBluetoothLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
// 2. 기기의 BLE 및 Bluetooth Classic 기능 지원 여부 확인
val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.getAdapter()
if (bluetoothAdapter == null) {
// Device doesn't support Bluetooth
Log.e(" - MainActivity", "기기의 BLE 및 Bluetooth Classic 기능 지원 여부 확인 : $bluetoothAdapter")
}
// 3_1. registerForActivityResult 함수로 결과를 처리할 콜백을 등록
enableBluetoothLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
::handleBluetoothIntentResult // 콜백 함수 참조
)
// 3_2. Bluetooth 가 비활성화된 경우 활성화를 요청
// val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter?.isEnabled == false) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
enableBluetoothLauncher.launch(enableBtIntent)
Log.i(" - MainActivity", "블루투스 활성화 요청: $enableBtIntent")
}
}
// 3_1 블루투스 활성화 콜백함수
private fun handleBluetoothIntentResult(result: ActivityResult) {
if (result.resultCode == Activity.RESULT_OK) {
// Bluetooth가 활성화되었습니다.
Log.i(" - MainActivity", "블루투스가 활성화되었습니다.")
} else {
// Bluetooth 활성화가 취소되었습니다.
Log.i(" - MainActivity", "블루투스 활성화가 취소되었습니다.")
}
}
}
1. 권한 설정
2. BLE 기능 검사 및 Bluetooth 사용 요청
3. BLE Scan
4. BLE Connect with GATT + Data transfer(Write/Read)
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF">
<!-- Scan Start 버튼 -->
<Button
android:id="@+id/btn_scan_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="50dp"
android:text="Scan Start"
android:background="@android:color/holo_blue_light"
android:textColor="@android:color/white"
android:padding="10dp"
android:textSize="16sp"
android:visibility="visible"/>
</RelativeLayout>
item_scan_list.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<RadioButton
android:id="@+id/radio_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/device_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Device Name"
android:textSize="16sp" />
</LinearLayout>
popup_scan_list.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000"
android:visibility="gone">
<!-- 팝업 컨테이너 -->
<LinearLayout
android:id="@+id/popup_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="16dp"
android:elevation="4dp">
<!-- RecyclerView: Scan List -->
<androidx.recyclerview.widget.RecyclerView
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:id="@+id/recycler_scan_list"
android:layout_width="match_parent"
android:layout_height="300dp"
android:scrollbars="vertical" />
<!-- Connect / Close 버튼 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="16dp">
<Button
android:id="@+id/btn_connect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect"
android:background="@android:color/holo_green_light"
android:textColor="@android:color/white"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Close"
android:background="@android:color/holo_red_light"
android:textColor="@android:color/white" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
MainActivity.kt
package com.example.simplebleapp
import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.example.simplebleapp.bleModules.ScanListAdapter
class MainActivity : AppCompatActivity() {
private val bluetoothAdapter: BluetoothAdapter? by lazy { BluetoothAdapter.getDefaultAdapter() }
private var scanListAdapter: ScanListAdapter = ScanListAdapter()
private var isPopupVisible = false
// Stops scanning after 10 seconds.
private val SCAN_PERIOD: Long = 10000
private val MAIN_LOG_TAG = " - MainActivity "
// View 변수 선언
private lateinit var btnScanStart: Button
private lateinit var btnConnect: Button
private lateinit var btnClose: Button
private lateinit var popupContainer: LinearLayout
private lateinit var recyclerScanList: RecyclerView
private lateinit var popupView: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// activity_main.xml의 View 초기화
btnScanStart = findViewById(R.id.btn_scan_start)
// activity_main.xml의 루트 레이아웃 가져오기
val rootLayout =
findViewById<RelativeLayout>(R.id.root_layout) // activity_main.xml의 루트 레이아웃 ID
// popup_scan_list.xml을 inflate
popupView = LayoutInflater.from(this)
.inflate(
R.layout.popup_scan_list,
rootLayout,
false
)
// popup_scan_list.xml 내부 View 초기
btnConnect = popupView.findViewById(R.id.btn_connect)
btnClose = popupView.findViewById(R.id.btn_close)
popupContainer = popupView.findViewById(R.id.popup_container)
recyclerScanList = popupView.findViewById(R.id.recycler_scan_list)
// RecyclerView 초기화
scanListAdapter.setupRecyclerView(recyclerScanList, this@MainActivity)
// 팝업을 루트 레이아웃에 추가
rootLayout.addView(popupView)
// Scan Start 버튼 클릭 리스너
btnScanStart.setOnClickListener {
if (checkPermissions()) {
Log.i(MAIN_LOG_TAG, " SCANING --")
startBleScan()
} else {
Log.i(MAIN_LOG_TAG, " Pemission Requseting --")
requestPermissions()
}
}
// Close 버튼 클릭 리스너
btnClose.setOnClickListener {
stopBleScan()
}
// Connect 버튼 클릭 리스너
btnConnect.setOnClickListener {
val selectedDevice = scanListAdapter.getSelectedDevice()
if (selectedDevice != null) {
Toast.makeText(this, "Selected: ${selectedDevice.name}", Toast.LENGTH_SHORT).show()
// BLE GATT 연결 로직은 이후 구현
} else {
Toast.makeText(this, "No device selected", Toast.LENGTH_SHORT).show()
}
}
}
override fun onDestroy() {
super.onDestroy()
Log.i(MAIN_LOG_TAG, "onDestroy")
Toast.makeText(this, "onDestroy", Toast.LENGTH_SHORT).show()
stopBleScan() // 스캔 중지
isPopupVisible = popupView.visibility == View.VISIBLE // 팝업 상태 저장
}
override fun onPause() {
super.onPause()
Log.i(MAIN_LOG_TAG, "onPause")
Toast.makeText(this, "onPause", Toast.LENGTH_SHORT).show()
stopBleScan() // 스캔 중지
isPopupVisible = popupView.visibility == View.VISIBLE // 팝업 상태 저장
}
override fun onResume() { //TODO : 앱 켜지면 자동으로 스캔해서 연결까지 동작
super.onResume()
Log.i(MAIN_LOG_TAG, "onResume")
Toast.makeText(this, "onResume", Toast.LENGTH_SHORT).show()
// if (isPopupVisible) { // 팝업 상태 복구
// popupView.visibility = View.VISIBLE
// popupContainer.visibility = View.VISIBLE
// btnScanStart.visibility = View.GONE
// } else if (scanResults.isEmpty()) { // 스캔 결과가 없으면 스캔 재개
// startBleScan()
// }
}
private fun startBleScan() {
Log.i(MAIN_LOG_TAG, "popupContainer : ${popupContainer} ")
Log.i(MAIN_LOG_TAG, "bluetoothAdapter : ${bluetoothAdapter}")
Log.i(MAIN_LOG_TAG, "bluetoothAdapter.bluetoothLeScanner : ${bluetoothAdapter?.bluetoothLeScanner}")
bluetoothAdapter?.bluetoothLeScanner?.startScan(scanCallback)
btnScanStart.visibility = View.GONE
popupView.visibility = View.VISIBLE
popupContainer.visibility = View.VISIBLE
// 10초 후 스캔 중지
Log.i(MAIN_LOG_TAG, "스캔 타임아웃 제한시간 : ${SCAN_PERIOD/1000}초 ")
popupContainer.postDelayed({
stopBleScan()
Toast.makeText(this, "Scan stopped after 10 seconds", Toast.LENGTH_SHORT).show()
}, SCAN_PERIOD)
}
private fun stopBleScan() {
popupView.visibility = View.GONE // 팝업 숨김
popupContainer.visibility = View.GONE // 팝업 컨테이너 숨김
btnScanStart.visibility = View.VISIBLE // Scan Start 버튼 활성화
bluetoothAdapter?.bluetoothLeScanner?.apply {
try {
bluetoothAdapter?.bluetoothLeScanner?.stopScan(scanCallback)
Log.e(MAIN_LOG_TAG, "블루투스 스캔 정지 ")
} catch (e: Exception) {
Log.e(MAIN_LOG_TAG, "Failed to stop BLE scan: ${e.message}")
}
}
// apply 를 안쓰는 경우
// val scanner = bluetoothAdapter?.bluetoothLeScanner
// if (scanner != null ) {
// try {
// scanner.stopScan(scanCallback)
// Log.e(" - MainActivity", "블루투스 스캔 정지 ")
// } catch (e: Exception) {
// Log.e("MainActivity", "Failed to stop BLE scan: ${e.message}")
// }
// }
}
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
if (result.scanRecord?.deviceName == null){
// DeviceName 이 Null 인 경우, 스캔리스트에 추가 X
return
}else{
scanListAdapter.addDeviceToAdapt(device)
}
}
override fun onScanFailed(errorCode: Int) {
Log.e(MAIN_LOG_TAG, "onScanFailed called with errorCode: $errorCode")
when (errorCode) {
ScanCallback.SCAN_FAILED_ALREADY_STARTED -> Log.e(MAIN_LOG_TAG, "Scan already started")
ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> Log.e(MAIN_LOG_TAG, "App registration failed")
ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> Log.e(MAIN_LOG_TAG, "Internal error")
ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> Log.e(MAIN_LOG_TAG, "Feature unsupported")
}
Toast.makeText(this@MainActivity, "Scan failed: $errorCode", Toast.LENGTH_SHORT).show()
}
}
private fun checkPermissions(): Boolean {
val permissions = listOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_FINE_LOCATION
)// TODO: 권한 추가 관련 동작 필요함
return permissions.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
}
private fun requestPermissions() {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_FINE_LOCATION
),
1
)// TODO: 권한 추가 관련 동작 필요함
}
}
AndroidManifest.xml
<manifest ~~~ 태그>
여기에 하단의 코드 삽입
<application ~~~~~ >
</application>
</manifest>
<!-- 이전 기기에서 레거시 Bluetooth 권한 요청 -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />-->
<!-- <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 10(API 레벨 29) 또는 Android 11 인 경우, 백그라운드에서 Bluetooth 을 하려면 해당 위치 권한이 필요함.-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Central 역할 ( Server )
앱이 Bluetooth 기기를 검색하는 경우에만 필요합니다.
앱이 Bluetooth 검색 결과를 물리적 위치 정보를 유도하는 데 사용하지 않는 경우,
<a href="#assert-never-for-location">앱이 물리적 위치를 유도하지 않는다고 강하게 주장할 수 있습니다</a>. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- <!– BLE Peripheral 역할 ( Client )-->
<!-- 앱이 Bluetooth 기기에서 검색 가능하도록 만드는 경우에만 필요합니다. –>-->
<!-- <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />-->
<!-- BLE Central 및 Peripheral ( 이미 페어링된 기기와 통신 )
앱이 이미 페어링된 Bluetooth 기기와 통신하는 경우에만 필요합니다. -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- BLE Central 역할 다른 git Code
BLE 스캔을 위해 위치 권한이 필요합니다. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- <!– 앱에 블루투스 클래식이 필요하다고 표시하는 방법–>-->
<!-- <uses-feature android:name="android.hardware.bluetooth" android:required="true"/>-->
<!-- 앱에 저전력 블루투스를 사용하는 경우-->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
1. 권한 설정
2. BLE 기능 검사 및 Bluetooth 사용 요청
3. BLE Scan
4. BLE Connect with GATT + Data transfer(Write/Read)
AndroidManifest.xml
위에 참고
popup_scan_list.xml
하단의 코드 추가
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000"
android:visibility="gone">
<!-- 팝업 컨테이너 -->
<LinearLayout
android:id="@+id/popup_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="16dp"
android:elevation="4dp">
<!-- RecyclerView: Scan List -->
<androidx.recyclerview.widget.RecyclerView
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:id="@+id/recycler_scan_list"
android:layout_width="match_parent"
android:layout_height="300dp"
android:scrollbars="vertical" />
<!-- Connect / Close 버튼 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="16dp">
<Button
android:id="@+id/btn_connect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect"
android:background="@android:color/holo_green_light"
android:textColor="@android:color/white"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Close"
android:background="@android:color/holo_red_light"
android:textColor="@android:color/white" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
item_scan_list.xml
하단의 코드 추가
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<RadioButton
android:id="@+id/radio_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
<TextView
android:id="@+id/device_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:text="Device Name"
android:textSize="16sp" />
</LinearLayout>
activity_main.xml
하단의 코드 추가
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF">
<!-- Scan Start 버튼 -->
<Button
android:id="@+id/btn_scan_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="50dp"
android:text="Scan Start"
android:background="@android:color/holo_blue_light"
android:textColor="@android:color/white"
android:padding="10dp"
android:textSize="16sp"
android:visibility="visible"/>
<!-- 데이터 입력창 -->
<EditText
android:id="@+id/et_input_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter data to send"
android:layout_below="@id/btn_scan_start"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" />
<!-- 데이터 전송 버튼 -->
<Button
android:id="@+id/btn_send_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send Data"
android:layout_below="@id/et_input_data"
android:layout_marginTop="16dp"
android:layout_centerHorizontal="true" />
<!-- 데이터 수신 버튼 -->
<Button
android:id="@+id/btn_receive_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Receive Data"
android:layout_below="@id/btn_send_data"
android:layout_marginTop="16dp"
android:layout_centerHorizontal="true" />
</RelativeLayout>
<!--<?xml version="1.0" encoding="utf-8"?>-->
<!--<androidx.constraintlayout.widget.ConstraintLayout -->
<!-- xmlns:android="http://schemas.android.com/apk/res/android"-->
<!-- xmlns:app="http://schemas.android.com/apk/res-auto"-->
<!-- xmlns:tools="http://schemas.android.com/tools"-->
<!-- android:id="@+id/main"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- tools:context=".MainActivity">-->
<!-- <TextView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Hello World!"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent" />-->
<!--</androidx.constraintlayout.widget.ConstraintLayout>-->
BleController.kt
하단의 코드 추가
// Operator Pack
package com.example.simplebleapp.bleModules
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import android.Manifest
import android.app.Activity
import android.os.Handler
import android.os.Build
// UI Pack
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import android.widget.LinearLayout
import android.widget.Toast
import android.content.Context
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
// BLE Pack
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import androidx.annotation.RequiresPermission
// Util Pack
import java.util.UUID
// Custom Package
class BleController(private val applicationContext: Context) {
// BLE 관련 멤버 변수
private lateinit var bluetoothManager: BluetoothManager
private lateinit var bluetoothAdapter: BluetoothAdapter
private lateinit var bluetoothLeScanner: BluetoothLeScanner
private var bluetoothGatt: BluetoothGatt? = null
// GATT 특성
private var writeCharacteristic: BluetoothGattCharacteristic? = null
private var readCharacteristic: BluetoothGattCharacteristic? = null
// UUID는 GATT 서비스와 특성을 식별하는 데 사용됩니다.
private val SERVICE_UUID = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb") // 예: Heart Rate Service
private val WRITE_CHARACTERISTIC_UUID = UUID.fromString("00002a39-0000-1000-8000-00805f9b34fb") // 예: Write Characteristic
private val READ_CHARACTERISTIC_UUID = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb") // 예: Read Characteristic
// ActivityResultLauncher를 클래스의 멤버 변수로 선언
private lateinit var enableBluetoothLauncher: ActivityResultLauncher<Intent>
// 스캔 제한 시간
private val SCAN_PERIOD: Long = 10000
private val BLECONT_LOG_TAG = " - BleController"
// 권한 상태를 저장하는 Map
private val permissionStatus = mutableMapOf(
"블루투스 활성화" to false,
"블루투스 스캔 권한" to false,
"위치 권한" to false,
"블루투스 연결 권한" to false
)
/**
* BLE 모듈 초기화
*/
fun setBleModules() {
// getSystemService는 Context가 완전히 초기화된 후에 호출되어야 함
bluetoothManager = applicationContext.getSystemService(BluetoothManager::class.java)
// bluetoothAdapter = bluetoothManager.getAdapter() // 이전 버전
bluetoothAdapter = bluetoothManager.adapter
// bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner() // 이전 버전
bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
}
/**
* BLE 권한 요청 런처 설정
*/
fun setBlePermissionLauncher(launcher: ActivityResultLauncher<Intent>) {
enableBluetoothLauncher = launcher
}
/**
* BLE 지원 여부 확인
*/
fun checkBleOperator() {
val bluetoothLEAvailable =
applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)
if (!bluetoothLEAvailable) {
Log.e(BLECONT_LOG_TAG, "기기의 BLE 지원 여부 확인 : $bluetoothLEAvailable")
// TODO: BLE 미지원 기기에 대한 에러 처리 필요
}
// if (bluetoothAdapter == null) {
// Log.e(BLECONT_LOG_TAG, "기기의 BLE 및 Bluetooth Classic 기능 지원 여부 확인 : $bluetoothAdapter")
// // TODO: Bluetooth 미지원 기기에 대한 에러 처리 필요
// }
}
/**
* BLE 권한 요청
*/
fun requestBlePermission(activity: Activity): MutableMap<String, Boolean>{
// val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
// 1. 블루투스 활성화 요청 <System Setting>
if (!bluetoothAdapter.isEnabled) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
enableBluetoothLauncher.launch(enableBtIntent) // registerForActivityResult로 처리
permissionStatus["블루투스 활성화"] = false // 활성화 여부는 런처 결과에서 처리
} else {
permissionStatus["블루투스 활성화"] = true
}
// 2. Android 12(API 31) 이상에서 BLE 스캔 권한 요청 <Permission Request>
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.BLUETOOTH_SCAN
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.BLUETOOTH_SCAN),
101 // 요청 코드
)
permissionStatus["블루투스 스캔 권한"] = false
} else permissionStatus["블루투스 스캔 권한"] = true
} else permissionStatus["블루투스 스캔 권한"] = true
// 3. 위치 정보 권한 요청 <Permission Request>
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
102 // 요청 코드
)
permissionStatus["위치 권한"] = false
} else {
permissionStatus["위치 권한"] = true
}
// 4. 블루투스 연결 권한 요청 <Permission Request>
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
103 // 요청 코드
)
permissionStatus["블루투스 연결 권한"] = false
} else permissionStatus["블루투스 연결 권한"] = true
} else permissionStatus["블루투스 연결 권한"] = true
return permissionStatus
}
/**
* BLE 스캔 시작
*/
fun <T> startBleScan(scanCallback: ScanCallback, popupContainer: T) {
bluetoothLeScanner.startScan(scanCallback)
// 10초 후 스캔 중지
Log.i(BLECONT_LOG_TAG, "스캔 타임아웃 제한시간 : ${SCAN_PERIOD / 1000}초 ")
when (popupContainer) {
is LinearLayout -> { // 특정 팝업창에 Text로 UI 표현하는 경우
popupContainer.postDelayed({
stopBleScan(scanCallback)
}, SCAN_PERIOD)
}
is Handler -> { // 일반 화면에 Text로 UI 표현하는 경우
popupContainer.postDelayed({ // SCAN_PERIOD 시간후에 발동되는 지연 함수
Log.w(BLECONT_LOG_TAG, "--스캔 타임아웃-- ")
bluetoothLeScanner.stopScan(scanCallback)
}, SCAN_PERIOD)
}
}
}
/**
* BLE 스캔 중지
*/
fun stopBleScan(scanCallback: ScanCallback) {
try {
bluetoothLeScanner.stopScan(scanCallback)
Log.e(BLECONT_LOG_TAG, "블루투스 스캔 정지")
} catch (e: Exception) {
Log.e(BLECONT_LOG_TAG, "Failed to stop BLE scan: ${e.message}")
}
}
// /**
// * BLE 장치 연결
// */
// @RequiresPermission(value = Manifest.permission.BLUETOOTH_CONNECT)
// fun connectToDevice(device: BluetoothDevice, onConnectionStateChange: (Boolean) -> Unit) {
// bluetoothGatt = device.connectGatt(applicationContext, false, object : BluetoothGattCallback() {
// override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
// super.onConnectionStateChange(gatt, status, newState)
// if (newState == BluetoothProfile.STATE_CONNECTED) {
// Log.d(BLECONT_LOG_TAG, "GATT 서버에 연결되었습니다.")
// onConnectionStateChange(true)
//
// // Android 12(API 31) 이상에서만 BLUETOOTH_CONNECT 권한 확인
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// if (ActivityCompat.checkSelfPermission(
// applicationContext,
// Manifest.permission.BLUETOOTH_CONNECT
// ) != PackageManager.PERMISSION_GRANTED
// ) {
// Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없습니다. discoverServices()를 호출할 수 없습니다.")
// Toast.makeText(applicationContext, "BLUETOOTH_CONNECT 권한이 없습니다.", Toast.LENGTH_SHORT).show()
// // 권한이 없으면 discoverServices()를 호출하지 않고 종료
// return
// }
// }
//
// // GATT 서비스 검색 시작
// Log.i(BLECONT_LOG_TAG,"gatt.discoverServices 시작")
// gatt.discoverServices()
// Log.i(BLECONT_LOG_TAG,"gatt.discoverServices 시작")
//
// } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
// Log.d(BLECONT_LOG_TAG, "Disconnected from GATT server.")
// onConnectionStateChange(false)
// }
// }
//
// override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
// super.onServicesDiscovered(gatt, status)
// if (status == BluetoothGatt.GATT_SUCCESS) {
// Log.d(BLECONT_LOG_TAG, "Services discovered: ${gatt.services}")
// } else {
// Log.w(BLECONT_LOG_TAG, "onServicesDiscovered received: $status")
// }
// }
// })
// }
/**
* BLE 장치 연결
*/
@RequiresPermission(value = Manifest.permission.BLUETOOTH_CONNECT)
fun connectToDevice(device: BluetoothDevice, onConnectionStateChange: (Boolean) -> Unit) {
bluetoothGatt = device.connectGatt(applicationContext, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d(BLECONT_LOG_TAG, "GATT 서버에 연결되었습니다.")
onConnectionStateChange(true)
// 권한 확인 및 GATT 서비스 검색
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(
applicationContext,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
try {
gatt.discoverServices() // GATT 서비스 검색
} catch (e: SecurityException) {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없어 discoverServices()를 호출할 수 없습니다.")
}
} else {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없습니다.")
}
} else {
try {
gatt.discoverServices() // GATT 서비스 검색
} catch (e: SecurityException) {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없어 discoverServices()를 호출할 수 없습니다.")
}
}
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(BLECONT_LOG_TAG, "GATT 서버 연결이 해제되었습니다.")
onConnectionStateChange(false)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d(BLECONT_LOG_TAG, "GATT 서비스 검색 성공")
// 검색된 모든 서비스와 특성을 로그로 출력
for (service in gatt.services) {
Log.d(BLECONT_LOG_TAG, "서비스 UUID: ${service.uuid}")
for (characteristic in service.characteristics) {
Log.d(BLECONT_LOG_TAG, " 특성 UUID: ${characteristic.uuid}")
}
}
// 특정 서비스와 특성 찾기
val service = gatt.getService(SERVICE_UUID)
if (service !=null) {
Log.d(BLECONT_LOG_TAG, "특정 서비스 발견: $SERVICE_UUID")
// 쓰기 특성 초기화
writeCharacteristic = service.getCharacteristic(WRITE_CHARACTERISTIC_UUID)
if (writeCharacteristic !=null) {
Log.d(BLECONT_LOG_TAG, "쓰기 특성 발견: $WRITE_CHARACTERISTIC_UUID")
} else {
Log.e(BLECONT_LOG_TAG, "쓰기 특성을 찾을 수 없습니다.")
Toast.makeText(applicationContext, "쓰기 특성을 찾을 수 없습니다.", Toast.LENGTH_SHORT).show()
}
// 읽기 특성 초기화
readCharacteristic = service.getCharacteristic(READ_CHARACTERISTIC_UUID)
if (readCharacteristic !=null) {
Log.d(BLECONT_LOG_TAG, "읽기 특성 발견: $READ_CHARACTERISTIC_UUID")
// 읽기 특성에 대해 알림(Notification) 활성화
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(
applicationContext,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
try {
gatt.setCharacteristicNotification(readCharacteristic, true)
Log.d(BLECONT_LOG_TAG, "읽기 특성에 대한 알림 활성화")
} catch (e: SecurityException) {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없어 알림을 활성화할 수 없습니다.")
Toast.makeText(applicationContext, "BLUETOOTH_CONNECT 권한이 없어 알림을 활성화할 수 없습니다.", Toast.LENGTH_SHORT).show()
}
} else {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없습니다.")
Toast.makeText(applicationContext, "BLUETOOTH_CONNECT 권한이 없습니다.", Toast.LENGTH_SHORT).show()
}
} else {
try {
gatt.setCharacteristicNotification(readCharacteristic, true)
Log.d(BLECONT_LOG_TAG, "읽기 특성에 대한 알림 활성화")
} catch (e: SecurityException) {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없어 알림을 활성화할 수 없습니다.")
Toast.makeText(applicationContext, "BLUETOOTH_CONNECT 권한이 없어 알림을 활성화할 수 없습니다.", Toast.LENGTH_SHORT).show()
}
}
} else {
Log.e(BLECONT_LOG_TAG, "읽기 특성을 찾을 수 없습니다.")
Toast.makeText(applicationContext, "읽기 특성을 찾을 수 없습니다.", Toast.LENGTH_SHORT).show()
}
} else {
Log.e(BLECONT_LOG_TAG, "특정 서비스를 찾을 수 없습니다: $SERVICE_UUID")
Toast.makeText(applicationContext, "특정 서비스를 찾을 수 없습니다: $SERVICE_UUID", Toast.LENGTH_SHORT).show()
}
} else {
Log.e(BLECONT_LOG_TAG, "GATT 서비스 검색 실패: $status")
Toast.makeText(applicationContext, "GATT 서비스 검색 실패: $status", Toast.LENGTH_SHORT).show()
}
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
super.onCharacteristicChanged(gatt, characteristic)
if (characteristic.uuid == READ_CHARACTERISTIC_UUID) {
val receivedData = characteristic.value // 수신된 데이터
val receivedString = String(receivedData) // ByteArray를 문자열로 변환
Log.i(BLECONT_LOG_TAG, "수신된 데이터: $receivedString")
// Toast로 수신된 데이터 표시
Toast.makeText(applicationContext, "Received Data: $receivedString", Toast.LENGTH_SHORT).show()
}
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
super.onCharacteristicWrite(gatt, characteristic, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(BLECONT_LOG_TAG, "데이터 전송 성공: ${String(characteristic.value)}")
} else {
Log.e(BLECONT_LOG_TAG, "데이터 전송 실패: $status")
}
}
})
}
/**
* 데이터 쓰기
*/
fun writeData(data: ByteArray) {
if (writeCharacteristic != null) {
writeCharacteristic?.value = data
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(
applicationContext,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
try {
bluetoothGatt?.writeCharacteristic(writeCharacteristic)
} catch (e: SecurityException) {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없어 데이터를 전송할 수 없습니다.")
}
} else {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없습니다.")
}
} else {
try {
bluetoothGatt?.writeCharacteristic(writeCharacteristic)
} catch (e: SecurityException) {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없어 데이터를 전송할 수 없습니다.")
}
}
} else {
Log.e(BLECONT_LOG_TAG, "쓰기 특성이 초기화되지 않았습니다.")
}
}
/**
* 데이터 읽기
*/
fun readData() {
if (readCharacteristic != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(
applicationContext,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
try {
bluetoothGatt?.readCharacteristic(readCharacteristic)
} catch (e: SecurityException) {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없어 데이터를 읽을 수 없습니다.")
}
} else {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없습니다.")
}
} else {
try {
bluetoothGatt?.readCharacteristic(readCharacteristic)
} catch (e: SecurityException) {
Log.e(BLECONT_LOG_TAG, "BLUETOOTH_CONNECT 권한이 없어 데이터를 읽을 수 없습니다.")
}
}
} else {
Log.e(BLECONT_LOG_TAG, "읽기 특성이 초기화되지 않았습니다.")
}
}
/**
* BLE 연결 해제
*/
fun disconnect() {
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
}
// Callback or 비슷한 함수 들
/**
* Bluetooth 활성화 요청 결과 처리
*/
fun handleBluetoothIntentResult(result: ActivityResult) {
// 특정 작업(예: 권한 요청, 다른 Activity 호출 등)의 결과를 처리할 Callback 함수
if (result.resultCode == Activity.RESULT_OK) {
Log.i(BLECONT_LOG_TAG, "블루투스가 활성화되었습니다.")
permissionStatus["블루투스 권한"] = true
} else {
Log.i(BLECONT_LOG_TAG, "블루투스 활성화가 취소되었습니다.")
permissionStatus["블루투스 권한"] = false
}
}
}
ScanListAdapter.kt
하단의 코드 추가
package com.example.simplebleapp.bleModules
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.simplebleapp.R
class ScanListAdapter() :
RecyclerView.Adapter<ScanListAdapter.ScanViewHolder>() {
private val devices = mutableListOf<BluetoothDevice>() // BLE 장치 목록을 저장하는 리스트
private var selectedPosition = -1 // 선택된 항목의 인덱스를 저장하는 변수
/* RecyclerView의 ViewHolder를 생성하는 메서드 */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScanViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_scan_list, parent, false) // item_scan_list 레이아웃을 inflate
return ScanViewHolder(view) // ViewHolder 반환
}
/* RecyclerView의 각 항목에 데이터를 바인딩하는 메서드 */
override fun onBindViewHolder(holder: ScanViewHolder, position: Int) {
val device = devices[position] // 현재 위치의 BLE 장치 정보 가져오기
holder.deviceName.text = device.name ?: "Unknown Device" // 장치 이름 설정 (없으면 "Unknown Device")
holder.radioButton.isChecked = position == selectedPosition // 라디오 버튼 선택 상태 설정
// 라디오 버튼 클릭 리스너
holder.radioButton.setOnClickListener {
// 선택된 항목의 인덱스를 업데이트
val previousPosition = selectedPosition
selectedPosition = holder.adapterPosition
// 이전 선택 항목과 현재 선택 항목을 갱신
notifyItemChanged(previousPosition) // 이전 선택 항목 갱신
notifyItemChanged(selectedPosition) // 현재 선택 항목 갱신
}
}
//
// holder.itemView.setOnClickListener {
// selectedPosition = holder.adapterPosition
// notifyDataSetChanged()
// }
/* RecyclerView의 항목 개수를 반환하는 메서드 */
override fun getItemCount(): Int = devices.size
/* 선택된 BLE 장치를 반환하는 메서드 */
fun getSelectedDevice(): BluetoothDevice? {
return if (selectedPosition != -1) devices[selectedPosition] else null// 선택된 장치가 없으면 반환
}
/* RecyclerView의 각 항목을 관리하는 ViewHolder 클래스 */
class ScanViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val deviceName: TextView = itemView.findViewById(R.id.device_name) // 장치 이름을 표시하는 TextView
val radioButton: RadioButton = itemView.findViewById(R.id.radio_button) // 선택 상태를 표시하는 RadioButton
}
/* RecyclerView를 초기화하는 메서드 */
fun setupRecyclerView(
recyclerScanList: RecyclerView,
mainActivityContext: Context
) {
// RecyclerView의 레이아웃 매니저를 설정 (세로 방향 리스트 형태)
recyclerScanList.layoutManager = LinearLayoutManager(mainActivityContext)
// recyclerScanList 자체에서 데이터를 직접 관리 X
// 그래서 데이터를 관리하고 화면에 표시하는 역할의 어댑터를 직접 넣어줘야함
recyclerScanList.adapter = this
}
/* BLE 장치를 RecyclerView에 추가하는 메서드 */
fun addDeviceToAdapt(deviceInfo: BluetoothDevice) {
if (!devices.contains(deviceInfo)) { // 중복된 MAC 주소의 장치는 추가하지 않음
devices.add(deviceInfo) // 새로운 BLE 장치 추가
this.notifyItemInserted(devices.size - 1) // 새로 추가된 항목만 RecyclerView에 업데이트
}
}
/* BLE 장치 목록을 초기화하는 메서드 */
fun clearDevices() {
devices.clear() // BLE 장치 리스트 초기화
selectedPosition = -1 // 선택된 항목 초기화
notifyDataSetChanged() // RecyclerView 갱신
}
}
MainActivity.kt
하단의 코드 추가
package com.example.simplebleapp
// Operator Pack
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
// UI Pack
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
// BLE Pack
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Intent
// Util Pack
import android.util.Log
import android.widget.EditText
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
// Custom Package
import com.example.simplebleapp.bleModules.ScanListAdapter
import com.example.simplebleapp.bleModules.BleController
class MainActivity : AppCompatActivity() {
// 1. ActivityResultLauncher를 클래스의 멤버 변수로 선언합니다.
private lateinit var enableBluetoothLauncher: ActivityResultLauncher<Intent>
private val bleController = BleController(this) // MainActivity는 Context를 상속받음
// private val handler = Handler()
private var scanListAdapter: ScanListAdapter = ScanListAdapter()
private var isPopupVisible = false
// Stops scanning after 10 seconds.
private val SCAN_PERIOD: Long = 10000
private val MAIN_LOG_TAG = " - MainActivity "
// View 변수 선언
private lateinit var btnScanStart: Button
private lateinit var btnConnect: Button
private lateinit var btnClose: Button
private lateinit var popupContainer: LinearLayout
private lateinit var recyclerScanList: RecyclerView
private lateinit var popupView: View
// Data Send, receive Button 및 Text 입력창
private lateinit var etInputData: EditText
private lateinit var btnSendData: Button
private lateinit var btnReceiveData: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// BLE 초기화 완료 -----------------------------------------------------------------------------------
// 1. BluetoothManager 및 BluetoothAdapter 초기화
bleController.setBleModules()
// 2_1. 권한요청 Launcher 등록
// registerForActivityResult 설명 >>
// Activity나 Fragment의 생명주기에 맞춰 실행되는 결과 처리 메커니즘을 제공하는 함수
// 특정 작업(예: 권한 요청, 다른 Activity 호출 등)의 결과를 비동기적으로 처리하기 위해 사용됨
enableBluetoothLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
{ result: ActivityResult ->
// 결과를 bleController로 전달
bleController.handleBluetoothIntentResult(result)
}
)
// 2_2. Launcher 객체 BleController 에 전달
bleController.setBlePermissionLauncher(enableBluetoothLauncher)
// 3. BLE 기능 검사
bleController.checkBleOperator()
// 권한 요청 메서드 수정
fun permissionRequest(onPermissionGranted: () -> Unit) {
val permissionOk = bleController.requestBlePermission(this)
var allPermissionsGranted = true
for ((key, value) in permissionOk) {
if (!value) {
Log.e(MAIN_LOG_TAG, "권한 없음 : $key")
Toast.makeText(this, "${key}이 활성화되지 않았습니다.", Toast.LENGTH_SHORT).show()
allPermissionsGranted = false
}
}
if (allPermissionsGranted) {
// 모든 권한이 허용된 경우 콜백 실행
onPermissionGranted()
} else {
Log.i(MAIN_LOG_TAG, "권한 요청 중... ${permissionOk}")
}
}
// fun permissionRequest(): Boolean {
// // 4. Bluetooth 가 비활성화 된 경우 활성화 요청
// val permissionOk = bleController.requestBlePermission(this)
// for ((key, value) in permissionOk) {
// if (!value){
// Log.e(MAIN_LOG_TAG, "권한 없음 : ${key}")
// Toast.makeText(this, "${key}이 활성화되지 않았습니다.", Toast.LENGTH_SHORT).show()
// return false
// }
// }
// return true
// }
// BLE 초기화 완료 -----------------------------------------------------------------------------------
// UI 초기화 완료 ------------------------------------------------------------------------------------
// activity_main.xml의 View 초기화
btnScanStart = findViewById(R.id.btn_scan_start)
// activity_main.xml의 루트 레이아웃 가져오기
val rootLayout =
findViewById<RelativeLayout>(R.id.root_layout) // activity_main.xml의 루트 레이아웃 ID
// popup_scan_list.xml을 inflate
popupView = LayoutInflater.from(this)
.inflate(
R.layout.popup_scan_list,
rootLayout,
false
)
// popup_scan_list.xml 내부 View 초기
btnConnect = popupView.findViewById(R.id.btn_connect)
btnClose = popupView.findViewById(R.id.btn_close)
popupContainer = popupView.findViewById(R.id.popup_container)
recyclerScanList = popupView.findViewById(R.id.recycler_scan_list)
// 데이터 입력창 및 버튼 초기화
etInputData = findViewById(R.id.et_input_data)
btnSendData = findViewById(R.id.btn_send_data)
btnReceiveData = findViewById(R.id.btn_receive_data)
// RecyclerView 초기화
scanListAdapter.setupRecyclerView(recyclerScanList, this@MainActivity)
// 팝업을 루트 레이아웃에 추가
rootLayout.addView(popupView)
// Scan Start 버튼 클릭 리스너
btnScanStart.setOnClickListener {
permissionRequest {
// 권한이 허용된 경우에만 BLE 스캔 시작
startBleScan()
}
}
// 데이터 전송 버튼 클릭 리스너
btnSendData.setOnClickListener {
val inputData = etInputData.text.toString() // EditText에서 입력된 데이터 가져오기
if (inputData.isNotEmpty()) {
val dataToSend = inputData.toByteArray() // 문자열을 ByteArray로 변환
bleController.writeData(dataToSend) // BLE로 데이터 전송
Toast.makeText(this, "Data Sent: $inputData", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Please enter data to send", Toast.LENGTH_SHORT).show()
}
}
// 데이터 수신 버튼 클릭 리스너
btnReceiveData.setOnClickListener {
bleController.readData() // BLE 데이터 읽기 요청
}
// Close 버튼 클릭 리스너
btnClose.setOnClickListener {
stopBleScanAndClearScanList()
}
// // Connect 버튼 클릭 리스너
// btnConnect.setOnClickListener {
// val selectedDevice = scanListAdapter.getSelectedDevice()
// if (selectedDevice != null) {
// Toast.makeText(this, "Selected: ${selectedDevice?.name}", Toast.LENGTH_SHORT).show()
// // TODO : BLE GATT 연결 로직은 이후 구현
// // 권한이 허용된 경우 BLE 장치 연결
// permissionRequest {
// // 권한이 허용된 경우에만 BLE 스캔 시작
// bleController.connectToDevice(selectedDevice) { isConnected ->
// if (isConnected) {
// Log.i(MAIN_LOG_TAG, "BLE 장치에 성공적으로 연결되었습니다.")
// } else {
// Log.d(MAIN_LOG_TAG, "BLE 장치 연결이 해제되었습니다.")
// }
// }
// }
// } else {
// Toast.makeText(this, "No device selected", Toast.LENGTH_SHORT).show()
// }
// }
// Connect 버튼 클릭 리스너
btnConnect.setOnClickListener {
val selectedDevice = scanListAdapter.getSelectedDevice()
if (selectedDevice != null) { // unknown Device 의 경우
Toast.makeText(this, "Selected: ${selectedDevice.name}", Toast.LENGTH_SHORT).show()
// 권한이 허용된 경우 BLE 장치 연결
if (ActivityCompat.checkSelfPermission( // 블루투스 커넥트 권한 검사
this,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) { // 블루투스 권한이 이미 있는 경우
bleController.connectToDevice(selectedDevice) { isConnected ->
if (isConnected) {
Log.i(MAIN_LOG_TAG, "BLE 장치에 성공적으로 연결되었습니다.")
} else {
Log.i(MAIN_LOG_TAG, "BLE 장치 연결이 해제되었습니다.")
}
}
} else { // 블루투스 권한이 없는 경우 권한 요청 후 다시 Connect
permissionRequest {
bleController.connectToDevice(selectedDevice) { isConnected ->
if (isConnected) {
Log.i(MAIN_LOG_TAG, "BLE 장치에 성공적으로 연결되었습니다.")
} else {
Log.i(MAIN_LOG_TAG, "BLE 장치 연결이 해제되었습니다.")
}
}
}
}
} else {
Toast.makeText(this, "No device selected", Toast.LENGTH_SHORT).show()
}
}
// UI 초기화 완료 ------------------------------------------------------------------------------------
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
when (requestCode) {
101 -> { // BLE 스캔 권한 요청 결과
Log.i(MAIN_LOG_TAG, "블루투스 스캔 권한이 허용되었습니다.")
}
102 -> { // 위치 권한 요청 결과
Log.i(MAIN_LOG_TAG, "위치 권한이 허용되었습니다.")
}
103 -> { // BLE 연결 권한 요청 결과
Log.i(MAIN_LOG_TAG, "블루투스 연결 권한이 허용되었습니다.")
}
}
} else {
// 권한이 거부된 경우
when (requestCode) {
101 -> {
Log.e(MAIN_LOG_TAG, "블루투스 스캔 권한이 거부되었습니다.")
Toast.makeText(this, "블루투스 스캔 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
}
102 -> {
Log.e(MAIN_LOG_TAG, "위치 권한이 거부되었습니다.")
Toast.makeText(this, "위치 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
}
103 -> {
Log.e(MAIN_LOG_TAG, "블루투스 연결 권한이 거부되었습니다.")
Toast.makeText(this, "블루투스 연결 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onDestroy() {
super.onDestroy()
Log.i(MAIN_LOG_TAG, "onDestroy")
bleController.disconnect()
Toast.makeText(this, "onDestroy", Toast.LENGTH_SHORT).show()
stopBleScanAndClearScanList()
isPopupVisible = popupView.visibility == View.VISIBLE // 팝업 상태 저장
}
override fun onPause() {
super.onPause()
Log.i(MAIN_LOG_TAG, "onPause")
// Toast.makeText(this, "onPause", Toast.LENGTH_SHORT).show()
stopBleScanAndClearScanList()
isPopupVisible = popupView.visibility == View.VISIBLE // 팝업 상태 저장
}
override fun onResume() { //TODO : 앱 켜지면 자동으로 스캔해서 연결까지 동작
super.onResume()
Log.i(MAIN_LOG_TAG, "onResume")
// Toast.makeText(this, "onResume", Toast.LENGTH_SHORT).show()
// if (isPopupVisible) { // 팝업 상태 복구
// popupView.visibility = View.VISIBLE
// popupContainer.visibility = View.VISIBLE
// btnScanStart.visibility = View.GONE
// } else if (scanResults.isEmpty()) { // 스캔 결과가 없으면 스캔 재개
// startBleScan()
// }
}
private fun startBleScan() {
try {
bleController.startBleScan(scanCallback, popupContainer)
btnScanStart.visibility = View.GONE
popupView.visibility = View.VISIBLE
popupContainer.visibility = View.VISIBLE
} catch (e: Exception) {
Log.e(MAIN_LOG_TAG, "Failed to stop BLE scan: ${e.message}")
}
}
private fun stopBleScanAndClearScanList() {
try {
bleController.stopBleScan(scanCallback)
Log.i(MAIN_LOG_TAG, "블루투스 스캔 정지 ")
btnScanStart.visibility = View.VISIBLE // Scan Start 버튼 활성화
popupView.visibility = View.GONE // 팝업 숨김
popupContainer.visibility = View.GONE // 팝업 컨테이너 숨김
scanListAdapter.clearDevices()
} catch (e: Exception) {
Log.e(MAIN_LOG_TAG, "Failed to stop BLE scan: ${e.message}")
}
}
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
if (result.scanRecord?.deviceName == null){
// DeviceName 이 Null 인 경우, 스캔리스트에 추가 X
return
}else{
scanListAdapter.addDeviceToAdapt(device)
}
}
override fun onScanFailed(errorCode: Int) {
Log.e(MAIN_LOG_TAG, "onScanFailed called with errorCode: $errorCode")
when (errorCode) {
ScanCallback.SCAN_FAILED_ALREADY_STARTED -> Log.e(MAIN_LOG_TAG, "Scan already started")
ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> Log.e(MAIN_LOG_TAG, "App registration failed")
ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> Log.e(MAIN_LOG_TAG, "Internal error")
ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> Log.e(MAIN_LOG_TAG, "Feature unsupported")
}
Toast.makeText(this@MainActivity, "Scan failed: $errorCode", Toast.LENGTH_SHORT).show()
}
}
}
'언어 정리 > kotlin, android' 카테고리의 다른 글
Webview 기능 [ Kotlin ] (1) | 2024.11.19 |
---|---|
android studio IDE 단축키 (0) | 2024.11.15 |
kotlin 문법정리 (0) | 2024.11.14 |
댓글