앱을 만들고 있는데 필요한 기능이 이미 페어링된 기기가 연결됐는지 감지하는 기능인데 플러터만으로는 구현이 불가능해서 찾아보게 되었습니다.
어떻게?#
- 앱이 백그라운드 상태일 때도 블루투스 감지
- 앱에서 주기적으로 체크하는 것이 아닌 콜백이나 이벤트로 처리
- 결정적으로 플러터에서 지원하지 않음
- 네이티브 코드 작성(BroadcastReceiver 및 EventChannel)
EventChannel#
💡 Quotation
네이티브(Android, iOS)와 플러터 간에 지속적인 이벤트 스트림을 주고받을 때 사용하는 통신 방법
⚠️ iOS 는 작성해 보지 않음
네이티브 코드 작성#
/PROJECT_DIR/android/src/kotlin/com/example/PROJECT_NAME/MainActivity.kt
안드로이드 네이티브 코드 시작점이 있는 파일인데 이곳에 서비스, 블루투스 연결 감지를 위한 코드를 작성하고 이벤트 채널을 이용하여 플러터 앱으로 전달을 하면 됩니다. 😄
우선 서비스와 블루투스 연결 감지를 위한 것들을 하나의 클래스로 만들기 위해 새로운 파일을 만들었습니다. 원본
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| package com.example.engine_oil_timer
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.PluginRegistry
import android.bluetooth.BluetoothDevice
class BluetoothForegroundService : Service() {
companion object {
private var eventSink: EventChannel.EventSink? = null
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
startForegroundService()
registerReceiver(bluetoothReceiver, IntentFilter("android.bluetooth.device.action.ACL_CONNECTED"))
registerReceiver(bluetoothReceiver, IntentFilter("android.bluetooth.device.action.ACL_DISCONNECTED"))
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(bluetoothReceiver)
}
private val bluetoothReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.action?.let { action ->
val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
val name = device?.name ?: "Unknown"
val address = device?.address ?: "Unknown"
when(action) {
"android.bluetooth.device.action.ACL_CONNECTED" -> {
val data = mapOf(
"event" to "connected",
"name" to name,
"address" to address
)
eventSink?.success(data)
}
"android.bluetooth.device.action.ACL_DISCONNECTED" -> {
val data = mapOf(
"event" to "disconnected",
"name" to name,
"address" to address
)
eventSink?.success(data)
}
}
}
}
}
private fun startForegroundService() {
val channelId = "bluetooth_service"
val channelName = "Bluetooth Service"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(chan)
}
val notification: Notification = Notification.Builder(this, channelId)
.setContentTitle("엔진 오일 타이머")
.setContentText("백그라운드에서 작동됩니다.")
.setSmallIcon(R.mipmap.ic_launcher)
.build()
startForeground(1, notification)
}
fun setEventSink(sink: EventChannel.EventSink) {
eventSink = sink
}
}
|
MainActivity.kt
에는 최종적으로 채널 설정을 하면 완성 됩니다. 원본
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| package com.example.engine_oil_timer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import android.bluetooth.BluetoothDevice
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val BLUETOOTH_EVENT_CHANNEL = "bluetooth/events"
private var eventSink: EventChannel.EventSink? = null
private val CHANNEL = "app.channel.shared.data"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// EventChannel 연결
EventChannel(flutterEngine?.dartExecutor?.binaryMessenger, BLUETOOTH_EVENT_CHANNEL)
.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
val service = BluetoothForegroundService()
service.setEventSink(events!!)
}
override fun onCancel(arguments: Any?) {}
})
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "moveToBackground") {
moveTaskToBack(true)
result.success(null)
} else if (call.method == "startService") {
val serviceIntent = Intent(this, BluetoothForegroundService::class.java)
startForegroundService(serviceIntent)
result.success(true)
}
}
}
}
|
플러터 앱 코드#
앱에서는 해당 기능을 싱글턴 클래스에 넣어놓고 스트림에 대한 Listening 설정, 연결 됐을때와 해제 됐을 때 실행해줄 콜백을 추가하여 완료 지었습니다. 원본
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| import 'package:engine_oil_timer/utility/system.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
class Bluetooth {
static final Bluetooth _instance = Bluetooth._internal();
Bluetooth._internal();
factory Bluetooth() {
return _instance;
}
final EventChannel _eventChannel = EventChannel('bluetooth/events');
Stopwatch _stopwatch = Stopwatch();
List<void Function()> _onConnect = [];
void addOnConnect(void Function() onConnect) => _onConnect.add(onConnect);
void removeOnConnect(void Function() onConnect) =>
_onConnect.remove(onConnect);
List<void Function()> _onDisconnect = [];
void addOnDisconnect(void Function() onDisconnect) =>
_onDisconnect.add(onDisconnect);
void removeOnDisconnect(void Function() onDisconnect) =>
_onDisconnect.remove(onDisconnect);
void listen() async {
_eventChannel
.receiveBroadcastStream()
.map((event) => Map<String, dynamic>.from(event))
.listen((event) {
final status = event['event'];
final name = event['name'];
final address = event['address'];
print('블루투스 감지 $status: $name ($address)');
if (System().data.deviceAddress.compareTo(address) == 0) {
if (status == 'connected') {
_stopwatch.start();
_onConnect.forEach((onConnect) {
onConnect();
});
} else if (status == 'disconnected') {
_stopwatch.stop();
_onDisconnect.forEach((onDisconnect) {
onDisconnect();
});
}
}
});
}
Future<List<BluetoothDevice>> getBoundedDevices() async {
return await FlutterBluetoothSerial.instance.getBondedDevices();
}
Future<String> getDeviceName() async {
String result = "";
List<BluetoothDevice> devices = await getBoundedDevices();
for (BluetoothDevice device in devices) {
if (device.address.compareTo(System().data.deviceAddress) == 0) {
if (device.name != null) {
result = device.name ?? "";
break;
}
}
}
return result;
}
Duration get elapsed => _stopwatch.elapsed;
String getElapsed() {
final h = _stopwatch.elapsed.inHours;
final m = _stopwatch.elapsed.inMinutes % 60;
final s = _stopwatch.elapsed.inSeconds % 60;
return '${h.toString().padLeft(2, '0')}:'
'${m.toString().padLeft(2, '0')}:'
'${s.toString().padLeft(2, '0')}';
}
}
|