react-native-move-sdk 2.4.1 → 2.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,7 +29,7 @@ def getExtOrIntegerDefault(name) {
29
29
  android {
30
30
  compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
31
31
  defaultConfig {
32
- minSdkVersion 16
32
+ minSdkVersion 26
33
33
  targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
34
34
  versionCode 1
35
35
  versionName "1.0"
@@ -66,8 +66,6 @@ repositories {
66
66
  url "https://dolphin.jfrog.io/artifactory/move-sdk-libs-release"
67
67
  }
68
68
 
69
-
70
-
71
69
  def found = false
72
70
  def defaultDir = null
73
71
  def androidSourcesName = 'React Native sources'
@@ -144,7 +142,7 @@ dependencies {
144
142
  // noinspection GradleDynamicVersion
145
143
 
146
144
  implementation "com.facebook.react:react-native:+"
147
- api "io.dolphin.move:move-sdk:2.4.1.70"
145
+ api "io.dolphin.move:move-sdk:2.4.2.71"
148
146
 
149
147
  testImplementation 'junit:junit:4.13.2'
150
148
  testImplementation "androidx.test:core:1.4.0"
@@ -1,3 +1,13 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
2
  package="in.dolph.move.sdk">
3
+ <application>
4
+ <receiver
5
+ android:name="in.dolph.move.sdk.BluetoothConnectionsReceiver"
6
+ android:exported="true">
7
+ <intent-filter>
8
+ <action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
9
+ <action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
10
+ </intent-filter>
11
+ </receiver>
12
+ </application>
3
13
  </manifest>
@@ -0,0 +1,96 @@
1
+ package `in`.dolph.move.sdk
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.bluetooth.BluetoothDevice
6
+ import android.content.BroadcastReceiver
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.SharedPreferences
10
+ import android.content.pm.PackageManager
11
+ import android.os.Build
12
+ import androidx.core.content.ContextCompat
13
+ import androidx.core.content.edit
14
+ import com.google.gson.Gson
15
+ import io.dolphin.move.MoveDevice
16
+
17
+ const val SHARED_PREF_CONNECTIONS_NAME = "move-react-connections"
18
+
19
+ class BluetoothConnectionsReceiver : BroadcastReceiver() {
20
+ override fun onReceive(context: Context, intent: Intent?) {
21
+ val device: BluetoothDevice =
22
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
23
+ intent?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
24
+ } else {
25
+ intent?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
26
+ } ?: return
27
+
28
+ when (intent?.action) {
29
+ BluetoothDevice.ACTION_ACL_CONNECTED -> {
30
+ handleDeviceConnection(context.applicationContext, device, true)
31
+ }
32
+
33
+ BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
34
+ handleDeviceConnection(context.applicationContext, device, false)
35
+ }
36
+ }
37
+ }
38
+
39
+ @SuppressLint("MissingPermission")
40
+ private fun handleDeviceConnection(context: Context, device: BluetoothDevice, isConnected: Boolean) {
41
+ val nativeSdkWrapper = NativeMoveSdkWrapper.getInstance(context.applicationContext)
42
+ if (!hasBluetoothReadPermissions(context)) {
43
+ nativeSdkWrapper.emitDeviceEvent(EVENT_MOVE_SDK_APP, "Missing BLUETOOTH_CONNECT permission")
44
+ nativeSdkWrapper.emitDeviceEvent(EVENT_MOVE_DEVICES, emptyList<MoveDevice>().toTypedArray())
45
+ return
46
+ }
47
+ val sharedPreferences: SharedPreferences =
48
+ context.getSharedPreferences(
49
+ SHARED_PREF_CONNECTIONS_NAME,
50
+ Context.MODE_PRIVATE
51
+ )
52
+ val name = device.name ?: device.address ?: ""
53
+ val moveDevice = MoveDevice(
54
+ id = device.address ?: "",
55
+ name = name,
56
+ manufacturerId = null,
57
+ )
58
+ nativeSdkWrapper.emitDeviceEvent(
59
+ EVENT_MOVE_SDK_APP,
60
+ "Device ${moveDevice.name} is ${if (isConnected) "connected" else "disconnected"}"
61
+ )
62
+ val gson = Gson()
63
+ val jsonString = try {
64
+ gson.toJson(moveDevice)
65
+ } catch (e: Exception) {
66
+ nativeSdkWrapper.emitDeviceEvent(EVENT_MOVE_SDK_APP, "Error parsing MoveDevice to json string")
67
+ null
68
+ }
69
+ sharedPreferences.edit {
70
+ if (isConnected) {
71
+ putString(moveDevice.id, jsonString)
72
+ } else {
73
+ remove(moveDevice.id)
74
+ }
75
+ }
76
+ val connectedDevices = sharedPreferences.all.mapNotNull {
77
+ try {
78
+ gson.fromJson(it.value as? String, MoveDevice::class.java)
79
+ } catch (e: Exception) {
80
+ null
81
+ }
82
+ }
83
+ nativeSdkWrapper.emitDeviceEvent(EVENT_MOVE_DEVICES, connectedDevices.toMoveDeviceTypedArray())
84
+ }
85
+
86
+ private fun hasBluetoothReadPermissions(context: Context): Boolean {
87
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
88
+ ContextCompat.checkSelfPermission(
89
+ context,
90
+ Manifest.permission.BLUETOOTH_CONNECT
91
+ ) == PackageManager.PERMISSION_GRANTED
92
+ } else {
93
+ true
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,186 @@
1
+ package `in`.dolph.move.sdk
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.bluetooth.BluetoothManager
6
+ import android.bluetooth.le.ScanCallback
7
+ import android.bluetooth.le.ScanResult
8
+ import android.content.Context
9
+ import android.content.SharedPreferences
10
+ import android.content.pm.PackageManager
11
+ import android.os.Build
12
+ import androidx.core.content.ContextCompat
13
+ import androidx.core.content.edit
14
+ import com.google.gson.Gson
15
+ import io.dolphin.move.MoveDevice
16
+ import io.dolphin.move.MoveSdk
17
+ import java.nio.ByteBuffer
18
+ import java.util.*
19
+
20
+ private const val TAG = "Android native scanner"
21
+
22
+ class DeviceScanner(
23
+ private val context: Context,
24
+ ) {
25
+
26
+ private val discoveredDevices = mutableSetOf<MoveDevice>()
27
+ private var onNewDevices: ((List<MoveDevice>) -> Unit)? = null
28
+ private var proximityId: String? = null
29
+ private var manufacturerId: Int? = null
30
+ private val btManager: BluetoothManager? = context.getSystemService(BluetoothManager::class.java)
31
+ private val gson = Gson()
32
+
33
+ private val leCallback = object : ScanCallback() {
34
+ @SuppressLint("MissingPermission")
35
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
36
+ if (proximityId != null && manufacturerId != null) {
37
+ val manufacturerSpecificDataId =
38
+ manufacturerId?.let { result.scanRecord?.getManufacturerSpecificData(it) }
39
+ val uuid = getProximityUUID(manufacturerSpecificDataId)
40
+ if (uuid.isNullOrEmpty() || !uuid.equals(proximityId, true)) return
41
+ }
42
+ val device = MoveSdk.get()?.convertToMoveDevice(result) ?: return
43
+ if (!discoveredDevices.contains(device)) {
44
+ discoveredDevices.add(device)
45
+ onNewDevices?.invoke(listOf(device))
46
+ }
47
+ }
48
+ }
49
+
50
+ @SuppressLint("MissingPermission")
51
+ fun startScanning(
52
+ filters: List<String>,
53
+ manufacturerId: Int?,
54
+ uuid: String?,
55
+ onNewDevices: (List<MoveDevice>) -> Unit,
56
+ onNewEvent: (String, String?) -> Unit,
57
+ ) {
58
+ if (filters.contains(MoveDeviceFilter.PAIRED.filter)) {
59
+ proceedWithConnectedDevices(onNewDevices, onNewEvent)
60
+ }
61
+ if (filters.contains(MoveDeviceFilter.BEACON.filter)) {
62
+ this.onNewDevices = onNewDevices
63
+ this.proximityId = uuid
64
+ this.manufacturerId = manufacturerId
65
+ proceedWithBleDevices(onNewDevices, onNewEvent)
66
+ }
67
+ }
68
+
69
+ @SuppressLint("MissingPermission")
70
+ fun stopScanning() {
71
+ btManager?.adapter?.bluetoothLeScanner?.stopScan(leCallback)
72
+ onNewDevices = null
73
+ proximityId = null
74
+ manufacturerId = null
75
+ }
76
+
77
+ private fun getProximityUUID(manufacturerSpecificData: ByteArray?): String? {
78
+ if (manufacturerSpecificData == null || manufacturerSpecificData.size < 23) {
79
+ return null
80
+ }
81
+ val resultBuffer = ByteBuffer.wrap(manufacturerSpecificData)
82
+ val uuidA = resultBuffer.getLong(2)
83
+ val uuidB = resultBuffer.getLong(10)
84
+ val uuid = UUID(uuidA, uuidB)
85
+ return uuid.toString().uppercase(Locale.getDefault())
86
+ }
87
+
88
+ private fun isPermissionGranted(permission: String): Boolean {
89
+ return try {
90
+ ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
91
+ } catch (e: Exception) {
92
+ false
93
+ }
94
+ }
95
+
96
+ private fun proceedWithConnectedDevices(
97
+ onNewDevices: (List<MoveDevice>) -> Unit,
98
+ onNewEvent: (String, String?) -> Unit,
99
+ ) {
100
+ val hasFeature = context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
101
+ if (!hasFeature) {
102
+ onNewEvent(TAG, "Missing FEATURE_BLUETOOTH")
103
+ onNewDevices.invoke(emptyList())
104
+ return
105
+ }
106
+ val sharedPreferences: SharedPreferences =
107
+ context.getSharedPreferences(
108
+ SHARED_PREF_CONNECTIONS_NAME,
109
+ Context.MODE_PRIVATE
110
+ )
111
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
112
+ if (!isPermissionGranted(Manifest.permission.BLUETOOTH_CONNECT)) {
113
+ onNewEvent(TAG, "Missing BLUETOOTH_CONNECT permission")
114
+ onNewDevices.invoke(emptyList())
115
+ if (sharedPreferences.all.isNotEmpty()) {
116
+ sharedPreferences.edit { clear() }
117
+ }
118
+ return
119
+ }
120
+ }
121
+ val connectedDevices = sharedPreferences.all.mapNotNull {
122
+ try {
123
+ gson.fromJson(it.value as? String, MoveDevice::class.java)
124
+ } catch (e: Exception) {
125
+ null
126
+ }
127
+ }
128
+ onNewDevices(connectedDevices)
129
+ }
130
+
131
+ @SuppressLint("MissingPermission")
132
+ private fun proceedWithBleDevices(
133
+ onNewDevices: (List<MoveDevice>) -> Unit,
134
+ onNewEvent: (String, String?) -> Unit,
135
+ ) {
136
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
137
+ if (!isPermissionGranted(Manifest.permission.BLUETOOTH_SCAN)) {
138
+ onNewEvent(TAG, "Missing BLUETOOTH_SCAN permission")
139
+ onNewDevices.invoke(emptyList())
140
+ return
141
+ }
142
+ if (!isPermissionGranted(Manifest.permission.BLUETOOTH_CONNECT)) {
143
+ onNewEvent(TAG, "Missing BLUETOOTH_CONNECT permission")
144
+ onNewDevices.invoke(emptyList())
145
+ return
146
+ }
147
+ }
148
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
149
+ if (!isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
150
+ onNewEvent(TAG, "Missing ACCESS_FINE_LOCATION permission")
151
+ onNewDevices.invoke(emptyList())
152
+ return
153
+ }
154
+ if (!isPermissionGranted(Manifest.permission.BLUETOOTH_ADMIN)) {
155
+ onNewEvent(TAG, "Missing BLUETOOTH_ADMIN permission")
156
+ onNewDevices.invoke(emptyList())
157
+ return
158
+ }
159
+ }
160
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R || Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
161
+ if (!isPermissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {
162
+ onNewEvent(TAG, "Missing ACCESS_BACKGROUND_LOCATION permission")
163
+ onNewDevices.invoke(emptyList())
164
+ return
165
+ }
166
+ }
167
+ if (btManager?.adapter?.isEnabled == false) {
168
+ onNewEvent(TAG, "Please turn on bluetooth")
169
+ onNewDevices.invoke(emptyList())
170
+ return
171
+ }
172
+ val hasFeature = context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)
173
+ if (!hasFeature) {
174
+ onNewEvent(TAG, "Missing FEATURE_BLUETOOTH_LE")
175
+ onNewDevices.invoke(emptyList())
176
+ return
177
+ }
178
+ discoveredDevices.clear()
179
+ btManager?.adapter?.bluetoothLeScanner?.startScan(leCallback)
180
+ }
181
+ }
182
+
183
+ enum class MoveDeviceFilter(val filter: String) {
184
+ BEACON("beacon"),
185
+ PAIRED("paired");
186
+ }
@@ -0,0 +1,27 @@
1
+ package `in`.dolph.move.sdk
2
+
3
+ import io.dolphin.move.MoveDevice
4
+ import io.dolphin.move.MoveScanResult
5
+
6
+ fun List<MoveScanResult>.toScanResultArray(): Array<Any> {
7
+ val data = mutableListOf<Any>()
8
+ forEach {
9
+ val map = mutableMapOf<String, Any?>()
10
+ map["isDiscovered"] = it.isDiscovered
11
+ map["name"] = it.device.name
12
+ map["device"] = it.device.toJsonString()
13
+ data.add(map)
14
+ }
15
+ return data.toTypedArray()
16
+ }
17
+
18
+ fun List<MoveDevice>.toMoveDeviceTypedArray(): Array<Any> {
19
+ val devices = mutableListOf<Any>()
20
+ forEach {
21
+ val map = mutableMapOf<String, Any?>()
22
+ map["name"] = it.name
23
+ map["data"] = it.toJsonString()
24
+ devices.add(map)
25
+ }
26
+ return devices.toTypedArray()
27
+ }
@@ -1,5 +1,6 @@
1
1
  package `in`.dolph.move.sdk
2
2
 
3
+ import com.google.gson.Gson
3
4
  import io.dolphin.move.MoveDetectionService
4
5
  import java.util.Locale
5
6
  import java.util.regex.Pattern
@@ -61,3 +62,11 @@ fun String.snakeToUpperCamelCaseParam(): String {
61
62
  fun String.equalsService(service: MoveDetectionService): Boolean {
62
63
  return equals(service.name().camelToSnakeCase(), ignoreCase = true)
63
64
  }
65
+
66
+ inline fun <reified T> T.toJsonString(): String {
67
+ return try {
68
+ Gson().toJson(this)
69
+ } catch (e: Exception) {
70
+ ""
71
+ }
72
+ }
@@ -17,6 +17,7 @@ import com.facebook.react.bridge.ReactApplicationContext
17
17
  import com.facebook.react.bridge.ReactContextBaseJavaModule
18
18
  import com.facebook.react.bridge.ReactMethod
19
19
  import com.facebook.react.bridge.ReadableArray
20
+ import com.facebook.react.bridge.ReadableMap
20
21
  import com.facebook.react.bridge.WritableArray
21
22
  import com.facebook.react.bridge.WritableMap
22
23
  import com.facebook.react.modules.core.DeviceEventManagerModule
@@ -30,8 +31,11 @@ internal const val EVENT_TRIP_STATE = "MoveSdk-TripState"
30
31
  internal const val EVENT_AUTH_STATE = "MoveSdk-AuthState"
31
32
  internal const val EVENT_ERRORS = "MoveSdk-Errors"
32
33
  internal const val EVENT_WARNINGS = "MoveSdk-Warnings"
34
+ internal const val EVENT_MOVE_DEVICES = "MoveSdk-Devices"
33
35
  internal const val EVENT_AUTH_BATTERY_PERMISSION = "MoveSdk-Permission-BatteryOptimization"
34
36
  internal const val EVENT_AUTH_OVERLAY_PERMISSION = "MoveSdk-Permission-Overlay"
37
+ internal const val EVENT_MOVE_SCAN_RESULT = "MOVE_SDK_SCAN_RESULT"
38
+ internal const val EVENT_MOVE_SDK_APP = "MOVE_SDK_APP_EVENT"
35
39
 
36
40
  internal const val ARGUMENT_ACCESS_TOKEN = "accessToken"
37
41
  internal const val ARGUMENT_REFRESH_TOKEN = "refreshToken"
@@ -42,6 +46,8 @@ internal const val ARGUMENT_WARNINGS = "warnings"
42
46
  internal const val ARGUMENT_LOG = "log"
43
47
  internal const val ARGUMENT_WARNING = "warnings"
44
48
  internal const val ARGUMENT_ERROR_REASON = "errorReason"
49
+ internal const val ARGUMENT_DEVICES = "devices"
50
+ internal const val ARGUMENT_SCAN_RESULTS = "results"
45
51
 
46
52
  internal const val PROMISE_OK = "OK"
47
53
 
@@ -86,7 +92,7 @@ class MoveSdkModule(context: ReactApplicationContext) : ReactContextBaseJavaModu
86
92
  timelineDetectionServices: ReadableArray,
87
93
  drivingServices: ReadableArray,
88
94
  walkingServices: ReadableArray,
89
- options: ReadableArray,
95
+ options: ReadableMap?,
90
96
  // Platform config
91
97
  recognitionNotificationTitle: String,
92
98
  recognitionNotificationText: String,
@@ -100,7 +106,6 @@ class MoveSdkModule(context: ReactApplicationContext) : ReactContextBaseJavaModu
100
106
  tripNotificationChannelDescription: String,
101
107
  promise: Promise?
102
108
  ) {
103
-
104
109
  nativeSdkWrapper.setup(
105
110
  userId,
106
111
  accessToken,
@@ -260,6 +265,17 @@ class MoveSdkModule(context: ReactApplicationContext) : ReactContextBaseJavaModu
260
265
  }
261
266
  }
262
267
 
268
+ internal fun emitDeviceEvent(eventName: String, data: String) {
269
+ try {
270
+ reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
271
+ .emit(eventName.toSnakeCase(), data)
272
+ } catch (t: Throwable) {
273
+ Log.e("SDK Module", t.message, t)
274
+ // java.lang.IllegalStateException: Tried to access a JS module before the React instance was fully set up.
275
+ // Calls to ReactContext#getJSModule should only happen once initialize() has been called on your native module.
276
+ }
277
+ }
278
+
263
279
  @ReactMethod
264
280
  fun addListener(eventName: String) {
265
281
  // Set up any upstream listeners or background tasks as necessary
@@ -294,6 +310,45 @@ class MoveSdkModule(context: ReactApplicationContext) : ReactContextBaseJavaModu
294
310
  promise.resolve(isAuthValid)
295
311
  }
296
312
 
313
+ @ReactMethod
314
+ fun registerDevices(devices: ReadableArray) {
315
+ try {
316
+ nativeSdkWrapper.registerDevices(devices.toArrayList() as? ArrayList<Map<String, String>>)
317
+ } catch (t: Throwable) {
318
+ Log.e("SDK Module", t.message, t)
319
+ }
320
+ }
321
+
322
+ @ReactMethod
323
+ fun unregisterDevices(devices: ReadableArray) {
324
+ try {
325
+ nativeSdkWrapper.unregisterDevices(devices.toArrayList() as? ArrayList<Map<String, String>>)
326
+ } catch (t: Throwable) {
327
+ Log.e("SDK Module", t.message, t)
328
+ }
329
+ }
330
+
331
+ @ReactMethod
332
+ fun getRegisteredDevices(promise: Promise) {
333
+ nativeSdkWrapper.getRegisteredDevices(promise)
334
+ }
335
+
336
+ @ReactMethod
337
+ fun startScanningDevices(filter: ReadableArray?, uuid: String?, manufacturerId: Int?) {
338
+ Log.i("MODULE_SCAN_DEVICE", "startScanningDevices: ${filter}")
339
+ val filters = try {
340
+ filter?.toArrayList()?.map { it.toString() }.orEmpty()
341
+ } catch (e: Exception) {
342
+ emptyList<String>()
343
+ }
344
+ nativeSdkWrapper.startScanningDevices(filters, manufacturerId, uuid)
345
+ }
346
+
347
+ @ReactMethod
348
+ fun stopScanningDevices() {
349
+ nativeSdkWrapper.stopScanningDevices()
350
+ }
351
+
297
352
  // region PERMISSIONS MODULE
298
353
  @SuppressLint("NewApi")
299
354
  override fun onActivityResult(
@@ -368,18 +423,23 @@ internal fun Array<*>.toWritableArray(): WritableArray {
368
423
  is String -> {
369
424
  data.pushString(value)
370
425
  }
426
+
371
427
  is Boolean -> {
372
428
  data.pushBoolean(value)
373
429
  }
430
+
374
431
  is Int -> {
375
432
  data.pushInt(value)
376
433
  }
434
+
377
435
  is Array<*> -> {
378
436
  data.pushArray(value.toWritableArray())
379
437
  }
438
+
380
439
  is List<*> -> {
381
440
  data.pushArray(value.toTypedArray().toWritableArray())
382
441
  }
442
+
383
443
  is Map<*, *> -> {
384
444
  data.pushMap(value.toWritableMap())
385
445
  }
@@ -397,18 +457,23 @@ internal fun Map<*, *>.toWritableMap(): WritableMap {
397
457
  is String -> {
398
458
  data.putString(entry.key.toString(), value)
399
459
  }
460
+
400
461
  is Boolean -> {
401
462
  data.putBoolean(entry.key.toString(), value)
402
463
  }
464
+
403
465
  is Int -> {
404
466
  data.putInt(entry.key.toString(), value)
405
467
  }
468
+
406
469
  is Array<*> -> {
407
470
  data.putArray(entry.key.toString(), value.toWritableArray())
408
471
  }
472
+
409
473
  is List<*> -> {
410
474
  data.putArray(entry.key.toString(), value.toTypedArray().toWritableArray())
411
475
  }
476
+
412
477
  is Map<*, *> -> {
413
478
  data.putMap(entry.key.toString(), value.toWritableMap())
414
479
  }