Skip to main content

Common Issues

This page covers the most frequently encountered issues when integrating the AFCore SDK, along with their symptoms, causes, and solutions.

1. "AFCore has not been initialized"

Symptoms: App crashes with IllegalStateException or logs an error message stating that AFCore has not been initialized.

Cause: A feature module (e.g., AFCore.activities(), AFCore.profile()) was accessed before AFCore.initialize() was called.

Solution: Ensure initialize() is called in your application entry point before any other AFCore usage.

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()

// Initialize AFCore FIRST — before any Activity or Service can use it
AFCore.initialize(
AFCoreConfig.Builder(context = this)
.baseUrl("https://api.example.com")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.build()
)
}
}

Common pitfalls:

  • Calling AFCore.profile() in a ContentProvider that runs before Application.onCreate() (Android)
  • Accessing AFCore from a SwiftUI view init() that runs before the app's init() (iOS)
  • Using AFCore in a background service that starts before the application class initializes

2. "AFCore is already initialized with a different configuration"

Symptoms: An exception is thrown on the second call to AFCore.initialize().

Cause: initialize() was called twice with different AFCoreConfig values. AFCore is a singleton and cannot be re-initialized with a different configuration.

Solution: Call initialize() only once. If you need to change configuration at runtime, use reconfigure().

// If you need to switch environments:
val newConfig = AFCoreConfig.Builder(context = applicationContext)
.baseUrl("https://api-v2.example.com")
.clientId("new-client-id")
.clientSecret("new-client-secret")
.build()

AFCore.reconfigure(newConfig) // Safe — atomically swaps configuration

See the Initialization guide for details on reconfigure().


3. Session Expires Unexpectedly

Symptoms: The user is unexpectedly logged out. The sessionState flow emits SESSION_EXPIRED.

Cause: The SDK's automatic token refresh failed. This can happen due to:

  • No network connectivity during the refresh window
  • Incorrect clientId or clientSecret
  • Server-side token revocation
  • Exponential backoff exhausted (1s, 2s, 4s, 8s, 16s, 30s)

Diagnosis:

  1. Enable debug logging to see token refresh attempts:
AFCoreConfig.Builder(context = this)
.enableLogging(true)
.logLevel(AFLogLevel.DEBUG)
.build()
  1. Look for log messages containing TokenManager or backoff to see refresh attempts and their results.

  2. Check for 401 or 403 HTTP status codes in the logs, which indicate credential issues.

Solution:

  • Verify clientId and clientSecret are correct
  • Ensure the device has network connectivity
  • Observe sessionState and prompt the user to re-login on SESSION_EXPIRED
  • Do not implement your own token refresh logic -- the SDK handles this automatically

4. Geofences Not Triggering

Symptoms: The app does not receive geofence entry/exit events. The user visits a facility but no automatic visit is recorded.

Cause: Several factors can prevent geofence triggers:

CauseSolution
Missing ACCESS_BACKGROUND_LOCATION permission (Android)Request the permission at runtime; check AFGeofencing.getStatus()
Location services disabledPrompt the user to enable location in system settings
Radius too small (< 100m)Use a radius of at least 100 meters for reliable triggers
GPS not available indoorsMove near windows or doors; Wi-Fi-based location may help
Too many geofences registered (OS limit)Android allows 100, iOS allows 20 per app
Battery saver / low-power modeOS may throttle location updates aggressively

Diagnosis:

lifecycleScope.launch {
val status = AFCore.geofencing().getStatus()
Log.d("Geofence", "Is monitoring: ${status.isMonitoring}")
Log.d("Geofence", "Has permission: ${status.hasPermission}")
Log.d("Geofence", "Location enabled: ${status.isLocationEnabled}")
Log.d("Geofence", "Registered count: ${status.registeredGeofences}")
}

5. SmartWalking Sync Not Working

Symptoms: Step data is not being submitted to the server. The activity dashboard shows no SmartWalking entries despite the user walking.

Cause: Common causes include:

CauseSolution
Health permissions not grantedRequest HealthKit (iOS) or Health Connect (Android) permissions
Auto-sync not enabledCall enableAutoSync() after initialization
No network connectivitySync will retry automatically when network is available
Battery too low (Android)WorkManager defers work when battery is critically low

Diagnosis:

lifecycleScope.launch {
val diagnostics = AFCore.smartWalking().getDiagnostics()
Log.d("SW", "Auto-sync enabled: ${diagnostics.isAutoSyncEnabled}")
Log.d("SW", "Health permission: ${diagnostics.hasHealthPermission}")
Log.d("SW", "Last sync: ${diagnostics.lastSyncTime}")
Log.d("SW", "Last sync result: ${diagnostics.lastSyncResult}")
}

6. HealthKit Permissions Show LIKELY_DENIED

Symptoms: hasPermissions() returns LIKELY_DENIED for HealthKit data types, even though you believe the user granted permission.

Cause: iOS does not reveal the exact HealthKit authorization status for privacy reasons. When the SDK requests step data and receives no results, it reports LIKELY_DENIED. This does not necessarily mean the user explicitly denied permission -- they may have never been prompted, or they may have granted permission but have no step data for the requested period.

Solution:

  • Guide the user to Settings > Health > Data Access & Devices to verify the permission is granted
  • Ensure you are requesting data for a period when the user actually had step data
  • Call the permission request flow again -- iOS will show the prompt if the user has not yet responded
  • Note that LIKELY_DENIED is the SDK's best guess. There is no API on iOS to check HealthKit authorization state for read permissions definitively.

7. iOS Keychain Data Stale After Reinstall

Symptoms: After uninstalling and reinstalling the app, the SDK appears to have stale authentication tokens or user data from the previous installation.

Cause: iOS Keychain data persists across app uninstall/reinstall by default. The SDK uses a ReinstallSentinel mechanism to detect reinstalls and clear stale Keychain data, but it may not work if:

  • The Keychain accessibility is not set to kSecAttrAccessibleAfterFirstUnlock
  • The sentinel value was corrupted

Solution:

  • Verify that your app's Keychain accessibility is set to kSecAttrAccessibleAfterFirstUnlock
  • On first launch after reinstall, the SDK should automatically detect the reinstall and clear stale data
  • If the issue persists, call AFCore.authentication().logout() to clear all stored credentials

8. Android EncryptedSharedPreferences Crash

Symptoms: SecurityException or InvalidKeyException crash on Android when the SDK tries to read or write encrypted preferences.

Cause: This typically happens when:

  • The device was migrated from another device (Android backup/restore)
  • The device's secure hardware state changed (factory reset with data migration)
  • The Android Keystore master key was corrupted

Solution: Catch the exception and clear the corrupted preferences:

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()

try {
AFCore.initialize(
AFCoreConfig.Builder(context = this)
.baseUrl("https://api.example.com")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.build()
)
} catch (e: SecurityException) {
// Clear corrupted encrypted preferences
Log.e("AFCore", "EncryptedSharedPreferences corrupted, clearing", e)
getSharedPreferences("af_core_prefs", MODE_PRIVATE).edit().clear().apply()

// Retry initialization
AFCore.initialize(
AFCoreConfig.Builder(context = this)
.baseUrl("https://api.example.com")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.build()
)
}
}
}
caution

Clearing encrypted preferences will log the user out. They will need to re-authenticate.


9. Cache Returning Stale Data

Symptoms: API calls return outdated data even after the user performed an action that should have changed it (e.g., submitting a visit, updating profile).

Cause: The in-memory GET cache has a 15-minute TTL. If you fetch data, then mutate it via POST/PUT/DELETE, a subsequent GET within 15 minutes may return the cached (pre-mutation) response.

Solution:

  • Mutations auto-invalidate: POST, PUT, and DELETE requests always skip the cache. However, related GET endpoints are not automatically invalidated.
  • Use skipCache: After a mutation, use skipCache on the follow-up GET to force a fresh response.
// Submit a visit (POST — skips cache automatically)
AFCore.activities().submitAutomaticVisit(locationId, date)

// Fetch updated activities with cache bypass
val freshData = AFCore.activities().get(month = 2, year = 2026, skipCache = true)

10. Concurrent Requests Causing Duplicates

Symptoms: You suspect that making the same API call from multiple ViewModels or views simultaneously causes duplicate network requests or duplicate server-side records.

Cause: This is not an issue with AFCore. The SDK's request coalescing mechanism (via CompletableDeferred) automatically merges concurrent identical GET requests into a single network call. All callers receive the same response.

What to verify:

  • GET requests are always coalesced -- no action needed
  • POST requests are not coalesced (each call makes a separate request). If you are seeing duplicate submissions, ensure your UI prevents double-taps or multiple button presses

11. Token Refresh Loop

Symptoms: Logs show repeated token refresh attempts. The app seems stuck in a cycle of refreshing tokens.

Cause: The server is returning errors on token refresh, and the SDK is retrying with exponential backoff (1s, 2s, 4s, 8s, 16s, 30s cap).

Diagnosis:

  1. Enable debug logging and look for TokenManager log entries
  2. Check the HTTP status codes on refresh responses:
    • 401 or 403: Credentials are invalid -- the refresh token may have been revoked server-side
    • 500 or 503: Server issue -- the SDK will keep retrying with backoff
    • 429: Rate limited -- the SDK's backoff should resolve this

Solution:

  • The exponential backoff (capped at 30 seconds) prevents true infinite loops
  • If the session cannot be recovered, the SDK will eventually emit SESSION_EXPIRED on the sessionState flow
  • Listen for SESSION_EXPIRED and prompt the user to log in again
  • Do not implement your own retry logic on top of the SDK's built-in backoff

12. watchOS Features Not Working

Symptoms: Geofencing, barcode scanning, or proximity detection methods throw errors or are unavailable on watchOS.

Cause: These features are not supported on watchOS due to hardware and OS limitations:

FeaturewatchOS SupportReason
GeofencingNot supportedNo CLLocationManager region monitoring
Barcode scanningNot supportedNo camera
Proximity/BeaconNot supportedNo BLE beacon ranging
Background auto-syncNot supportedNo WorkManager or background delivery

Solution: Check feature availability before showing UI:

// watchOS: Check feature availability
let geofencingStatus = try await AFCore.geofencing().getStatus()
if geofencingStatus.isAvailable {
showGeofencingUI() // Won't be true on watchOS
}

let hasPermissions = try await AFCore.smartWalking().hasPermissions()
// SmartWalking works on watchOS, but only during foreground sessions

On watchOS, SmartWalking syncs only occur during foreground sessions. Schedule syncs early in the session to ensure data is transmitted before the session ends.