This sample demonstrates how to use the Fused Location Provider and Fused Orientation Provider to implement an ArcGIS Maps SDK Custom Location Data Source Location Provider.

Use case
The Fused Location Provider can provide more accurate location information than a single location provider. It uses GPS, Wi-Fi, and cell network data to determine the device’s location. In urban areas, it can also use 3D building data in urban areas to improve GPS accuracy. Similarly, the Fused Orientation Provider uses a fusion of magnetometer, accelerometer, and gyroscope data to provide a more accurate device orientation.
How to use the sample
Start the sample and allow the app to access your device’s location. The sample will display your location on the map. Use the priority and interval settings to change the location provider’s behavior. Note the change in the location display when changing these settings—namely the change in the rate at which the expanding blue ring animation triggers (which signifies an updated location).
How it works
- Implement the
CustomLocationDataSource.LocationProviderinterface overriding theheadingsandlocationsflows. - Create a
FusedLocationProviderClientandFusedOrientationProviderClientto get the device’s location and orientation. - Request location and orientation updates from the clients, then emit these values into the
locationsandheadingsflows. Utilize the functioncreateArcGISLocationFromFusedLocation(...)to convert a fusedLocationobject into anArcGISLocationobject. - Create a
LocationDisplaywithrememberLocationDisplay()and set it to the composableMapView. - Set the
LocationDisplaydata source to aCustomLocationDataSourcewhich implements theLocationProviderinterface.
Relevant API
- CustomLocationDataSource
- Location
- LocationDataSource
- LocationDisplay
- LocationProvider
Additional information
The fused location and orientation APIs are part of Google Play Services. The fused location provider intelligently combines different signals, such as GPS and Wi-Fi, to provide location information. The fused orientation provider is a new API that allows users to access orientation information on Android devices.
Tags
cell, fused, GPS, headings, locations, orientation, Wifi
Sample Code
/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.showdevicelocationusingfusedlocationdatasource
import android.Manifestimport android.os.Bundleimport android.widget.Toastimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.activity.result.contract.ActivityResultContractsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.showdevicelocationusingfusedlocationdatasource.screens.ShowDeviceLocationUsingFusedLocationDataSource
class MainActivity : ComponentActivity() {
private var isLocationPermissionGranted = false
private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { isLocationPermissionGranted = true } else { Toast.makeText(this, "Location permission is required to run this sample!", Toast.LENGTH_SHORT).show() } enableEdgeToEdge() setContent { SampleAppTheme { ShowDeviceLocationUsingFusedLocationDataSource() } } }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN) ArcGISEnvironment.applicationContext = applicationContext
requestLocationPermission() }
private fun requestLocationPermission() { requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }
@Composable private fun ShowDeviceLocationUsingFusedLocationDataSource() { Surface(color = MaterialTheme.colorScheme.background) { ShowDeviceLocationUsingFusedLocationDataSource( sampleName = getString(R.string.show_device_location_using_fused_location_data_source), locationPermissionGranted = isLocationPermissionGranted ) } }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.showdevicelocationusingfusedlocationdatasource.components
import android.annotation.SuppressLintimport android.content.Contextimport android.os.Looperimport android.util.Logimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.location.CustomLocationDataSourceimport com.arcgismaps.location.Locationimport com.google.android.gms.location.DeviceOrientationimport com.google.android.gms.location.DeviceOrientationListenerimport com.google.android.gms.location.DeviceOrientationRequestimport com.google.android.gms.location.FusedOrientationProviderClientimport com.google.android.gms.location.LocationCallbackimport com.google.android.gms.location.LocationRequestimport com.google.android.gms.location.LocationResultimport com.google.android.gms.location.LocationServicesimport com.google.android.gms.location.Priorityimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.Jobimport kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.MutableSharedFlowimport kotlinx.coroutines.flow.asSharedFlowimport kotlinx.coroutines.launchimport java.time.Instantimport java.util.concurrent.Executors
class FusedLocationAndOrientationProvider(applicationContext: Context) : CustomLocationDataSource.LocationProvider {
private val _headings = MutableSharedFlow<Double>() // Note the override property here, required to implement the LocationProvider interface override val headings: Flow<Double> = _headings.asSharedFlow()
private val _locations = MutableSharedFlow<Location>() // Note the override property here, required to implement the LocationProvider interface override val locations: Flow<Location> = _locations.asSharedFlow()
// Set up fused location provider states private var fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(applicationContext) private var locationCallback: LocationCallback? = null private var emitLocationsJob: Job? = null private var priority: Int = Priority.PRIORITY_HIGH_ACCURACY private var intervalInSeconds: Long = 1L
// Set up fused orientation provider states private var fusedOrientationProviderClient: FusedOrientationProviderClient = LocationServices.getFusedOrientationProviderClient(applicationContext) private var orientationListener: DeviceOrientationListener? = null private var emitHeadingsJob: Job? = null
/** * Pass changes in priority to the fused location provider. */ fun onPriorityChanged(priority: Int) { this.priority = priority startNewFusedLocationProvider(priority, intervalInSeconds) }
/** * Pass changes in interval to the fused location provider. */ fun onIntervalChanged(interval: Long) { this.intervalInSeconds = interval startNewFusedLocationProvider(priority, interval) }
/** * Start the fused location and orientation providers. */ fun start() { startNewFusedLocationProvider(priority, intervalInSeconds) startNewFusedOrientationProvider() }
/** * Stop the fused location and orientation providers. */ fun stop() { // Stop emitting locations into the locations flow emitLocationsJob?.cancel() locationCallback?.let { fusedLocationProviderClient.removeLocationUpdates(it) } // Stop emitting headings into the headings flow emitHeadingsJob?.cancel() orientationListener?.let { fusedOrientationProviderClient.removeOrientationUpdates(it) } }
/** * Create a location request with the given priority and interval. Create a callback to receive location updates * and then request location updates with the location request and callback. In the callback, emit the location and * heading updates into the override flows in the LocationProvider interface. */ @SuppressLint("MissingPermission") // Permission requests are handled in MainActivity private fun startNewFusedLocationProvider( priority: Int = Priority.PRIORITY_HIGH_ACCURACY, intervalInSeconds: Long = 1L ) { // Cancel any current jobs emitting into locations emitLocationsJob?.cancel()
// Clear any previous location updates locationCallback?.let { fusedLocationProviderClient.removeLocationUpdates(it) }
// Create a location request with the desired priority and interval val locationRequest = LocationRequest.Builder(priority, intervalInSeconds * 1000).build()
// Create a new location callback to emit location updates locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { locationResult.lastLocation?.let { fusedLocation -> emitLocationsJob = CoroutineScope(Dispatchers.IO).launch { // Emit the ArcGIS location object into the Location Provider's overridden locations flow _locations.emit( createArcGISLocationFromFusedLocation( fusedLocation = fusedLocation ) ) } } } } // Requests location updates with the given request and results delivered to the given listener on the specified // Looper locationCallback?.let { locationCallback -> fusedLocationProviderClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() ) } }
/** * Create a new fused orientation provider and request orientation updates. Emit the heading updates into the * override flow in the LocationProvider interface. */ private fun startNewFusedOrientationProvider() { // Cancel any current jobs emitting into locations emitHeadingsJob?.cancel()
// Create an FOP listener orientationListener = DeviceOrientationListener { orientation: DeviceOrientation -> emitHeadingsJob = CoroutineScope(Dispatchers.IO).launch { // Emit the fused orientation's heading into the Location Provider's overridden headings flow _headings.emit(orientation.headingDegrees.toDouble()) } }
// Create a new orientation request with the default request period. // Other DeviceOrientationRequest than OUTPUT_PERIOD_DEFAULT can be defined here. val orientationRequest = DeviceOrientationRequest.Builder(DeviceOrientationRequest.OUTPUT_PERIOD_DEFAULT).build()
// Register the request and listener orientationListener?.let { fusedOrientationProviderClient .requestOrientationUpdates(orientationRequest, Executors.newSingleThreadExecutor(), it) .addOnSuccessListener { Log.i( FusedLocationAndOrientationProvider::class.simpleName, "Registration Success" ) } .addOnFailureListener { error -> Log.e( FusedLocationAndOrientationProvider::class.simpleName, "Registration Failure: " + error.message ) } } }}
/** * Creates an ArcGIS Maps SDK Location object from a Fused Location object. */private fun createArcGISLocationFromFusedLocation(fusedLocation: android.location.Location): Location { return Location.create( position = Point( x = fusedLocation.longitude, y = fusedLocation.latitude, z = fusedLocation.altitude, SpatialReference.wgs84() ), horizontalAccuracy = fusedLocation.accuracy.toDouble(), verticalAccuracy = fusedLocation.verticalAccuracyMeters.toDouble(), speed = fusedLocation.speed.toDouble(), course = fusedLocation.bearing.toDouble(), // If the timestamp is more than 5 seconds old, set lastKnown to true lastKnown = (Instant.now().toEpochMilli() - fusedLocation.time) > 5000, timestamp = Instant.ofEpochMilli(fusedLocation.time), )}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.showdevicelocationusingfusedlocationdatasource.components
import android.app.Applicationimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.location.CustomLocationDataSourceimport com.arcgismaps.location.LocationDisplayAutoPanModeimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.view.LocationDisplayimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.launch
class ShowDeviceLocationUsingFusedLocationDataSourceViewModel(application: Application) : AndroidViewModel(application) {
// Create an ArcGIS map val arcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigation)
// Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()
// Create a fused location and fused orientation provider to get location and orientation updates from the fused // location and fused orientation APIs private val fusedLocationProvider = FusedLocationAndOrientationProvider(getApplication())
/** * Pass changes in priority to the fused location provider. */ fun onPriorityChanged(priority: Int) { fusedLocationProvider.onPriorityChanged(priority) }
/** * Pass changes in interval to the fused location provider. */ fun onIntervalChanged(interval: Long) { fusedLocationProvider.onIntervalChanged(interval) }
/** * Initialize the location display with a custom location data source using the fused location provider. */ fun initialize(locationDisplay: LocationDisplay) {
viewModelScope.launch { arcGISMap.load().onFailure { error -> messageDialogVM.showMessageDialog( "Failed to load map", error.message.toString() ) } }
// Set the location display to be used by this view model locationDisplay.apply { // Set the location display's data source to a Custom Location DataSource which implements a location // provider interface on the Fused Location API dataSource = CustomLocationDataSource { fusedLocationProvider } // Keep track of the job so it can be canceled elsewhere viewModelScope.launch { // Start the data source dataSource.start() // Start emitting fused locations into the data source fusedLocationProvider.start() } // Set the AutoPan mode to recenter around the location display setAutoPanMode(LocationDisplayAutoPanMode.CompassNavigation) } }
override fun onCleared() { super.onCleared() fusedLocationProvider.stop() }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.showdevicelocationusingfusedlocationdatasource.screens
import androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.wrapContentSizeimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Settingsimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.FloatingActionButtonimport androidx.compose.material3.Iconimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.ModalBottomSheetimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.rememberModalBottomSheetStateimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Alignment.Companion.CenterVerticallyimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplayimport com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBoximport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.showdevicelocationusingfusedlocationdatasource.components.ShowDeviceLocationUsingFusedLocationDataSourceViewModelimport com.google.android.gms.location.Priorityimport kotlin.math.roundToInt
/** * Main screen layout for the sample app */@OptIn(ExperimentalMaterial3Api::class)@Composablefun ShowDeviceLocationUsingFusedLocationDataSource(sampleName: String, locationPermissionGranted: Boolean) { val locationDisplay = rememberLocationDisplay() val mapViewModel: ShowDeviceLocationUsingFusedLocationDataSourceViewModel = viewModel() // On first composition, initialize the sample. var isViewmodelInitialized by remember { mutableStateOf(false) } LaunchedEffect(isViewmodelInitialized) { if (!isViewmodelInitialized && locationPermissionGranted) { mapViewModel.initialize(locationDisplay) isViewmodelInitialized = true } } // Set up the bottom sheet controls val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var showBottomSheet by remember { mutableStateOf(false) }
// list of configurable location priority modes val priorityModes = mapOf( Priority.PRIORITY_HIGH_ACCURACY to "High accuracy", Priority.PRIORITY_BALANCED_POWER_ACCURACY to "Balanced power accuracy", Priority.PRIORITY_LOW_POWER to "Low power", Priority.PRIORITY_PASSIVE to "Passive" ) var currentPriority by remember { mutableIntStateOf(priorityModes.keys.first()) } var currentInterval by remember { mutableFloatStateOf(1F) }
Scaffold(topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( modifier = Modifier .fillMaxSize() .padding(it), ) { Box { MapView( modifier = Modifier .fillMaxSize(), arcGISMap = mapViewModel.arcGISMap, locationDisplay = locationDisplay ) } // Show bottom sheet with override parameter options if (showBottomSheet) { ModalBottomSheet( modifier = Modifier.wrapContentSize(), onDismissRequest = { showBottomSheet = false }, sheetState = sheetState ) { DropDownMenuBox( modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 12.dp), textFieldLabel = "Priority", textFieldValue = priorityModes[currentPriority].toString(), dropDownItemList = priorityModes.values.toList(), onIndexSelected = { index -> currentPriority = priorityModes.keys.toList()[index] mapViewModel.onPriorityChanged(currentPriority) } ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = CenterVertically ) { Text(text = "Set desired interval for location updates:", style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 12.dp)) Text(text = currentInterval.roundToInt().toString() + " sec", modifier = Modifier.padding(end = 12.dp)) } Slider( value = currentInterval, onValueChange = { valueChanged -> currentInterval = valueChanged mapViewModel.onIntervalChanged(valueChanged.toLong()) }, valueRange = 0f..30f, modifier = Modifier.padding(start = 12.dp, end = 12.dp), steps = 29 ) } } }
mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } }, // Floating action button to show the parameter overrides bottom sheet floatingActionButton = { if (!showBottomSheet) { FloatingActionButton(modifier = Modifier.padding(bottom = 36.dp, end = 12.dp), onClick = { showBottomSheet = true }) { Icon( imageVector = Icons.Filled.Settings, contentDescription = "Show fused location options" ) } } })}