Skip to content
Draft
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
79 changes: 79 additions & 0 deletions location/api/location.api
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,93 @@ public final class io/customer/location/ModuleLocation : io/customer/sdk/core/mo
public static final field Companion Lio/customer/location/ModuleLocation$Companion;
public static final field MODULE_NAME Ljava/lang/String;
public fun <init> (Lio/customer/location/LocationModuleConfig;)V
public final fun canTrackLocation ()Z
public fun getModuleConfig ()Lio/customer/location/LocationModuleConfig;
public synthetic fun getModuleConfig ()Lio/customer/sdk/core/module/CustomerIOModuleConfig;
public fun getModuleName ()Ljava/lang/String;
public final fun getRequiredPermissions ()[Ljava/lang/String;
public final fun getTrackingEligibility ()Lio/customer/location/consent/LocationTrackingEligibility;
public fun initialize ()V
public static final fun instance ()Lio/customer/location/ModuleLocation;
public final fun isLocationServicesEnabled ()Z
public final fun isLocationTrackingEnabled ()Z
public final fun onPermissionResult ()V
public final fun permissionStatus ()Lio/customer/location/permission/LocationPermissionStatus;
public final fun setLocationTrackingEnabled (Z)V
}

public final class io/customer/location/ModuleLocation$Companion {
public final fun instance ()Lio/customer/location/ModuleLocation;
}

public abstract class io/customer/location/consent/LocationTrackingEligibility {
}

public final class io/customer/location/consent/LocationTrackingEligibility$Eligible : io/customer/location/consent/LocationTrackingEligibility {
public static final field INSTANCE Lio/customer/location/consent/LocationTrackingEligibility$Eligible;
}

public final class io/customer/location/consent/LocationTrackingEligibility$LocationServicesDisabled : io/customer/location/consent/LocationTrackingEligibility {
public static final field INSTANCE Lio/customer/location/consent/LocationTrackingEligibility$LocationServicesDisabled;
}

public final class io/customer/location/consent/LocationTrackingEligibility$NotEnabled : io/customer/location/consent/LocationTrackingEligibility {
public static final field INSTANCE Lio/customer/location/consent/LocationTrackingEligibility$NotEnabled;
}

public final class io/customer/location/consent/LocationTrackingEligibility$PermissionRequired : io/customer/location/consent/LocationTrackingEligibility {
public static final field INSTANCE Lio/customer/location/consent/LocationTrackingEligibility$PermissionRequired;
}

public abstract class io/customer/location/error/LocationError {
}

public final class io/customer/location/error/LocationError$LocationServicesDisabled : io/customer/location/error/LocationError {
public static final field INSTANCE Lio/customer/location/error/LocationError$LocationServicesDisabled;
public fun toString ()Ljava/lang/String;
}

public final class io/customer/location/error/LocationError$PermissionDenied : io/customer/location/error/LocationError {
public static final field INSTANCE Lio/customer/location/error/LocationError$PermissionDenied;
public fun toString ()Ljava/lang/String;
}

public final class io/customer/location/error/LocationError$ServiceUnavailable : io/customer/location/error/LocationError {
public static final field INSTANCE Lio/customer/location/error/LocationError$ServiceUnavailable;
public fun toString ()Ljava/lang/String;
}

public final class io/customer/location/error/LocationError$Timeout : io/customer/location/error/LocationError {
public static final field INSTANCE Lio/customer/location/error/LocationError$Timeout;
public fun toString ()Ljava/lang/String;
}

public final class io/customer/location/error/LocationError$TrackingDisabled : io/customer/location/error/LocationError {
public static final field INSTANCE Lio/customer/location/error/LocationError$TrackingDisabled;
public fun toString ()Ljava/lang/String;
}

public final class io/customer/location/error/LocationError$Unknown : io/customer/location/error/LocationError {
public fun <init> ()V
public fun <init> (Ljava/lang/Throwable;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/Throwable;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/Throwable;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/Throwable;Ljava/lang/String;)Lio/customer/location/error/LocationError$Unknown;
public static synthetic fun copy$default (Lio/customer/location/error/LocationError$Unknown;Ljava/lang/Throwable;Ljava/lang/String;ILjava/lang/Object;)Lio/customer/location/error/LocationError$Unknown;
public fun equals (Ljava/lang/Object;)Z
public final fun getCause ()Ljava/lang/Throwable;
public final fun getMessage ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/customer/location/permission/LocationPermissionStatus : java/lang/Enum {
public static final field AUTHORIZED Lio/customer/location/permission/LocationPermissionStatus;
public static final field DENIED Lio/customer/location/permission/LocationPermissionStatus;
public static final field NOT_DETERMINED Lio/customer/location/permission/LocationPermissionStatus;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lio/customer/location/permission/LocationPermissionStatus;
public static fun values ()[Lio/customer/location/permission/LocationPermissionStatus;
}

161 changes: 157 additions & 4 deletions location/src/main/kotlin/io/customer/location/ModuleLocation.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package io.customer.location

import io.customer.location.consent.LocationTrackingEligibility
import io.customer.location.di.LocationComponent
import io.customer.location.permission.LocationPermissionStatus
import io.customer.sdk.communication.Event
import io.customer.sdk.communication.subscribe
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.core.module.CustomerIOModule

Expand All @@ -8,26 +13,174 @@ import io.customer.sdk.core.module.CustomerIOModule
*
* This module provides location tracking capabilities including:
* - Permission and consent management
* - One-shot location capture
* - Continuous location tracking
* - One-shot location capture (trackOnce)
* - Continuous location tracking (start/stop)
* - Automatic location tracking on initialization and app lifecycle
*
* ## Usage
*
* ```kotlin
* // Get the location module instance
* val location = ModuleLocation.instance()
*
* // Enable location tracking
* location.setLocationTrackingEnabled(enabled = true)
*
* // Check permission status
* when (location.permissionStatus()) {
* LocationPermissionStatus.NOT_DETERMINED -> // Request permission
* LocationPermissionStatus.DENIED -> // Guide user to settings
* LocationPermissionStatus.AUTHORIZED -> // Can track location
* }
* ```
*
* ## Cross-Platform API
*
* This module exposes a unified API that is consistent across Android and iOS,
* enabling wrapper SDKs (React Native, Flutter, Expo) to use a single interface.
* Platform-specific details are handled internally.
*/
class ModuleLocation(
config: LocationModuleConfig
) : CustomerIOModule<LocationModuleConfig> {
override val moduleName: String = MODULE_NAME
override val moduleConfig: LocationModuleConfig = config

private val logger = SDKComponent.logger
private val eventBus = SDKComponent.eventBus
private val trackingEligibilityChecker by lazy { LocationComponent.trackingEligibilityChecker }
private val permissionsHelper by lazy { LocationComponent.permissionsHelper }

override fun initialize() {
// Module initialization will be implemented in future PRs
logger.debug("Location module initialized")

eventBus.subscribe<Event.ResetEvent> {
logger.debug("Resetting location module state")
LocationComponent.reset()
}
}

// region Public API - Cross-Platform

/**
* Gets the current location permission status.
*
* This method only checks the current state - it does NOT request permissions.
* The app is responsible for requesting permissions through platform-specific APIs.
*
* @return The current [LocationPermissionStatus]
*/
fun permissionStatus(): LocationPermissionStatus {
val context = SDKComponent.android().applicationContext
return permissionsHelper.getPermissionStatus(context)
}

/**
* Checks if device location services are enabled.
*
* If this returns false, the user needs to enable location services
* in their device settings before location tracking can work.
*
* @return true if location services are enabled, false otherwise
*/
fun isLocationServicesEnabled(): Boolean {
val context = SDKComponent.android().applicationContext
return permissionsHelper.locationServicesEnabled(context)
}

/**
* Sets whether location tracking is enabled.
*
* Location tracking requires both opt-in AND permission.
* Call this method to enable/disable location tracking from your app.
*
* @param enabled true to enable tracking, false to disable
*/
fun setLocationTrackingEnabled(enabled: Boolean) {
trackingEligibilityChecker.isTrackingEnabled = enabled
logger.debug("Location tracking enabled: $enabled")
}

/**
* Checks if location tracking is enabled.
*
* @return true if tracking is enabled, false otherwise
*/
fun isLocationTrackingEnabled(): Boolean {
return trackingEligibilityChecker.isTrackingEnabled
}

/**
* Checks if location tracking is currently allowed.
*
* Returns true only if:
* 1. Tracking has been enabled via [setLocationTrackingEnabled]
* 2. Location permission is granted
* 3. Device location services are enabled
*
* @return true if location tracking can proceed, false otherwise
*/
fun canTrackLocation(): Boolean {
return trackingEligibilityChecker.canTrackLocation()
}

/**
* Gets the current tracking eligibility status.
*
* Useful for providing user feedback about why tracking is disabled.
*
* @return The current [LocationTrackingEligibility] status
*/
fun getTrackingEligibility(): LocationTrackingEligibility {
return trackingEligibilityChecker.getTrackingEligibility()
}

// endregion

// region Public API - Android-Specific

/**
* Gets the list of permissions required for location tracking.
*
* This is an Android-specific helper for requesting permissions.
* Use with `ActivityCompat.requestPermissions()`.
*
* @return Array of permission strings (ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
*/
fun getRequiredPermissions(): Array<String> {
return permissionsHelper.getRequiredPermissions()
}

/**
* Call this after requesting location permissions from the user.
*
* This helps the SDK distinguish between [LocationPermissionStatus.NOT_DETERMINED]
* (never asked) and [LocationPermissionStatus.DENIED] (user explicitly denied).
*
* Example:
* ```kotlin
* ActivityCompat.requestPermissions(activity, location.getRequiredPermissions(), REQ_CODE)
* location.onPermissionResult() // Call after requesting
* ```
*/
fun onPermissionResult() {
permissionsHelper.markPermissionRequested()
}

// endregion

companion object {
const val MODULE_NAME: String = "Location"

/**
* Gets the singleton instance of the Location module.
*
* @throws IllegalStateException if the module has not been initialized
*/
@JvmStatic
fun instance(): ModuleLocation {
return SDKComponent.modules[MODULE_NAME] as? ModuleLocation
?: throw IllegalStateException("ModuleLocation not initialized")
?: throw IllegalStateException("ModuleLocation not initialized. Make sure to add the location module when initializing CustomerIO.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.customer.location.consent

/**
* Represents the current eligibility status for location tracking.
*
* This sealed class provides a unified way to understand why location
* tracking may or may not be available, enabling wrapper SDKs to
* provide appropriate user feedback.
*/
sealed class LocationTrackingEligibility {
/**
* Location tracking is eligible - tracking enabled, permission granted, services enabled.
*/
object Eligible : LocationTrackingEligibility()

/**
* Location tracking has not been enabled via [ModuleLocation.setLocationTrackingEnabled].
* The app needs to call setTrackingEnabled(true) to enable tracking.
*/
object NotEnabled : LocationTrackingEligibility()

/**
* Location permission has not been granted by the user.
* The app needs to request location permission from the user.
*/
object PermissionRequired : LocationTrackingEligibility()

/**
* Device location services are disabled in system settings.
* The user needs to enable location services in their device settings.
*/
object LocationServicesDisabled : LocationTrackingEligibility()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.customer.location.di

import io.customer.location.permission.LocationPermissionsHelper
import io.customer.location.store.LocationPreferenceStore
import io.customer.location.tracking.TrackingEligibilityChecker
import io.customer.sdk.core.di.DiGraph
import io.customer.sdk.core.di.SDKComponent

/**
* Dependency injection component for the Location module.
*
* Provides singleton instances of location-related dependencies
* that can be accessed throughout the module.
*/
internal object LocationComponent : DiGraph() {

private val androidComponent
get() = SDKComponent.android()

private val applicationContext
get() = androidComponent.applicationContext

/**
* Preference store for location module settings.
*/
val preferenceStore: LocationPreferenceStore
get() = singleton { LocationPreferenceStore(applicationContext) }

/**
* Permission helper for checking location permission status.
*/
val permissionsHelper: LocationPermissionsHelper
get() = singleton { LocationPermissionsHelper(preferenceStore) }

/**
* Checker for determining if location tracking is eligible.
*/
val trackingEligibilityChecker: TrackingEligibilityChecker
get() = singleton { TrackingEligibilityChecker(applicationContext, preferenceStore, permissionsHelper) }

/**
* Resets all location component dependencies.
* Called when the SDK is reset.
*/
override fun reset() {
preferenceStore.clearAll()
super.reset()
}
}
Loading
Loading