Skip to content

Commit

Permalink
Merge pull request #350 from PeriodPals/feat/geolocation/secure-locat…
Browse files Browse the repository at this point in the history
…ion-handling

Feat/geolocation/secure-location-handling : Secure location handling
  • Loading branch information
charliemangano authored Dec 19, 2024
2 parents ed29b12 + a12b21e commit 54d1a4e
Show file tree
Hide file tree
Showing 18 changed files with 508 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import com.android.periodpals.model.authentication.AuthenticationViewModel
import com.android.periodpals.model.location.Location
import com.android.periodpals.model.location.UserLocationViewModel
import com.android.periodpals.model.user.AuthenticationUserData
import com.android.periodpals.model.user.UserViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.After
Expand All @@ -36,7 +36,7 @@ class GPSServiceImplInstrumentedTest {
private lateinit var activity: ComponentActivity
private lateinit var gpsService: GPSServiceImpl
private lateinit var authenticationViewModel: AuthenticationViewModel
private lateinit var userViewModel: UserViewModel
private lateinit var userLocationViewModel: UserLocationViewModel

// Default location
private val defaultLat = Location.DEFAULT_LOCATION.latitude
Expand All @@ -45,7 +45,7 @@ class GPSServiceImplInstrumentedTest {
@Before
fun setup() {
authenticationViewModel = mock(AuthenticationViewModel::class.java)
userViewModel = mock(UserViewModel::class.java)
userLocationViewModel = mock(UserLocationViewModel::class.java)

scenario = ActivityScenario.launch(ComponentActivity::class.java)

Expand All @@ -55,7 +55,7 @@ class GPSServiceImplInstrumentedTest {

scenario.onActivity { activity ->
this.activity = activity
gpsService = GPSServiceImpl(this.activity, authenticationViewModel, userViewModel)
gpsService = GPSServiceImpl(this.activity, authenticationViewModel, userLocationViewModel)
}

// Once the GPSService has been initialized, set its state to resumed
Expand Down
19 changes: 11 additions & 8 deletions app/src/main/java/com/android/periodpals/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import com.android.periodpals.model.authentication.AuthenticationModelSupabase
import com.android.periodpals.model.authentication.AuthenticationViewModel
import com.android.periodpals.model.chat.ChatViewModel
import com.android.periodpals.model.location.LocationViewModel
import com.android.periodpals.model.location.UserLocationModelSupabase
import com.android.periodpals.model.location.UserLocationViewModel
import com.android.periodpals.model.timer.TimerManager
import com.android.periodpals.model.timer.TimerRepositorySupabase
import com.android.periodpals.model.timer.TimerViewModel
Expand Down Expand Up @@ -88,6 +90,9 @@ class MainActivity : ComponentActivity() {
private val userModel = UserRepositorySupabase(supabaseClient)
private val userViewModel = UserViewModel(userModel)

private val userLocationModel = UserLocationModelSupabase(supabaseClient)
private val userLocationViewModel = UserLocationViewModel(userLocationModel)

private val alertModel = AlertModelSupabase(supabaseClient)
private val alertViewModel = AlertViewModel(alertModel)

Expand All @@ -98,7 +103,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

gpsService = GPSServiceImpl(this, authenticationViewModel, userViewModel)
gpsService = GPSServiceImpl(this, authenticationViewModel, userLocationViewModel)
pushNotificationsService =
PushNotificationsServiceImpl(this, authenticationViewModel, userViewModel)
timerManager = TimerManager(this)
Expand All @@ -111,10 +116,7 @@ class MainActivity : ComponentActivity() {
GoogleApiAvailability.getInstance().makeGooglePlayServicesAvailable(this)

// Set up the OfflinePlugin for offline storage
val offlinePluginFactory =
StreamOfflinePluginFactory(
appContext = applicationContext,
)
val offlinePluginFactory = StreamOfflinePluginFactory(appContext = applicationContext)
val statePluginFactory =
StreamStatePluginFactory(config = StatePluginConfig(), appContext = this)

Expand Down Expand Up @@ -173,7 +175,7 @@ class MainActivity : ComponentActivity() {
*/
fun userAuthStateLogic(
authenticationViewModel: AuthenticationViewModel,
navigationActions: NavigationActions
navigationActions: NavigationActions,
) {
when (authenticationViewModel.userAuthenticationState.value) {
is UserAuthenticationState.SuccessIsLoggedIn -> navigationActions.navigateTo(Screen.PROFILE)
Expand All @@ -190,7 +192,7 @@ fun PeriodPalsApp(
alertViewModel: AlertViewModel,
timerViewModel: TimerViewModel,
chatClient: ChatClient,
chatViewModel: ChatViewModel
chatViewModel: ChatViewModel,
) {
val navController = rememberNavController()
val navigationActions = NavigationActions(navController)
Expand Down Expand Up @@ -229,7 +231,8 @@ fun PeriodPalsApp(
authenticationViewModel,
locationViewModel,
gpsService,
navigationActions)
navigationActions,
)
}
composable(Screen.EDIT_ALERT) {
EditAlertScreen(locationViewModel, gpsService, alertViewModel, navigationActions)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.android.periodpals.model.location

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Data Transfer Object (DTO) representing a user's location.
*
* @property uid The unique identifier of the user.
* @property location The geographical location of the user.
*/
@Serializable
data class UserLocationDto(
@SerialName("uid") val uid: String,
@SerialName("locationGIS") val location: LocationGIS,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.android.periodpals.model.location

/** Interface representing a model for user location. */
interface UserLocationModel {

/**
* Creates a new location.
*
* @param locationDto The [Location] data transfer object to be inserted.
* @param onSuccess A callback function to be invoked upon successful operation.
* @param onFailure A callback function to be invoked with an `Exception` if the operation fails.
*/
suspend fun create(
locationDto: UserLocationDto,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit,
)

/**
* Inserts or updates a location.
*
* @param locationDto The [Location] data transfer object to be inserted or updated.
* @param onSuccess A callback function to be invoked upon successful operation.
* @param onFailure A callback function to be invoked with an `Exception` if the operation fails.
*/
suspend fun update(
locationDto: UserLocationDto,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.android.periodpals.model.location

import android.util.Log
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.postgrest.postgrest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

/**
* Implementation of the UserLocationModel interface using Supabase.
*
* @property supabase The Supabase client used for database operations.
*/
class UserLocationModelSupabase(private val supabase: SupabaseClient) : UserLocationModel {

companion object {
private const val TAG = "UserLocationModelSupabase"
private const val LOCATIONS = "locations"
}

/**
* Creates a new location in the Supabase database.
*
* @param locationDto The [LocationGIS] data transfer object to be inserted.
* @param onSuccess A callback function to be invoked upon successful operation.
* @param onFailure A callback function to be invoked with an Exception if the operation fails.
*/
override suspend fun create(
locationDto: UserLocationDto,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit,
) {
Log.d(TAG, "create: sending location dto: $locationDto")
try {
withContext(Dispatchers.IO) {
supabase.postgrest[LOCATIONS].insert(locationDto)
Log.d(TAG, "create: success")
onSuccess()
}
} catch (e: Exception) {
Log.e(TAG, "create: fail to create location of user ${locationDto.uid}: ${e.message}")
onFailure(e)
}
}

/**
* Updates a location in the Supabase database.
*
* @param locationDto The [LocationGIS] data transfer object to be inserted or updated.
* @param onSuccess A callback function to be invoked upon successful operation.
* @param onFailure A callback function to be invoked with an Exception if the operation fails.
*/
override suspend fun update(
locationDto: UserLocationDto,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit,
) {
Log.d(TAG, "update: sending location dto: $locationDto")
try {
withContext(Dispatchers.IO) {
supabase.postgrest[LOCATIONS].update(locationDto) { filter { eq("uid", locationDto.uid) } }
Log.d(TAG, "update: success")
onSuccess()
}
} catch (e: Exception) {
Log.e(TAG, "update: fail to update location of user ${locationDto.uid}: ${e.message}")
onFailure(e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.android.periodpals.model.location

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class UserLocationViewModel(private val userLocationModel: UserLocationModel) : ViewModel() {

companion object {
private const val TAG = "UserLocationViewModel"
}

/**
* Create a user location, or update it if it does already exists.
*
* Note: This implementation does not use upsert for security reasons.
*
* @param uid The unique identifier of the user.
* @param location The new [LocationGIS] of the user.
* @param onSuccess A callback function to be invoked upon successful operation.
* @param onFailure A callback function to be invoked with an `Exception` if the operation fails.
*/
fun uploadUserLocation(
uid: String,
location: LocationGIS,
onSuccess: () -> Unit = { Log.d(TAG, "uploadUserLocation success callback") },
onFailure: (Exception) -> Unit = { e: Exception ->
Log.d(TAG, "uploadUserLocation failure callback: ${e.message}")
},
) {
val locationDto = UserLocationDto(uid = uid, location = location)
viewModelScope.launch {
// try to create a new location
Log.d(TAG, "uploadUserLocation: trying to create location $locationDto for user $uid")
userLocationModel.create(
locationDto = locationDto,
onSuccess = {
Log.d(TAG, "uploadUserLocation: create user location successful")
onSuccess()
},
onFailure = { e: Exception ->
Log.d(TAG, "createUserLocation: location already exists, updating instead")
update(locationDto, onSuccess, onFailure)
},
)
}
}

private fun update(
locationDto: UserLocationDto,
onSuccess: () -> Unit,
onFailure: (Exception) -> Unit,
) {
Log.d(TAG, "update: trying to update location $locationDto")
viewModelScope.launch {
userLocationModel.update(
locationDto = locationDto,
onSuccess = {
Log.d(TAG, "update: update user location successful")
onSuccess()
},
onFailure = { e: Exception ->
Log.d(TAG, "update: failed to upsert location of user ${locationDto.uid}: ${e.message}")
onFailure(e)
},
)
}
}
}
7 changes: 0 additions & 7 deletions app/src/main/java/com/android/periodpals/model/user/User.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package com.android.periodpals.model.user

import com.android.periodpals.model.location.Location
import com.android.periodpals.model.location.LocationGIS
import com.android.periodpals.model.location.parseLocationGIS

/**
* Data class representing a user.
*
Expand All @@ -13,7 +9,6 @@ import com.android.periodpals.model.location.parseLocationGIS
* @property dob The date of birth of the user.
* @property preferredDistance The preferred radius distance for receiving alerts.
* @property fcmToken The Firebase Cloud Messaging token for the user (optional).
* @property locationGIS The geographic location of the user. Default is the default location.
*/
data class User(
val name: String,
Expand All @@ -22,7 +17,6 @@ data class User(
val dob: String,
val preferredDistance: Int,
val fcmToken: String? = null,
val locationGIS: LocationGIS = parseLocationGIS(Location.DEFAULT_LOCATION),
) {
/**
* Converts the User object to a UserDto object.
Expand All @@ -37,7 +31,6 @@ data class User(
dob = this.dob,
preferred_distance = this.preferredDistance,
fcm_token = this.fcmToken,
locationGIS = this.locationGIS,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.android.periodpals.model.user

import com.android.periodpals.model.location.LocationGIS
import kotlinx.serialization.Serializable

/**
Expand All @@ -12,7 +11,6 @@ import kotlinx.serialization.Serializable
* @property dob The age of the user.
* @property preferred_distance The preferred radius distance for receiving alerts.
* @property fcm_token The Firebase Cloud Messaging token for the user (optional).
* @property locationGIS The geographic location of the user.
*/
@Serializable
data class UserDto(
Expand All @@ -22,7 +20,6 @@ data class UserDto(
val dob: String,
val preferred_distance: Int,
val fcm_token: String? = null,
val locationGIS: LocationGIS,
) {
/**
* Converts this UserDto to a User object.
Expand All @@ -37,7 +34,6 @@ data class UserDto(
dob = this.dob,
preferredDistance = this.preferred_distance,
fcmToken = this.fcm_token,
locationGIS = this.locationGIS,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ class UserRepositorySupabase(private val supabase: SupabaseClient) : UserReposit
dob = user.dob,
preferred_distance = user.preferredDistance,
fcm_token = user.fcmToken,
locationGIS = user.locationGIS,
)
supabase.postgrest[USERS].insert(userDto)
}
Expand Down
Loading

0 comments on commit 54d1a4e

Please sign in to comment.