Skip to content

Commit

Permalink
Merge branch 'refs/heads/main' into megh/implement-e2ee-for-locations-sp
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-megh-l committed Jan 9, 2025
2 parents 3b7092c + 87cf70d commit 53d85d3
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 42 deletions.
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
<p align="center"> <a href="https://canopas.com/contact"><img src="./screenshots/cta_banner2.png" alt=""></a></p>

# GroupTrack - Stay connected, Anywhere!
# Grouptrack - Stay connected, Anywhere!
Enhancing family safety and communication with real-time location sharing and modern UIs.

<img src="./screenshots/cover_image.png" alt="cover" width="100%"/>

## Overview
Welcome to GroupTrack, an open-source Android application designed to enhance family safety through real-time location sharing and communication features. GroupTrack aims to provide peace of mind by ensuring the safety of your loved ones and facilitating seamless communication regardless of their location.
Welcome to Grouptrack, an open-source Android application designed to enhance family safety through real-time location sharing and communication features. Grouptrack aims to provide peace of mind by ensuring the safety of your loved ones and facilitating seamless communication regardless of their location.

GroupTrack adopts the MVVM architecture pattern and leverages Jetpack Compose for building modern UIs declaratively. This architecture ensures a clear separation of concerns, making the codebase more maintainable and testable. Jetpack Compose simplifies UI development by allowing developers to define UI elements and their behavior in a more intuitive way, resulting in a seamless user experience.
Grouptrack adopts the MVVM architecture pattern and leverages Jetpack Compose for building modern UIs declaratively. This architecture ensures a clear separation of concerns, making the codebase more maintainable and testable. Jetpack Compose simplifies UI development by allowing developers to define UI elements and their behavior in a more intuitive way, resulting in a seamless user experience.

## Download App
<a href="https://play.google.com/store/apps/details?id=com.canopas.yourspace"><img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" width="200"></img></a>


## Features
GroupTrack is currently in active development 🚧, with plans to incorporate additional features shortly.
Grouptrack is currently in active development 🚧, with plans to incorporate additional features shortly.

GroupTrack ensures your loved ones' well-being with:
Grouptrack ensures your loved ones' well-being with:

- [X] Real-time Location Sharing
- [X] Secure Communication
- [X] Location History with Routes
- [X] Geo-fencing
- [X] End-to-End Encryption
- [ ] SOS Help Alert

## Security Features

### End-to-End Encryption
Grouptrack ensures the privacy and security of your data by implementing end-to-end encryption. This encryption method ensures that only the group members can access the location data, preventing unauthorized access by third parties.

- 🔐 **Signal Protocol Integration**
- Leverages the industry-leading Signal Protocol for end-to-end encryption
- Provides advanced cryptographic protection for all shared location data

- 🔒 **Comprehensive Data Protection**
- All location data are encrypted before transmission
- Encryption keys are uniquely generated for each user and space
- No third-party, including Grouptrack servers, can access unencrypted data

- 🔑 **Advanced Encryption Mechanisms**
- Utilizes industry-standard encryption algorithms (e.g., AES-256)
- Implements secure key exchange protocols
- Ensures data integrity and confidentiality

- 🛡️ **Privacy Guarantees**
- Encryption happens client-side before data leaves the device
- Encrypted data is stored securely with no server-side decryption

*Note: End-to-end encryption ensures that only intended group member can decrypt and view shared information.*

## Screenshots

<table>
Expand Down Expand Up @@ -70,14 +95,14 @@ Use the `applicationId` value specified in the `app/build.gradle` file of the ap
Once the project is created, you will need to add the `google-services.json` file to the app module.
For more information, refer to the [Firebase documentation](https://firebase.google.com/docs/android/setup).

GroupTrack uses the following Firebase services, Make sure you enable them in your Firebase project:
Grouptrack uses the following Firebase services, Make sure you enable them in your Firebase project:
- Authentication (Phone, Google)
- Firestore (To store user data)
</details>

## Tech stack

GroupTrack utilizes the latest Android technologies and adheres to industry best practices. Below is the current tech stack used in the development process:
Grouptrack utilizes the latest Android technologies and adheres to industry best practices. Below is the current tech stack used in the development process:

- MVVM Architecture
- Jetpack Compose
Expand All @@ -100,13 +125,13 @@ GroupTrack utilizes the latest Android technologies and adheres to industry best
The Canopas team enthusiastically welcomes contributions and project participation! There are a bunch of things you can do if you want to contribute! The [Contributor Guide](CONTRIBUTING.md) has all the information you need for everything from reporting bugs to contributing entire new features. Please don't hesitate to jump in if you'd like to, or even ask us questions if something isn't clear.

## Credits
GroupTrack is owned and maintained by the [Canopas team](https://canopas.com/). You can follow them on X at [@canopassoftware](https://x.com/canopassoftware) for project updates and releases. If you are interested in building apps or designing products, please let us know. We'd love to hear from you!
Grouptrack is owned and maintained by the [Canopas team](https://canopas.com/). You can follow them on X at [@canopassoftware](https://x.com/canopassoftware) for project updates and releases. If you are interested in building apps or designing products, please let us know. We'd love to hear from you!

<a href="https://canopas.com/contact"><img src="./screenshots/cta_btn.png" width=300></a>

## License

GroupTrack is licensed under the Apache License, Version 2.0.
Grouptrack is licensed under the Apache License, Version 2.0.

```
Copyright 2024 Canopas Software LLP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ fun HomeScreen(verifyingSpace: Boolean) {
modifier = Modifier
.padding(it)
) {
HomeScreenContent(navController)
HomeScreenContent(navController, viewModel::dismissSpaceSelection)

HomeTopBar(verifyingSpace)
}
Expand Down Expand Up @@ -207,13 +207,13 @@ private fun MapControl(

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun HomeScreenContent(navController: NavHostController) {
fun HomeScreenContent(navController: NavHostController, dismissSpaceSelection: () -> Unit) {
NavHost(
navController = navController,
startDestination = AppDestinations.map.path
) {
tabComposable(AppDestinations.map.path) {
MapScreen()
MapScreen(dismissSpaceSelection)
}

tabComposable(AppDestinations.places.path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ class HomeScreenViewModel @Inject constructor(
_state.value.copy(showSpaceSelectionPopup = !state.value.showSpaceSelectionPopup)
}

fun dismissSpaceSelection() {
_state.value = _state.value.copy(showSpaceSelectionPopup = false)
}

fun navigateToCreateSpace() {
navigator.navigateTo(AppDestinations.createSpace.path)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private const val DEFAULT_CAMERA_ZOOM_FOR_SELECTED_USER = 17f

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MapScreen() {
fun MapScreen(dismissSpaceSelection: () -> Unit) {
val viewModel = hiltViewModel<MapViewModel>()
val state by viewModel.state.collectAsState()
val scope = rememberCoroutineScope()
Expand Down Expand Up @@ -136,7 +136,7 @@ fun MapScreen() {
}

Box(modifier = Modifier.fillMaxSize()) {
MapView(cameraPositionState)
MapView(cameraPositionState, dismissSpaceSelection)

Column(
modifier = Modifier
Expand All @@ -145,6 +145,7 @@ fun MapScreen() {
) {
Column(modifier = Modifier.align(Alignment.End)) {
MapControlBtn(icon = R.drawable.ic_map_type) {
dismissSpaceSelection()
viewModel.toggleStyleSheetVisibility(true)
}

Expand All @@ -153,6 +154,7 @@ fun MapScreen() {
show = relocate
) {
scope.launch {
dismissSpaceSelection()
if (state.isMapLoaded) {
cameraPositionState.animate(
CameraUpdateFactory.newLatLngZoom(
Expand All @@ -169,6 +171,7 @@ fun MapScreen() {
containerColor = AppTheme.colorScheme.primary,
contentColor = AppTheme.colorScheme.textInversePrimary
) {
dismissSpaceSelection()
viewModel.navigateToPlaces()
}
}
Expand Down Expand Up @@ -212,6 +215,7 @@ fun MapScreen() {
}
items(state.members) {
MapUserItem(it) {
dismissSpaceSelection()
viewModel.showMemberDetail(it)
}
}
Expand Down Expand Up @@ -330,7 +334,8 @@ fun PermissionFooter(onClick: () -> Unit) {

@Composable
private fun MapView(
cameraPositionState: CameraPositionState
cameraPositionState: CameraPositionState,
dismissSpaceSelection: () -> Unit
) {
val viewModel = hiltViewModel<MapViewModel>()
val state by viewModel.state.collectAsState()
Expand Down Expand Up @@ -368,6 +373,15 @@ private fun MapView(
),
onMapLoaded = {
viewModel.onMapLoaded()
},
onMapClick = {
dismissSpaceSelection()
if (state.showUserDetails) {
viewModel.dismissMemberDetail()
}
if (state.isStyleSheetVisible) {
viewModel.toggleStyleSheetVisibility(false)
}
}
) {
if (state.members.isNotEmpty()) {
Expand All @@ -377,6 +391,10 @@ private fun MapView(
location = it.location!!,
isSelected = it.user.id == state.selectedUser?.user?.id
) {
dismissSpaceSelection()
if (state.isStyleSheetVisible) {
viewModel.toggleStyleSheetVisibility(false)
}
viewModel.showMemberDetail(it)
}
}
Expand All @@ -388,7 +406,12 @@ private fun MapView(
user = currentUser,
location = location.toApiLocation(currentUser.id),
isSelected = currentUser.id == state.selectedUser?.user?.id
) {}
) {
dismissSpaceSelection()
if (state.isStyleSheetVisible) {
viewModel.toggleStyleSheetVisibility(false)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class MapViewModel @Inject constructor(
}

fun toggleStyleSheetVisibility(isVisible: Boolean) {
_state.value = _state.value.copy(isStyleSheetVisible = isVisible)
_state.value = _state.value.copy(isStyleSheetVisible = isVisible, showUserDetails = false)
}

fun showJourneyTimeline() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fun JourneyLocationItem(
}
}

val title = fromAddress?.formattedTitle(toAddress) ?: ""
val title = formattedAddress(fromAddress, toAddress)

Row(
verticalAlignment = Alignment.Top,
Expand Down Expand Up @@ -469,34 +469,39 @@ internal fun getFormattedLocationTimeForFirstItem(createdAt: Long): String {
}

fun Address.formattedTitle(toAddress: Address?): String {
val fromName = extractLocationName(this)
val toName = toAddress?.let { extractLocationName(it) } ?: "Unknown Road"

return if (toAddress == null) {
fromName
} else {
"$fromName -> $toName"
}
return formattedAddress(this, toAddress)
}

private fun extractLocationName(address: Address): String {
val featureName = address.featureName?.trim()
val thoroughfare = address.thoroughfare?.trim()
fun formattedAddress(fromPlace: Address?, toPlace: Address?): String {
val fromCity = fromPlace?.locality ?: ""
val toCity = toPlace?.locality ?: ""

val potentialNames = listOf(
featureName,
thoroughfare
).filterNot { it.isNullOrEmpty() }
val fromArea = fromPlace?.subLocality ?: ""
val toArea = toPlace?.subLocality ?: ""

val cleanedNames = potentialNames.map { it?.replace(Regex("^[A-Za-z0-9]+\\+.*"), "")?.trim() }
val name = cleanedNames.firstOrNull { it?.isNotEmpty() == true } ?: "Unknown Road"
val fromState = fromPlace?.adminArea ?: ""
val toState = toPlace?.adminArea ?: ""

val resultName = if (name.matches(Regex("^[0-9].*"))) {
val streetName = cleanedNames.getOrNull(1) ?: ""
"$name $streetName".trim()
} else {
name
return when {
toPlace == null -> formatAddress(listOf(fromArea, fromCity))
fromArea == toArea -> formatAddress(listOf(fromArea, fromCity))
fromCity == toCity -> formatTwoPlaceAddress(listOf(fromArea), listOf(toArea, fromCity))
fromState == toState -> formatTwoPlaceAddress(listOf(fromArea, fromCity), listOf(toArea, toCity))
else -> formatTwoPlaceAddress(listOf(fromCity, fromState), listOf(toCity, toState))
}
}

fun formatTwoPlaceAddress(fromPlace: List<String>, toPlace: List<String>): String {
val isFromPlaceEmpty = fromPlace.all { it.isEmpty() }
val isToPlaceEmpty = toPlace.all { it.isEmpty() }

return when {
!isFromPlaceEmpty && !isToPlaceEmpty -> "${formatAddress(fromPlace)} -> ${formatAddress(toPlace)}"
!isFromPlaceEmpty && isToPlaceEmpty -> formatAddress(fromPlace)
else -> formatAddress(toPlace)
}
}

return resultName
fun formatAddress(parts: List<String>): String {
return parts.joinToString(", ") { it.ifEmpty { "Unknown" } }
}
Binary file modified screenshots/cover_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 53d85d3

Please sign in to comment.