Location Sharing

Introduction

Stream’s Chat SDK allows users to share their location with other members of a channel.

This guide demonstrates how to implement a location-sharing feature where users can send their location to a chat channel and display it using a custom attachment component with maps.

Location PickerStatic LocationLive Location
Location Picker
Static Location
Live Location

The implementation uses Jetpack Compose for the UI and the Stream Chat Android SDK for chat functionality.

Prerequisites

  • Stream Chat SDK: Ensure you have integrated the Stream Chat Compose SDK into your project. Follow the Compose In-App Messaging Tutorial for setup instructions.
  • Permissions: Add location permissions to your app.
  • Dependencies: Include the necessary dependencies for location services.

1: Set Up Dependencies

Add the following dependencies to your app/build.gradle file to enable Stream Chat and location services:

dependencies {
    // Stream Chat SDK
    implementation("io.getstream:stream-chat-android-compose:$stream_version")
    implementation("io.getstream:stream-chat-android-offline:$stream_version")

    // Google Play Services for location
    implementation("com.google.android.gms:play-services-location:$play_services_version")
}

Replace $stream_version with the latest version from the Stream Chat Android GitHub releases.

Sync your project to ensure dependencies are resolved.

2: Request Location Permissions

To access the user’s location, add the following permissions to your AndroidManifest.xml:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

Ensure permissions are requested before accessing location services. Check out more about location permissions in the Android documentation.

3: Send Device Location Updates

To send device location updates, create a simple class that handles location updates and sends them to the chat channel. This class will use the FusedLocationProviderClient to get location updates and the ChatClient.updateLiveLocation to update a started live location message.

/**
 * Consider using a foreground service for continuous location updates while the app is in the background.
 */
class SharedLocationService(private val context: Context) : LocationCallback() {

    private val locationClient = LocationServices.getFusedLocationProviderClient(context)

    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    private val chatClient by lazy { ChatClient.instance() }

    @Volatile
    private var activeLiveLocations: List<Location> = emptyList()

    // Track whether we are currently receiving updates
    @Volatile
    private var isReceivingUpdates = false

    var currentDeviceId: String = UnknownDeviceId
        private set

    // Call this when the user connects to the client
    fun start() {
        scope.launch { currentDeviceId = /* Get current device if logic */ }

        // Fetch user's active live locations
        chatClient.queryActiveLocations().enqueue()

        // Listen for changes in current user's active live locations
        chatClient.globalStateFlow
            .flatMapLatest { it.currentUserActiveLiveLocations }
            .onEach { userActiveLiveLocations ->

                activeLiveLocations = userActiveLiveLocations.toList()

                if (userActiveLiveLocations.isEmpty()) {
                    // No active locations, stop receiving updates
                    locationClient.removeLocationUpdates(this)
                    isReceivingUpdates = false
                } else {
                    if (!isReceivingUpdates) {
                        // If we are not already receiving updates, request location updates
                        val hasPermission = ContextCompat.checkSelfPermission(
                            context,
                            Manifest.permission.ACCESS_FINE_LOCATION,
                        ) == PackageManager.PERMISSION_GRANTED
                        if (hasPermission) {
                            val request = LocationRequest.Builder(
                                Priority.PRIORITY_HIGH_ACCURACY,
                                5000L, // Location updates internal in millis
                            ).build()
                            locationClient.requestLocationUpdates(request, this, Looper.getMainLooper())
                            isReceivingUpdates = true
                        } else {
                            // Log or handle the case where location permission is not granted
                        }
                    } else {
                        // Already receiving updates, no need to request again
                    }
                }
            }
            .launchIn(scope)
    }

    override fun onLocationResult(result: LocationResult) {

        val locationsToUpdate = activeLiveLocations

        // Check if we have any active live locations to update
        for (deviceLocation in result.locations) {
            locationsToUpdate
                .filterNot { it.endAt?.before(Date()) ?: false } // Filter out expired locations
                .forEach { activeLiveLocation ->
                    chatClient.updateLiveLocation(
                        messageId = activeLiveLocation.messageId,
                        latitude = deviceLocation.latitude,
                        longitude = deviceLocation.longitude,
                        deviceId = currentDeviceId,
                    ).enqueue { /* Handle success or error */ }
                }
        }
    }

    // Call this when the user disconnects from the client
    fun stop() {
        // Stop receiving location updates
        locationClient.removeLocationUpdates(this)
        scope.cancel()
    }
}

Check out SharedLocationService for a complete implementation.

4: Add Location Picker

To allow users to select a location, you can create a location picker dialog or screen. This example uses a custom attachment picker tab.

internal class LocationPickerTabFactory(
    private val viewModelFactory: SharedLocationViewModelFactory,
) : AttachmentsPickerTabFactory {

    override val attachmentsPickerMode: AttachmentsPickerMode =
        CustomPickerMode()

    override fun isPickerTabEnabled(channel: Channel): Boolean =
        channel.config.sharedLocationsEnabled

    @Composable
    override fun PickerTabIcon(isEnabled: Boolean, isSelected: Boolean) {
        Icon(
            imageVector = Icons.Rounded.ShareLocation,
            contentDescription = "Share Location",
            tint = when {
                isEnabled -> ChatTheme.colors.textLowEmphasis
                else -> ChatTheme.colors.disabled
            },
        )
    }

    @Composable
    override fun PickerTabContent(
        onAttachmentPickerAction: (AttachmentPickerAction) -> Unit,
        attachments: List<AttachmentPickerItemState>,
        onAttachmentsChanged: (List<AttachmentPickerItemState>) -> Unit,
        onAttachmentItemSelected: (AttachmentPickerItemState) -> Unit,
        onAttachmentsSubmitted: (List<AttachmentMetaData>) -> Unit,
    ) {
        LocationPicker(
            viewModelFactory = viewModelFactory,
            onDismiss = { onAttachmentPickerAction(AttachmentPickerBack) },
        )
    }
}

Then set up the LocationPickerTabFactory in the ChatTheme:

val attachmentsPickerTabFactories = AttachmentsPickerTabFactories.defaultFactories() +
    LocationPickerTabFactory(viewModelFactory = SharedLocationViewModelFactory(cid))
ChatTheme(
    attachmentsPickerTabFactories = attachmentsPickerTabFactories,
)

LocationPicker uses SharedLocationViewModel to handle the following:

  • Send a static location through the ChatClient.sendStaticLocation function when the user clicks the “Send Current Location” button.
  • Start a live location through the ChatClient.startLiveLocationSharing function when the user clicks the “Share Live Location” button.

LocationPicker renders a map using Leaflet.js through a WebView.

Check out LocationPickerTabFactory, LocationPicker, and SharedLocationViewModel for a complete implementation.

5: Display Shared Locations

To display shared locations in the chat, you can override the MessageItemCenterContent component factory and create a custom component that renders the location on a map.

class CustomChatComponentFactory(
    private val delegate: ChatComponentFactory = object : ChatComponentFactory {},
) : ChatComponentFactory by deltegate {

    @Composable
    override fun ColumnScope.MessageItemCenterContent(
        messageItem: MessageItemState,
        onLongItemClick: (Message) -> Unit,
        onPollUpdated: (Message, Poll) -> Unit,
        onCastVote: (Message, Poll, Option) -> Unit,
        onRemoveVote: (Message, Poll, Vote) -> Unit,
        selectPoll: (Message, Poll, PollSelectionType) -> Unit,
        onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit,
        onClosePoll: (String) -> Unit,
        onAddPollOption: (poll: Poll, option: String) -> Unit,
        onGiphyActionClick: (GiphyAction) -> Unit,
        onQuotedMessageClick: (Message) -> Unit,
        onLinkClick: ((Message, String) -> Unit)?,
        onUserMentionClick: (User) -> Unit,
        onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit,
    ) {
        val message = messageItem.message
        // Check if the message has a shared location and is not deleted
        if (message.hasSharedLocation() && !message.isDeleted()) {
            val location = requireNotNull(message.sharedLocation)
            SharedLocationItem(
                modifier = Modifier.widthIn(max = ChatTheme.dimens.messageItemMaxWidth),
                message = message,
                location = location,
                onMapClick = { url -> onLinkClick?.invoke(message, url) },
                onMapLongClick = { onLongItemClick(message) },
            )
        } else {
            // Fallback to the default implementation
            with(delegate) {
                MessageItemCenterContent(
                    messageItem = messageItem,
                    onLongItemClick = onLongItemClick,
                    onGiphyActionClick = onGiphyActionClick,
                    onQuotedMessageClick = onQuotedMessageClick,
                    onLinkClick = onLinkClick,
                    onUserMentionClick = onUserMentionClick,
                    onMediaGalleryPreviewResult = onMediaGalleryPreviewResult,
                    onPollUpdated = onPollUpdated,
                    onCastVote = onCastVote,
                    onRemoveVote = onRemoveVote,
                    selectPoll = selectPoll,
                    onAddAnswer = onAddAnswer,
                    onClosePoll = onClosePoll,
                    onAddPollOption = onAddPollOption,
                )
            }
        }
    }
}

When the shared location has an endAt date, it is considered a live location sharing.

In this guide, stoping a live location sharing is handled in the SharedLocationViewModel by calling the ChatClient.stopLiveLocationSharing function.

Then set up the CustomChatComponentFactory in the ChatTheme:

val chatComponentFactory = CustomChatComponentFactory()
ChatTheme(
    chatComponentFactory = chatComponentFactory,
)

Learn more about Component Factory.

Check out SharedLocationItem for a complete implementation.

Summary

In this guide, you learned how to implement a location-sharing feature in your Stream Chat application using Jetpack Compose. You set up dependencies, requested location permissions, sent device location updates, added a location picker, and displayed shared locations in the chat.

© Getstream.io, Inc. All Rights Reserved.