-
Notifications
You must be signed in to change notification settings - Fork 0
/
BluetoothHelper.java
279 lines (236 loc) · 11 KB
/
BluetoothHelper.java
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
package com.example.automatedattendancemonitoring;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.ResolvableApiException;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResponse;
import com.google.android.gms.tasks.Task;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
/**
* Class with static helper functions for more convenient use of bluetooth (including bluetooth LE)
*/
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "unused"})
public final class BluetoothHelper {
/**
* To prevent creating instances of this class
*/
private BluetoothHelper() {
}
/**
* The UUID used to identify the app
*/
public static final ParcelUuid UUID = ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
/**
* Default bluetooth adapter. ADAPTER should be checked for null before using this class.
*/
public static final BluetoothAdapter ADAPTER = BluetoothAdapter.getDefaultAdapter();
// Helper functions
/**
* <p>If bluetooth is enabled, executes {@param toExecute} and returns {@code null}.
* Otherwise, enables bluetooth and registers {@link BroadcastReceiver},
* which will execute {@param toExecute} after enabling.</p>
* <p>Note, that if bluetooth will not be enabled for some reason,
* {@link BroadcastReceiver} will still be registered and will
* execute {@param toExecute} if bluetooth will be enabled later.
* To avoid that, use {@link Activity#unregisterReceiver(BroadcastReceiver)},
* passing returned {@link BroadcastReceiver}.</p>
*
* @param activity activity to register {@link BroadcastReceiver} in
* @param toExecute code to be executed when bluetooth will be enabled
* @return {@link BroadcastReceiver} instance used to capture bluetooth enabling
* of {@code null}, if bluetooth was already enabled
*/
public static BroadcastReceiver enableBluetoothAndExecute(Activity activity, Runnable toExecute) {
if (ADAPTER.isEnabled()) {
toExecute.run();
return null;
}
BroadcastReceiver stateChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1);
if (state != BluetoothAdapter.STATE_ON) return;
toExecute.run();
activity.unregisterReceiver(this);
}
};
activity.registerReceiver(stateChangedReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
ADAPTER.enable();
return stateChangedReceiver;
}
/**
* Turns on geolocation services. This is needed for scanning on some android versions
*
* @param activity activity, which will be used to request permissions
*/
private static void turnOnGPS(Activity activity) {
LocationRequest locationRequest = new LocationRequest();
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder()
.addLocationRequest(locationRequest);
Task<LocationSettingsResponse> locationSettingsTask = LocationServices.getSettingsClient(activity).checkLocationSettings(builder.build());
locationSettingsTask.addOnCompleteListener(task -> {
try {
LocationSettingsResponse response = task.getResult(ApiException.class);
} catch (ResolvableApiException resolvable) {
try {
resolvable.startResolutionForResult(activity, 0);
} catch (IntentSender.SendIntentException e) {
Log.w("GPS", "Could not enable GPS");
}
} catch (ApiException exception) {
Log.w("GPS", "Could not enable GPS");
}
});
}
// Advertising functions
/**
* Asks for all necessary permissions, enables all necessary services and
* starts Bluetooth LE advertising using {@link #advertise(String, AdvertiseCallback)}.
* If advertising succeeds, {@code onAdvertisingStarted} is executed.
* If user denies giving permission or enabling service or advertising is failed
* for any other reason, {@code onError} is called.
* Returns {@link AdvertiseCallback} instance, which can be used to stop advertising
* by passing it to {@link BluetoothLeAdvertiser#stopAdvertising(AdvertiseCallback)}.
*
* @param activity activity, which will be used to request permissions
* @param data string of data to be advertised
* @param onAdvertisingStarted function to be executed in case starting advertising is started successfully
* @param onError function to be executed in case starting advertising is failed
* @return {@link AdvertiseCallback} instance used in starting advertising
*/
public static AdvertiseCallback advertise(@NotNull Activity activity,
@NotNull String data,
@Nullable Consumer<AdvertiseSettings> onAdvertisingStarted,
@Nullable Consumer<Integer> onError) {
AdvertiseCallback callback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
if (onAdvertisingStarted != null) onAdvertisingStarted.accept(settingsInEffect);
Log.i("advertise", "Advertising started successfully");
}
@Override
public void onStartFailure(int errorCode) {
if (onError != null) onError.accept(errorCode);
else
Log.w("advertise", "Advertising failed, but no onError provided: " + errorCode);
}
};
enableBluetoothAndExecute(activity, () -> advertise(data, callback));
return callback;
}
/**
* Starts Bluetooth LE advertising with default settings and given {@link AdvertiseCallback}.
* Advertisement data consists of {@link #UUID} and {@code data} parameter.
*
* @param data string of data to be advertised
* @param callback callback that will be used to start the advertising
*/
public static void advertise(@NotNull String data,
@NotNull AdvertiseCallback callback) {
ADAPTER.getBluetoothLeAdvertiser().startAdvertising(
// settings
new AdvertiseSettings.Builder().build(),
// data
new AdvertiseData.Builder()
.addServiceUuid(BluetoothHelper.UUID)
.addServiceData(BluetoothHelper.UUID, data.getBytes(StandardCharsets.UTF_8))
.build(),
// callback
callback
);
}
// Scan functions
/**
* Asks for all necessary permissions, enables all necessary services and
* starts Bluetooth LE scan using {@link #scan(ScanCallback)}.
* If scan is successful, passes all received scans to {@code onScanResults}.
* If user denies giving permission or enabling service or scan is failed
* for any other reason, {@code onError} is called.
* Returns {@link ScanCallback} instance, which can be used to stop scan
* by passing it to {@link BluetoothLeScanner#stopScan(ScanCallback)}.
*
* @param activity activity, which will be used to request permissions
* @param onScanResults function to pass received scan results to
* @param onError function to be executed in case starting scan is failed
* @return {@link ScanCallback} instance used in starting scan
*/
public static ScanCallback scan(@NotNull Activity activity,
@NotNull Consumer<String> onScanResults,
@Nullable Consumer<Integer> onError) {
ScanCallback callback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
byte[] dataBytes = null;
if (result != null && result.getScanRecord() != null) {
dataBytes = result.getScanRecord().getServiceData(BluetoothHelper.UUID);
}
if (dataBytes == null) {
Log.w("scan", "got empty result");
return;
}
String data = new String(dataBytes, StandardCharsets.UTF_8);
Log.i("scan", "got scan result: " + data);
onScanResults.accept(data);
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult result : results) {
this.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result);
}
}
@Override
public void onScanFailed(int errorCode) {
if (onError != null) onError.accept(errorCode);
else Log.w("scan", "Scanning failed, but no onError provided: " + errorCode);
}
};
turnOnGPS(activity);
enableBluetoothAndExecute(activity, () -> scan(callback));
return callback;
}
/**
* Starts Bluetooth LE scan with default settings,
* one filter by {@link #UUID} and given {@link ScanCallback}.
*
* @param callback callback that will be used to start the scan
*/
public static void scan(@NotNull ScanCallback callback) {
ADAPTER.getBluetoothLeScanner().startScan(
//filters
Collections.singletonList(
new ScanFilter.Builder()
.setServiceUuid(BluetoothHelper.UUID)
.build()
),
//settings
new ScanSettings.Builder().build(),
//callback
callback
);
}
}