Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,10 @@ dependencies {
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

implementation 'com.expofp:common:4.11.0'
implementation 'com.expofp:fplan:4.11.0'
implementation 'com.expofp:fplan:5.1.0'

implementation 'com.expofp:crowdconnected:4.11.0'
// implementation 'com.expofp:crowdconnectedbackground:4.11.0'
implementation 'com.expofp:crowdconnected:5.1.0'
// implementation 'com.expofp:crowdconnectedbackground:5.1.0'
implementation 'net.crowdconnected.android.core:android-core:2.1.0'
implementation 'net.crowdconnected.android.ips:android-ips:2.1.0'
implementation 'net.crowdconnected.android.geo:android-geo:2.1.0'
Expand Down
17 changes: 3 additions & 14 deletions android/src/main/java/com/expofp/ExpofpModule.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
package com.expofp

import com.facebook.react.bridge.NativeModule
import android.util.Log
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.expofp.fplan.SharedFplanView
import com.expofp.fplan.models.Settings
import com.facebook.react.bridge.UiThreadUtil

class ExpofpModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName() = "ExpofpModule"

@ReactMethod
fun preload(url: String, promise: Promise) {
try {
val context = this.reactApplicationContext.applicationContext
UiThreadUtil.runOnUiThread {
SharedFplanView.preload(url, Settings(), context)
}
promise.resolve(null)
} catch (e: Exception) {
promise.reject("PRELOAD_ERROR", e.message)
}
Log.d("ExpofpModule", "preload: not implemented")
promise.reject("PRELOAD_UNAVAILABLE", "ExpoFP Android v5 preload is not implemented yet")
}
}
155 changes: 50 additions & 105 deletions android/src/main/java/com/expofp/ExpofpViewManager.kt
Original file line number Diff line number Diff line change
@@ -1,133 +1,78 @@
package com.expofp

import android.app.Application
import android.util.Log
import android.view.View
import com.expofp.common.GlobalLocationProvider
import com.expofp.crowdconnected.CrowdConnectedProvider
import com.expofp.crowdconnected.Mode
import com.expofp.fplan.FplanView
import com.expofp.fplan.models.FplanViewState
import com.expofp.fplan.api.app.ExpoFpPlan
import com.expofp.fplan.api.app.model.ExpoFpLinkType
import com.expofp.fplan.api.locationProvider.IExpoFpLocationProvider
import com.expofp.fplan.ui.ExpoFpView
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.expofp.R
import com.expofp.fplan.contracts.DownloadOfflinePlanCallback
import com.expofp.fplan.models.OfflinePlanInfo
import com.expofp.crowdconnected.ExpoFpCrowdConnectedLocationProvider
// import com.expofp.crowdconnectedbackground.ExpoFpCrowdConnectedBackgroundLocationProvider
import com.expofp.crowdconnected.ExpoFpCrowdConnectedLocationProviderSettings
import com.expofp.crowdconnected.ExpoFpCrowdConnectedNavigationType

class ExpofpViewManager : SimpleViewManager<View>() {
class ExpofpViewManager : SimpleViewManager<ExpoFpView>() {
private var reactContext: ThemedReactContext? = null

override fun getName() = "ExpofpView"

override fun createViewInstance(reactContext: ThemedReactContext): View {
override fun createViewInstance(reactContext: ThemedReactContext): ExpoFpView {
this.reactContext = reactContext
var view = FplanView(reactContext)

return view;
ExpoFpPlan.initialize(reactContext.applicationContext)
return ExpoFpView(reactContext)
}

override fun onDropViewInstance(view: View) {
(view as? FplanView)?.destroy()
override fun onDropViewInstance(view: ExpoFpView) {
super.onDropViewInstance(view)
}

private fun getExpoKeyFromUrl(url: String): String {
return url.substringAfter("https://").substringBefore(".expofp.com")
}

private fun openMapForUrl(view: FplanView, url: String) {
val expoKey = getExpoKeyFromUrl(url)
val settings = com.expofp.fplan.models.Settings().withGlobalLocationProvider()

val offlinePlanManager = FplanView.getOfflinePlanManager(reactContext)
val latestOfflinePlan = offlinePlanManager.allOfflinePlansFromCache
.filter { offlinePlanInfo -> offlinePlanInfo.expoKey == expoKey }
.maxByOrNull { offlinePlanInfo -> offlinePlanInfo.version }

if (latestOfflinePlan != null) {
Log.d("ExpofpModule", latestOfflinePlan.expoKey)
view.openOfflinePlan(latestOfflinePlan, "", settings)
return
}

val ctx = this.reactContext ?: run {
view.load(url, settings)
return
}

val am = ctx.assets
val cachePlanExists = try {
am.open("${expoKey}.zip").close()
true
} catch (e: Exception) {
false
}

if (cachePlanExists) {
try {
Log.d("ExpofpModule", "openZipFromAssets: ${expoKey}.zip")
view.openZipFromAssets("${expoKey}.zip", "", settings, ctx)
return
} catch (e: Exception) {
Log.d("ExpofpModule", "failed to open asset zip, loading url: $url")
view.load(url, settings)
return
}
}

Log.d("ExpofpModule", "asset zip not found, loading url: $url")
view.load(url, settings)
}

private fun triggerOfflinePlanDownload(expoKey: String) {
val offlinePlanManager = FplanView.getOfflinePlanManager(reactContext)
offlinePlanManager.downloadOfflinePlanToCache(expoKey, object : DownloadOfflinePlanCallback {
override fun onCompleted(offlinePlanInfo: OfflinePlanInfo) {
Log.d("ExpofpModule", "downloaded offline plan: ${offlinePlanInfo.expoKey} v${offlinePlanInfo.version}")
}

override fun onError(message: String) {
Log.e("ExpofpModule", "offline plan download failed: $message")
}
})
private fun createCrowdConnectedProvider(settingsMap: ReadableMap): IExpoFpLocationProvider? {
val context = reactContext?.applicationContext ?: return null
val application = (context as? Application) ?: return null

val cc = if (settingsMap.hasKey("crowdConnected")) settingsMap.getMap("crowdConnected") else settingsMap
if (cc == null) return null

val appKey = if (cc.hasKey("appKey")) cc.getString("appKey") else null
val token = if (cc.hasKey("token")) cc.getString("token") else null
val secret = if (cc.hasKey("secret")) cc.getString("secret") else null
if (appKey.isNullOrEmpty() || token.isNullOrEmpty() || secret.isNullOrEmpty()) return null
val aliases = mutableMapOf<String, String>()
aliases["onesignal_user_id"] = cc.getString("oneSignalUserId") ?: ""
val settings = ExpoFpCrowdConnectedLocationProviderSettings(
appKey = appKey,
token = token,
secret = secret,
navigationType = ExpoFpCrowdConnectedNavigationType.ALL,
isAllowedInBackground = false,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make background mode configurable from JavaScript.

The isAllowedInBackground property is hardcoded to false, preventing JavaScript code from controlling background location tracking. This limits flexibility and may break use cases that require background positioning.

Consider adding a React prop and backing field to expose this setting:

 class ExpofpViewManager : SimpleViewManager<ExpoFpView>() {
     private var reactContext: ThemedReactContext? = null
+    private var enableBackground: Boolean = false

Then update the provider creation:

         val settings = ExpoFpCrowdConnectedLocationProviderSettings(
             appKey = appKey,
             token = token,
             secret = secret,
             navigationType = ExpoFpCrowdConnectedNavigationType.ALL,
-            isAllowedInBackground = false,
+            isAllowedInBackground = enableBackground,
             isHeadingEnabled = true,
             aliases = aliases,
             notificationText = "Indoor navigation is active",
             serviceIcon = R.drawable.placeholder_icon
         )

And add a setter:

    @ReactProp(name = "enableBackground")
    fun setEnableBackground(view: ExpoFpView, value: Boolean) {
        enableBackground = value
    }
🤖 Prompt for AI Agents
In android/src/main/java/com/expofp/ExpofpViewManager.kt around line 54,
isAllowedInBackground is hardcoded to false which prevents JS from toggling
background location; add a private backing field (e.g. enableBackground: Boolean
= false) to the ViewManager, expose a React prop name (e.g.
"enableBackground"/"enableBackgroundLocation") and implement a @ReactProp setter
that updates the backing field, then use that backing field when
constructing/creating the provider (replace the hardcoded false with
enableBackground) so the JS prop controls background mode.

isHeadingEnabled = true,
aliases = aliases,
notificationText = "Indoor navigation is active",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not localized

serviceIcon = R.drawable.placeholder_icon
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace the placeholder icon with a production asset.

Using R.drawable.placeholder_icon in production code can result in a poor user experience if the placeholder is visually unappealing or confusing.

🤖 Prompt for AI Agents
In android/src/main/java/com/expofp/ExpofpViewManager.kt around line 58, the
code sets serviceIcon = R.drawable.placeholder_icon which is a temporary asset;
replace this with the correct production drawable resource (e.g.,
R.drawable.service_icon_prod) by adding the final asset files into the
appropriate res/drawable(-density) folders, update the reference here to the
production resource name, ensure naming follows Android resource conventions,
and verify the image renders correctly across screen densities and themes.

)

// ExpoFpCrowdConnectedBackgroundLocationProvider(application, settings)
return ExpoFpCrowdConnectedLocationProvider(application, settings)
}

@ReactProp(name = "settings")
fun setSettings(view: FplanView, settingsMap: ReadableMap?) {
println("setSettings: $settingsMap")
settingsMap?.let {
var appKey = settingsMap.getString("appKey")
val token = settingsMap.getString("token")
val secret = settingsMap.getString("secret")
if (appKey != null && token != null && secret != null) {
val context = reactContext?.applicationContext ?: return
val application = context as? Application ?: return
val aliases = mutableMapOf<String, String>()
aliases["onesignal_user_id"] = it.getString("oneSignalUserId") ?: ""
val lpSettings = com.expofp.crowdconnected.Settings(
settingsMap.getString("appKey") ?: "",
settingsMap.getString("token") ?: "",
settingsMap.getString("secret") ?: "",
Mode.IPS_AND_GPS,
true,
aliases
)
lpSettings.setServiceNotificationInfo("Background Location is running", R.drawable.placeholder_icon);

val locationProvider = CrowdConnectedProvider(application, lpSettings)
// val locationProvider = CrowdConnectedBackgroundProvider(application, lpSettings)
GlobalLocationProvider.init(locationProvider)
GlobalLocationProvider.start()
}
if (view.state.equals(FplanViewState.Created)) {
val url = it.getString("url") ?: ""
val expoKey = getExpoKeyFromUrl(url)
fun setSettings(view: ExpoFpView, settingsMap: ReadableMap?) {
if (settingsMap == null) return
val url = settingsMap.getString("url") ?: return
val context = reactContext?.applicationContext ?: return
val expoKey = getExpoKeyFromUrl(url)

openMapForUrl(view, url)
triggerOfflinePlanDownload(expoKey)
}
}
ExpoFpPlan.initialize(context)
val p = ExpoFpPlan.createPlanPresenter(planLink = ExpoFpLinkType.ExpoKey(expoKey))
val ccProvider = createCrowdConnectedProvider(settingsMap)
if (ccProvider != null) p.setLocationProvider(ccProvider)
view.attachPresenter(p)
}
}