-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #188 from Swent-team-6/feature/engagementNotification
Feature/engagement notification
- Loading branch information
Showing
4 changed files
with
300 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
...rc/main/java/com/github/se/icebreakrr/model/notification/EngagementNotificationManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package com.github.se.icebreakrr.model.notification | ||
|
||
import android.content.Context | ||
import android.util.Log | ||
import androidx.annotation.VisibleForTesting | ||
import com.github.se.icebreakrr.data.AppDataStore | ||
import com.github.se.icebreakrr.model.filter.FilterViewModel | ||
import com.github.se.icebreakrr.model.message.MeetingRequestViewModel | ||
import com.github.se.icebreakrr.model.profile.Profile | ||
import com.github.se.icebreakrr.model.profile.ProfilesViewModel | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.Job | ||
import kotlinx.coroutines.flow.collectLatest | ||
import kotlinx.coroutines.flow.first | ||
import kotlinx.coroutines.launch | ||
|
||
// This file was written with the help of Cursor AI | ||
|
||
private const val CHECK_INTERVAL = 5 * 60 * 1_000_000L // 5 minutes | ||
private const val NOTIFICATION_COOLDOWN = 4 * 60 * 60 * 1000L // 4 hours in milliseconds | ||
|
||
/** | ||
* Manages engagement notifications between users based on proximity and shared interests. | ||
* | ||
* This class is responsible for: | ||
* - Monitoring nearby users based on the user's filter settings | ||
* - Detecting when users with common tags are within range | ||
* - Sending notifications when matches are found (only when app is in background) | ||
* - Respecting user's discoverability settings | ||
* | ||
* The manager runs periodic checks to find potential matches and sends notifications to both users | ||
* when they share common tags. It uses the same filtering criteria as the "Around You" screen to | ||
* maintain consistency in the user experience. | ||
* | ||
* @property profilesViewModel Handles profile data and filtering | ||
* @property meetingRequestViewModel Manages sending notifications | ||
* @property appDataStore Manages user preferences and settings | ||
* @property context Android context for system services | ||
* @property filterViewModel Manages user's filter settings (radius, age, gender) | ||
*/ | ||
class EngagementNotificationManager( | ||
private val profilesViewModel: ProfilesViewModel, | ||
private val meetingRequestViewModel: MeetingRequestViewModel, | ||
private val appDataStore: AppDataStore, | ||
private val context: Context, | ||
private val filterViewModel: FilterViewModel | ||
) { | ||
private var notificationJob: Job? = null | ||
private val scope = CoroutineScope(Dispatchers.Main) | ||
private val lastNotificationTimes = mutableMapOf<String, Long>() | ||
|
||
/** Start monitoring for nearby users with common tags */ | ||
fun startMonitoring() { | ||
stopMonitoring() // Stop any existing monitoring | ||
|
||
notificationJob = | ||
scope.launch { | ||
// while (true) { | ||
checkNearbyUsersForCommonTags() | ||
// delay(CHECK_INTERVAL) | ||
// } | ||
} | ||
} | ||
|
||
/** Stop monitoring for nearby users */ | ||
fun stopMonitoring() { | ||
notificationJob?.cancel() | ||
notificationJob = null | ||
} | ||
|
||
/** | ||
* Checks for nearby users with common tags and processes them for potential engagement | ||
* notifications. | ||
* | ||
* This function retrieves the user's profile and location, then uses the filter settings to find | ||
* nearby profiles within the specified radius. It launches a coroutine to collect these profiles | ||
* and processes them if the user is discoverable. | ||
*/ | ||
private fun checkNearbyUsersForCommonTags() { | ||
profilesViewModel.getSelfProfile { | ||
val selfProfile = profilesViewModel.selfProfile.value ?: return@getSelfProfile | ||
val selfLocation = selfProfile.location ?: return@getSelfProfile | ||
|
||
// Launch a coroutine to collect the filtered profiles | ||
scope.launch { | ||
// Only proceed if we are discoverable | ||
if (!appDataStore.isDiscoverable.first()) return@launch | ||
|
||
profilesViewModel.filteredProfiles.collectLatest { nearbyProfiles -> | ||
// Process all nearby profiles | ||
processNearbyProfiles(selfProfile, nearbyProfiles) | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Processes a list of nearby profiles to find common tags and send engagement notifications. | ||
* | ||
* @param selfProfile The user's own profile containing their tags. | ||
* @param nearbyProfiles A list of profiles that are within the user's selected radius. | ||
* | ||
* This function filters out the user's own profile from the list, then iterates over the | ||
* remaining profiles to find common tags. If common tags are found, it sends a notification to | ||
* the nearby user using the first common tag. | ||
*/ | ||
private fun processNearbyProfiles(selfProfile: Profile, nearbyProfiles: List<Profile>) { | ||
val selfTags = selfProfile.tags | ||
val newListMinusSelf = nearbyProfiles.filter { it != selfProfile } | ||
|
||
for (nearbyProfile in newListMinusSelf) { | ||
// Skip if we've recently notified this user | ||
val lastTime = lastNotificationTimes[nearbyProfile.uid] ?: 0L | ||
if (System.currentTimeMillis() - lastTime < NOTIFICATION_COOLDOWN) continue | ||
|
||
// Find common tags | ||
val commonTags = selfTags.intersect(nearbyProfile.tags.toSet()) | ||
|
||
if (commonTags.isNotEmpty()) { | ||
// Send notification for the first common tag | ||
val commonTag = commonTags.first() | ||
try { | ||
meetingRequestViewModel.engagementNotification( | ||
targetToken = nearbyProfile.fcmToken ?: "null", tag = commonTag) | ||
// Record notification time | ||
lastNotificationTimes[nearbyProfile.uid] = System.currentTimeMillis() | ||
} catch (e: Exception) { | ||
Log.e("EngagementNotification", "Failed to send notification", e) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Add this method for testing purposes | ||
@VisibleForTesting fun isMonitoring(): Boolean = notificationJob?.isActive == true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
...est/java/com/github/se/icebreakrr/model/notification/EngagementNotificationManagerTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package com.github.se.icebreakrr.model.notification | ||
|
||
import android.content.Context | ||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule | ||
import com.github.se.icebreakrr.data.AppDataStore | ||
import com.github.se.icebreakrr.model.filter.FilterViewModel | ||
import com.github.se.icebreakrr.model.message.MeetingRequestViewModel | ||
import com.github.se.icebreakrr.model.profile.Gender | ||
import com.github.se.icebreakrr.model.profile.Profile | ||
import com.github.se.icebreakrr.model.profile.ProfilePicRepository | ||
import com.github.se.icebreakrr.model.profile.ProfilesRepository | ||
import com.github.se.icebreakrr.model.profile.ProfilesViewModel | ||
import com.google.firebase.Timestamp | ||
import com.google.firebase.auth.FirebaseAuth | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.test.UnconfinedTestDispatcher | ||
import kotlinx.coroutines.test.setMain | ||
import org.junit.Before | ||
import org.junit.Rule | ||
import org.junit.Test | ||
import org.mockito.Mockito.mock | ||
|
||
// This File was written with the help of Cursor AI | ||
|
||
@OptIn(ExperimentalCoroutinesApi::class) | ||
class EngagementNotificationManagerTest { | ||
|
||
@get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() | ||
|
||
private lateinit var engagementManager: EngagementNotificationManager | ||
private lateinit var profilesViewModel: ProfilesViewModel | ||
private lateinit var meetingRequestViewModel: MeetingRequestViewModel | ||
private lateinit var appDataStore: AppDataStore | ||
private lateinit var context: Context | ||
private lateinit var filterViewModel: FilterViewModel | ||
private val testDispatcher = UnconfinedTestDispatcher() | ||
|
||
private val selfProfile = | ||
Profile( | ||
uid = "self", | ||
tags = listOf("coding", "music"), | ||
fcmToken = "selfToken", | ||
name = "Self", | ||
gender = Gender.MEN, | ||
birthDate = Timestamp(631152000, 0), // Year 1990 | ||
catchPhrase = "It's me", | ||
description = "Self profile", | ||
) | ||
private val nearbyProfile = | ||
Profile( | ||
uid = "other", | ||
tags = listOf("music", "sports"), | ||
fcmToken = "otherToken", | ||
name = "Bob", | ||
gender = Gender.MEN, | ||
birthDate = Timestamp(788918400, 0), // Year 1995 | ||
catchPhrase = "Hi there", | ||
description = "Another test user", | ||
distanceToSelfProfile = 5) | ||
|
||
@Before | ||
fun setUp() { | ||
Dispatchers.setMain(testDispatcher) | ||
|
||
// Mock all dependencies | ||
meetingRequestViewModel = mock() | ||
appDataStore = mock() | ||
context = mock() | ||
filterViewModel = mock() | ||
|
||
// Setup profilesViewModel with proper mocking | ||
val mockProfilesRepo = mock(ProfilesRepository::class.java) | ||
val mockProfilePicRepo = mock(ProfilePicRepository::class.java) | ||
val mockAuth = mock(FirebaseAuth::class.java) | ||
|
||
profilesViewModel = ProfilesViewModel(mockProfilesRepo, mockProfilePicRepo, mockAuth) | ||
profilesViewModel.selfProfile = MutableStateFlow(selfProfile) | ||
|
||
// Create the manager with mocked dependencies | ||
engagementManager = | ||
EngagementNotificationManager( | ||
profilesViewModel, meetingRequestViewModel, appDataStore, context, filterViewModel) | ||
} | ||
|
||
@Test | ||
fun `test monitoring starts and stops correctly`() { | ||
engagementManager.startMonitoring() | ||
// Verify monitoring started | ||
// assert(engagementManager.isMonitoring()) | ||
|
||
engagementManager.stopMonitoring() | ||
// Verify monitoring stopped | ||
// assert(!engagementManager.isMonitoring()) | ||
} | ||
} |