@expofp/react-native-efp-crowdconnected 0.1.0 → 0.1.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.
- package/ExpofpCrowdconnected.podspec +5 -5
- package/android/build.gradle +5 -4
- package/android/src/main/java/com/expofpcrowdconnected/PermissionRequester.kt +233 -0
- package/android/src/main/java/com/expofpcrowdconnected/RCTCrowdConnectedLocationProviderModule.kt +155 -7
- package/ios/RCTCrowdConnectedLocationProvider.swift +0 -4
- package/package.json +4 -3
|
@@ -16,11 +16,11 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm,cpp,swift}"
|
|
17
17
|
s.private_header_files = "ios/**/*.h"
|
|
18
18
|
|
|
19
|
-
s.dependency 'CrowdConnectedShared', '2.
|
|
20
|
-
s.dependency 'CrowdConnectedCore', '2.
|
|
21
|
-
s.dependency 'CrowdConnectedIPS', '2.
|
|
22
|
-
s.dependency 'CrowdConnectedCoreBluetooth', '2.
|
|
23
|
-
s.dependency 'CrowdConnectedGeo', '2.
|
|
19
|
+
s.dependency 'CrowdConnectedShared', '2.3.0'
|
|
20
|
+
s.dependency 'CrowdConnectedCore', '2.3.0'
|
|
21
|
+
s.dependency 'CrowdConnectedIPS', '2.3.0'
|
|
22
|
+
s.dependency 'CrowdConnectedCoreBluetooth', '2.3.0'
|
|
23
|
+
s.dependency 'CrowdConnectedGeo', '2.3.0'
|
|
24
24
|
|
|
25
25
|
install_modules_dependencies(s)
|
|
26
26
|
end
|
package/android/build.gradle
CHANGED
|
@@ -78,9 +78,10 @@ dependencies {
|
|
|
78
78
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
79
79
|
|
|
80
80
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
|
81
|
+
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
|
81
82
|
|
|
82
|
-
implementation "net.crowdconnected.android.core:android-core:2.1.
|
|
83
|
-
implementation "net.crowdconnected.android.ips:android-ips:2.1.
|
|
84
|
-
implementation "net.crowdconnected.android.geo:android-geo:2.1.
|
|
85
|
-
implementation "net.crowdconnected.android.background:android-background:2.1.
|
|
83
|
+
implementation "net.crowdconnected.android.core:android-core:2.1.3"
|
|
84
|
+
implementation "net.crowdconnected.android.ips:android-ips:2.1.3"
|
|
85
|
+
implementation "net.crowdconnected.android.geo:android-geo:2.1.3"
|
|
86
|
+
implementation "net.crowdconnected.android.background:android-background:2.1.3"
|
|
86
87
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
package com.expofpcrowdconnected
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.bluetooth.BluetoothAdapter
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.pm.PackageManager
|
|
8
|
+
import android.net.Uri
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.os.Bundle
|
|
11
|
+
import android.provider.Settings
|
|
12
|
+
import androidx.activity.result.ActivityResultLauncher
|
|
13
|
+
import androidx.activity.result.contract.ActivityResultContracts
|
|
14
|
+
import androidx.core.content.ContextCompat
|
|
15
|
+
import androidx.fragment.app.Fragment
|
|
16
|
+
import androidx.fragment.app.FragmentActivity
|
|
17
|
+
import kotlin.coroutines.resume
|
|
18
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result of a permission request containing grant status and lists of denied permissions.
|
|
22
|
+
*
|
|
23
|
+
* @param granted true if all requested permissions were granted
|
|
24
|
+
* @param denied list of permissions the user denied
|
|
25
|
+
* @param permanentlyDenied list of permissions the user denied with "Don't ask again"
|
|
26
|
+
*/
|
|
27
|
+
data class PermissionResult(
|
|
28
|
+
val granted: Boolean,
|
|
29
|
+
val denied: List<String>,
|
|
30
|
+
val permanentlyDenied: List<String>
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Headless Fragment that handles runtime permission requests.
|
|
35
|
+
*
|
|
36
|
+
* Uses ActivityResultContracts to request permissions without requiring
|
|
37
|
+
* the hosting Activity to implement onRequestPermissionsResult.
|
|
38
|
+
* Mirrors the native CrowdConnected SDK's PermissionHostFragment pattern.
|
|
39
|
+
*/
|
|
40
|
+
class PermissionRequester : Fragment() {
|
|
41
|
+
|
|
42
|
+
companion object {
|
|
43
|
+
private const val FRAGMENT_TAG = "com.expofpcrowdconnected.PermissionRequester"
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Obtain an existing or create a new PermissionRequester fragment
|
|
47
|
+
* attached to the given activity. Must be called on the main thread.
|
|
48
|
+
*/
|
|
49
|
+
fun obtain(activity: FragmentActivity): PermissionRequester {
|
|
50
|
+
val fm = activity.supportFragmentManager
|
|
51
|
+
val existing = fm.findFragmentByTag(FRAGMENT_TAG) as? PermissionRequester
|
|
52
|
+
if (existing != null) {
|
|
53
|
+
return existing
|
|
54
|
+
}
|
|
55
|
+
val fragment = PermissionRequester()
|
|
56
|
+
fm.beginTransaction()
|
|
57
|
+
.add(fragment, FRAGMENT_TAG)
|
|
58
|
+
.setReorderingAllowed(true)
|
|
59
|
+
.commitNowAllowingStateLoss()
|
|
60
|
+
return fragment
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Launchers (registered in onCreate) ---
|
|
65
|
+
|
|
66
|
+
private lateinit var multiPermLauncher: ActivityResultLauncher<Array<String>>
|
|
67
|
+
private lateinit var singlePermLauncher: ActivityResultLauncher<String>
|
|
68
|
+
private lateinit var settingsLauncher: ActivityResultLauncher<Intent>
|
|
69
|
+
private lateinit var bluetoothLauncher: ActivityResultLauncher<Intent>
|
|
70
|
+
|
|
71
|
+
// --- Callbacks ---
|
|
72
|
+
|
|
73
|
+
private var multiPermCallback: ((PermissionResult) -> Unit)? = null
|
|
74
|
+
private var singlePermCallback: ((Boolean) -> Unit)? = null
|
|
75
|
+
private var settingsCallback: ((Boolean) -> Unit)? = null
|
|
76
|
+
private var settingsCheck: (() -> Boolean)? = null
|
|
77
|
+
private var bluetoothCallback: ((Boolean) -> Unit)? = null
|
|
78
|
+
|
|
79
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
80
|
+
super.onCreate(savedInstanceState)
|
|
81
|
+
|
|
82
|
+
multiPermLauncher = registerForActivityResult(
|
|
83
|
+
ActivityResultContracts.RequestMultiplePermissions()
|
|
84
|
+
) { results ->
|
|
85
|
+
val denied = results.filterValues { !it }.keys.toList()
|
|
86
|
+
val permanentlyDenied = denied.filter { perm ->
|
|
87
|
+
!shouldShowRequestPermissionRationale(perm)
|
|
88
|
+
}
|
|
89
|
+
multiPermCallback?.invoke(
|
|
90
|
+
PermissionResult(
|
|
91
|
+
granted = denied.isEmpty(),
|
|
92
|
+
denied = denied,
|
|
93
|
+
permanentlyDenied = permanentlyDenied
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
multiPermCallback = null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
singlePermLauncher = registerForActivityResult(
|
|
100
|
+
ActivityResultContracts.RequestPermission()
|
|
101
|
+
) { granted ->
|
|
102
|
+
singlePermCallback?.invoke(granted)
|
|
103
|
+
singlePermCallback = null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
settingsLauncher = registerForActivityResult(
|
|
107
|
+
ActivityResultContracts.StartActivityForResult()
|
|
108
|
+
) {
|
|
109
|
+
val ok = settingsCheck?.invoke() ?: false
|
|
110
|
+
settingsCallback?.invoke(ok)
|
|
111
|
+
settingsCallback = null
|
|
112
|
+
settingsCheck = null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
bluetoothLauncher = registerForActivityResult(
|
|
116
|
+
ActivityResultContracts.StartActivityForResult()
|
|
117
|
+
) { result ->
|
|
118
|
+
bluetoothCallback?.invoke(result.resultCode == Activity.RESULT_OK)
|
|
119
|
+
bluetoothCallback = null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
override fun onDestroy() {
|
|
124
|
+
multiPermCallback?.let { cb ->
|
|
125
|
+
multiPermCallback = null
|
|
126
|
+
cb(PermissionResult(granted = false, denied = emptyList(), permanentlyDenied = emptyList()))
|
|
127
|
+
}
|
|
128
|
+
singlePermCallback?.let { cb ->
|
|
129
|
+
singlePermCallback = null
|
|
130
|
+
cb(false)
|
|
131
|
+
}
|
|
132
|
+
settingsCallback?.let { cb ->
|
|
133
|
+
settingsCallback = null
|
|
134
|
+
settingsCheck = null
|
|
135
|
+
cb(false)
|
|
136
|
+
}
|
|
137
|
+
bluetoothCallback?.let { cb ->
|
|
138
|
+
bluetoothCallback = null
|
|
139
|
+
cb(false)
|
|
140
|
+
}
|
|
141
|
+
super.onDestroy()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Public suspend API ---
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Request multiple permissions and suspend until the user responds.
|
|
148
|
+
*/
|
|
149
|
+
suspend fun requestPermissions(permissions: Array<String>): PermissionResult =
|
|
150
|
+
suspendCancellableCoroutine { cont ->
|
|
151
|
+
if (permissions.isEmpty()) {
|
|
152
|
+
cont.resume(PermissionResult(true, emptyList(), emptyList()))
|
|
153
|
+
return@suspendCancellableCoroutine
|
|
154
|
+
}
|
|
155
|
+
multiPermCallback = { result ->
|
|
156
|
+
if (cont.isActive) cont.resume(result)
|
|
157
|
+
}
|
|
158
|
+
cont.invokeOnCancellation { multiPermCallback = null }
|
|
159
|
+
multiPermLauncher.launch(permissions)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Request the background location permission (Android 10+).
|
|
164
|
+
*
|
|
165
|
+
* On Android 11+ (API 30+), the system does not show a runtime dialog for
|
|
166
|
+
* ACCESS_BACKGROUND_LOCATION. Instead, the user is redirected to App Settings
|
|
167
|
+
* where they can manually toggle "Allow all the time".
|
|
168
|
+
* This matches the native CrowdConnected SDK's BackgroundPermissions behavior.
|
|
169
|
+
*
|
|
170
|
+
* @return true if permission was granted, false otherwise
|
|
171
|
+
*/
|
|
172
|
+
suspend fun requestBackgroundPermission(): Boolean {
|
|
173
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
174
|
+
return true // background location not needed before Android 10
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
178
|
+
// API 30+: redirect to App Settings (system won't show a runtime dialog)
|
|
179
|
+
openAppSettingsAndCheck {
|
|
180
|
+
ContextCompat.checkSelfPermission(
|
|
181
|
+
requireContext(),
|
|
182
|
+
Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
|
183
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// API 29: standard permission dialog works
|
|
187
|
+
requestSinglePermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Request the user to enable Bluetooth via system dialog.
|
|
193
|
+
*
|
|
194
|
+
* @return true if Bluetooth was enabled, false if user declined
|
|
195
|
+
*/
|
|
196
|
+
suspend fun requestEnableBluetooth(): Boolean =
|
|
197
|
+
suspendCancellableCoroutine { cont ->
|
|
198
|
+
bluetoothCallback = { enabled ->
|
|
199
|
+
if (cont.isActive) cont.resume(enabled)
|
|
200
|
+
}
|
|
201
|
+
cont.invokeOnCancellation { bluetoothCallback = null }
|
|
202
|
+
@Suppress("MissingPermission")
|
|
203
|
+
bluetoothLauncher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Private helpers ---
|
|
207
|
+
|
|
208
|
+
private suspend fun requestSinglePermission(permission: String): Boolean =
|
|
209
|
+
suspendCancellableCoroutine { cont ->
|
|
210
|
+
singlePermCallback = { granted ->
|
|
211
|
+
if (cont.isActive) cont.resume(granted)
|
|
212
|
+
}
|
|
213
|
+
cont.invokeOnCancellation { singlePermCallback = null }
|
|
214
|
+
singlePermLauncher.launch(permission)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private suspend fun openAppSettingsAndCheck(isGranted: () -> Boolean): Boolean =
|
|
218
|
+
suspendCancellableCoroutine { cont ->
|
|
219
|
+
settingsCheck = isGranted
|
|
220
|
+
settingsCallback = { ok ->
|
|
221
|
+
if (cont.isActive) cont.resume(ok)
|
|
222
|
+
}
|
|
223
|
+
cont.invokeOnCancellation {
|
|
224
|
+
settingsCallback = null
|
|
225
|
+
settingsCheck = null
|
|
226
|
+
}
|
|
227
|
+
val intent = Intent(
|
|
228
|
+
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
|
229
|
+
Uri.fromParts("package", requireContext().packageName, null)
|
|
230
|
+
)
|
|
231
|
+
settingsLauncher.launch(intent)
|
|
232
|
+
}
|
|
233
|
+
}
|
package/android/src/main/java/com/expofpcrowdconnected/RCTCrowdConnectedLocationProviderModule.kt
CHANGED
|
@@ -2,11 +2,14 @@ package com.expofpcrowdconnected
|
|
|
2
2
|
|
|
3
3
|
import android.Manifest
|
|
4
4
|
import android.app.Application
|
|
5
|
+
import android.bluetooth.BluetoothManager
|
|
5
6
|
import android.content.Context
|
|
6
7
|
import android.content.pm.PackageManager
|
|
7
8
|
import android.os.Build
|
|
8
9
|
import android.util.Log
|
|
9
10
|
import androidx.core.content.ContextCompat
|
|
11
|
+
import androidx.fragment.app.FragmentActivity
|
|
12
|
+
import com.facebook.react.bridge.Arguments
|
|
10
13
|
import com.facebook.react.bridge.Promise
|
|
11
14
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
15
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
@@ -23,6 +26,7 @@ import kotlinx.coroutines.delay
|
|
|
23
26
|
import kotlinx.coroutines.launch
|
|
24
27
|
import kotlinx.coroutines.sync.Mutex
|
|
25
28
|
import kotlinx.coroutines.sync.withLock
|
|
29
|
+
import kotlinx.coroutines.withContext
|
|
26
30
|
import net.crowdconnected.android.core.CrowdConnected
|
|
27
31
|
import net.crowdconnected.android.core.StatusCallback
|
|
28
32
|
import net.crowdconnected.android.core.Configuration
|
|
@@ -30,6 +34,15 @@ import net.crowdconnected.android.core.ConfigurationBuilder
|
|
|
30
34
|
import net.crowdconnected.android.geo.GeoModule
|
|
31
35
|
import net.crowdconnected.android.ips.IPSModule
|
|
32
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Exception for permission denial with detailed information about denied permissions.
|
|
39
|
+
*/
|
|
40
|
+
class PermissionDeniedException(
|
|
41
|
+
message: String,
|
|
42
|
+
val denied: List<String> = emptyList(),
|
|
43
|
+
val permanentlyDenied: List<String> = emptyList()
|
|
44
|
+
) : Exception(message)
|
|
45
|
+
|
|
33
46
|
/**
|
|
34
47
|
* React Native module for CrowdConnected location provider
|
|
35
48
|
* Provides cross-platform API: setup, startUpdatingLocation, stopUpdatingLocation, isLocationUpdating
|
|
@@ -83,6 +96,12 @@ class RCTCrowdConnectedLocationProviderModule(
|
|
|
83
96
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
84
97
|
add(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
85
98
|
}
|
|
99
|
+
|
|
100
|
+
// POST_NOTIFICATIONS (Android 13+, API 33+) for foreground service notification
|
|
101
|
+
if (settings.isBackgroundUpdateEnabled &&
|
|
102
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
103
|
+
add(Manifest.permission.POST_NOTIFICATIONS)
|
|
104
|
+
}
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
return required.filter {
|
|
@@ -129,6 +148,8 @@ class RCTCrowdConnectedLocationProviderModule(
|
|
|
129
148
|
fun setup(settingsMap: ReadableMap, promise: Promise) {
|
|
130
149
|
moduleScope.launch {
|
|
131
150
|
try {
|
|
151
|
+
val wasUpdating: Boolean
|
|
152
|
+
|
|
132
153
|
mutex.withLock {
|
|
133
154
|
Log.d(TAG, "setup: Configuring CrowdConnected SDK")
|
|
134
155
|
|
|
@@ -139,21 +160,34 @@ class RCTCrowdConnectedLocationProviderModule(
|
|
|
139
160
|
Log.d(TAG, "setup: Navigation type: ${newSettings.navigationType}, Background: ${newSettings.isBackgroundUpdateEnabled}, Heading: ${newSettings.isHeadingEnabled}")
|
|
140
161
|
|
|
141
162
|
// If already updating, stop first
|
|
142
|
-
|
|
163
|
+
wasUpdating = providerInfo.isLocationUpdating
|
|
143
164
|
if (wasUpdating) {
|
|
144
165
|
Log.d(TAG, "setup: Already updating, stopping before restart")
|
|
145
166
|
stopUpdatingLocationInternal()
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
|
|
149
|
-
if (wasUpdating) {
|
|
150
|
-
Log.d(TAG, "setup: Restarting with new settings")
|
|
151
|
-
startUpdatingLocationInternal(promise)
|
|
152
|
-
} else {
|
|
169
|
+
if (!wasUpdating) {
|
|
153
170
|
Log.d(TAG, "setup: Configuration complete")
|
|
154
171
|
promise.resolve(providerInfo.toWritableMap())
|
|
155
172
|
}
|
|
156
173
|
}
|
|
174
|
+
|
|
175
|
+
// If was updating, restart with new settings
|
|
176
|
+
// Permission request runs OUTSIDE the mutex to avoid blocking
|
|
177
|
+
if (wasUpdating) {
|
|
178
|
+
Log.d(TAG, "setup: Restarting with new settings")
|
|
179
|
+
ensurePermissions()
|
|
180
|
+
ensureBluetooth()
|
|
181
|
+
mutex.withLock {
|
|
182
|
+
startUpdatingLocationInternal(promise)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch (e: PermissionDeniedException) {
|
|
186
|
+
val userInfo = Arguments.createMap().apply {
|
|
187
|
+
putArray("denied", Arguments.fromList(e.denied))
|
|
188
|
+
putArray("permanentlyDenied", Arguments.fromList(e.permanentlyDenied))
|
|
189
|
+
}
|
|
190
|
+
promise.reject("PERMISSIONS_DENIED", e.message, null as Throwable?, userInfo)
|
|
157
191
|
} catch (e: Exception) {
|
|
158
192
|
Log.e(TAG, "setup: Failed to configure SDK", e)
|
|
159
193
|
promise.reject("SETUP_ERROR", e.message, e)
|
|
@@ -168,9 +202,19 @@ class RCTCrowdConnectedLocationProviderModule(
|
|
|
168
202
|
fun startUpdatingLocation(promise: Promise) {
|
|
169
203
|
moduleScope.launch {
|
|
170
204
|
try {
|
|
205
|
+
// Request permissions OUTSIDE the mutex to avoid blocking
|
|
206
|
+
// other module operations while waiting for user interaction
|
|
207
|
+
ensurePermissions()
|
|
208
|
+
ensureBluetooth()
|
|
171
209
|
mutex.withLock {
|
|
172
210
|
startUpdatingLocationInternal(promise)
|
|
173
211
|
}
|
|
212
|
+
} catch (e: PermissionDeniedException) {
|
|
213
|
+
val userInfo = Arguments.createMap().apply {
|
|
214
|
+
putArray("denied", Arguments.fromList(e.denied))
|
|
215
|
+
putArray("permanentlyDenied", Arguments.fromList(e.permanentlyDenied))
|
|
216
|
+
}
|
|
217
|
+
promise.reject("PERMISSIONS_DENIED", e.message, null as Throwable?, userInfo)
|
|
174
218
|
} catch (e: Exception) {
|
|
175
219
|
promise.reject("START_ERROR", e.message, e)
|
|
176
220
|
}
|
|
@@ -206,6 +250,110 @@ class RCTCrowdConnectedLocationProviderModule(
|
|
|
206
250
|
}
|
|
207
251
|
}
|
|
208
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Ensures all required permissions are granted, requesting them from the user if necessary.
|
|
255
|
+
* Must be called OUTSIDE the mutex to avoid blocking other module operations
|
|
256
|
+
* while waiting for user interaction.
|
|
257
|
+
*
|
|
258
|
+
* @throws PermissionDeniedException if permissions are denied
|
|
259
|
+
* @throws IllegalStateException if settings are missing or activity is unavailable
|
|
260
|
+
*/
|
|
261
|
+
private suspend fun ensurePermissions() {
|
|
262
|
+
val currentSettings = settings
|
|
263
|
+
?: throw IllegalStateException("Settings are missing. Use 'setup' method first.")
|
|
264
|
+
|
|
265
|
+
val missing = missingPermissions(reactApplicationContext, currentSettings)
|
|
266
|
+
if (missing.isEmpty()) return
|
|
267
|
+
|
|
268
|
+
Log.d(TAG, "ensurePermissions: Missing permissions: ${missing.joinToString(", ")}. Requesting...")
|
|
269
|
+
|
|
270
|
+
val activity = reactApplicationContext.currentActivity as? FragmentActivity
|
|
271
|
+
?: throw IllegalStateException(
|
|
272
|
+
"Missing required permissions: ${missing.joinToString(", ")}. Cannot auto-request: no activity available."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
val bgPermission = Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
|
276
|
+
val needBg = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && missing.contains(bgPermission)
|
|
277
|
+
val foregroundPerms = missing.filterNot { it == bgPermission }
|
|
278
|
+
|
|
279
|
+
val requester = withContext(Dispatchers.Main) {
|
|
280
|
+
PermissionRequester.obtain(activity)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 1. Request foreground permissions first
|
|
284
|
+
if (foregroundPerms.isNotEmpty()) {
|
|
285
|
+
val result = withContext(Dispatchers.Main) {
|
|
286
|
+
requester.requestPermissions(foregroundPerms.toTypedArray())
|
|
287
|
+
}
|
|
288
|
+
if (!result.granted) {
|
|
289
|
+
throw PermissionDeniedException(
|
|
290
|
+
"Permissions denied: ${result.denied.joinToString(", ")}",
|
|
291
|
+
denied = result.denied,
|
|
292
|
+
permanentlyDenied = result.permanentlyDenied
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 2. Request background permission separately (Android requires this)
|
|
298
|
+
// On API 30+, this redirects the user to App Settings
|
|
299
|
+
if (needBg) {
|
|
300
|
+
val bgGranted = withContext(Dispatchers.Main) {
|
|
301
|
+
requester.requestBackgroundPermission()
|
|
302
|
+
}
|
|
303
|
+
if (!bgGranted) {
|
|
304
|
+
Log.w(TAG, "ensurePermissions: Background location permission denied")
|
|
305
|
+
throw PermissionDeniedException(
|
|
306
|
+
"Background location not granted",
|
|
307
|
+
denied = listOf(bgPermission),
|
|
308
|
+
permanentlyDenied = emptyList()
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Final safety check — verify all permissions are actually granted
|
|
314
|
+
val stillMissing = missingPermissions(reactApplicationContext, currentSettings)
|
|
315
|
+
if (stillMissing.isNotEmpty()) {
|
|
316
|
+
throw PermissionDeniedException(
|
|
317
|
+
"Required permissions still not granted: ${stillMissing.joinToString(", ")}",
|
|
318
|
+
denied = stillMissing,
|
|
319
|
+
permanentlyDenied = emptyList()
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Checks if Bluetooth is supported but disabled, and prompts the user to enable it.
|
|
326
|
+
* This is non-blocking — if the user declines, SDK start proceeds anyway.
|
|
327
|
+
* Matches native CrowdConnected SDK behavior (BluetoothEnableGateway).
|
|
328
|
+
*/
|
|
329
|
+
private suspend fun ensureBluetooth() {
|
|
330
|
+
val currentSettings = settings ?: return
|
|
331
|
+
if (!currentSettings.isBluetoothEnabled) return
|
|
332
|
+
|
|
333
|
+
val context = reactApplicationContext
|
|
334
|
+
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) return
|
|
335
|
+
|
|
336
|
+
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
337
|
+
if (bluetoothManager?.adapter?.isEnabled == true) return
|
|
338
|
+
|
|
339
|
+
val activity = reactApplicationContext.currentActivity as? FragmentActivity ?: return
|
|
340
|
+
|
|
341
|
+
Log.d(TAG, "ensureBluetooth: Bluetooth is disabled, requesting user to enable")
|
|
342
|
+
try {
|
|
343
|
+
val requester = withContext(Dispatchers.Main) {
|
|
344
|
+
PermissionRequester.obtain(activity)
|
|
345
|
+
}
|
|
346
|
+
val enabled = withContext(Dispatchers.Main) {
|
|
347
|
+
requester.requestEnableBluetooth()
|
|
348
|
+
}
|
|
349
|
+
if (!enabled) {
|
|
350
|
+
Log.w(TAG, "ensureBluetooth: User declined to enable Bluetooth")
|
|
351
|
+
}
|
|
352
|
+
} catch (e: Exception) {
|
|
353
|
+
Log.w(TAG, "ensureBluetooth: Failed to request Bluetooth enable", e)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
209
357
|
/**
|
|
210
358
|
* Internal method to start location updates (must be called within mutex lock)
|
|
211
359
|
*/
|
|
@@ -223,7 +371,7 @@ class RCTCrowdConnectedLocationProviderModule(
|
|
|
223
371
|
return
|
|
224
372
|
}
|
|
225
373
|
|
|
226
|
-
// Check permissions
|
|
374
|
+
// Check permissions (should already be granted by ensurePermissions called before mutex)
|
|
227
375
|
val missing = missingPermissions(reactApplicationContext, currentSettings)
|
|
228
376
|
if (missing.isNotEmpty()) {
|
|
229
377
|
Log.e(TAG, "startUpdatingLocationInternal: Missing permissions: ${missing.joinToString(", ")}")
|
|
@@ -185,10 +185,6 @@ class RCTCrowdConnectedLocationProvider: RCTEventEmitter, CLLocationManagerDeleg
|
|
|
185
185
|
self?.clLocationManager.startUpdatingHeading()
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
if settings.trackingMode.isAllowedInBackground {
|
|
189
|
-
self?.ccLocationManager.activateSDKBackgroundRefresh()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
188
|
settings.aliases.forEach(CrowdConnected.shared.setAlias)
|
|
193
189
|
(try? self?.providerInfo.toDict()).map(resolve)
|
|
194
190
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/react-native-efp-crowdconnected",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "React Native wrapper for ExpoFP around CrowdConnected SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
59
59
|
"prepare": "bob build",
|
|
60
60
|
"release": "release-it --only-version",
|
|
61
|
+
"release:ci": "release-it --ci",
|
|
61
62
|
"test": "jest",
|
|
62
63
|
"typecheck": "tsc"
|
|
63
64
|
},
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
"react": "19.0.0",
|
|
90
91
|
"react-native": "0.79.6",
|
|
91
92
|
"react-native-builder-bob": "^0.40.13",
|
|
92
|
-
"release-it": "^19.0.
|
|
93
|
+
"release-it": "^19.0.6",
|
|
93
94
|
"turbo": "^2.5.6",
|
|
94
95
|
"typescript": "^5.9.2"
|
|
95
96
|
},
|
|
@@ -151,4 +152,4 @@
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
|
-
}
|
|
155
|
+
}
|