diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..324450d2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,54 @@ + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 512528d6..506cca0b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -118,6 +118,7 @@ dependencies { androidTestImplementation('androidx.test.espresso:espresso-contrib:3.4.0') { exclude module: "protobuf-lite" } + implementation('androidx.test.espresso:espresso-idling-resource:3.4.0') // Import the BoM for the Firebase platform implementation platform('com.google.firebase:firebase-bom:30.0.2') diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/CacheTests.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/CacheTests.kt index eb21bf97..b6b8d093 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/CacheTests.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/CacheTests.kt @@ -8,13 +8,13 @@ import androidx.annotation.ColorInt import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import ch.epfl.sweng.rps.TestUtils.initializeForTest +import ch.epfl.sweng.rps.remote.Env import ch.epfl.sweng.rps.models.remote.LeaderBoardInfo import ch.epfl.sweng.rps.models.remote.User import ch.epfl.sweng.rps.models.ui.UserStat import ch.epfl.sweng.rps.persistence.Cache import ch.epfl.sweng.rps.persistence.PrivateStorage import ch.epfl.sweng.rps.persistence.Storage -import ch.epfl.sweng.rps.remote.Env import ch.epfl.sweng.rps.services.ServiceLocator import com.google.firebase.ktx.Firebase import io.mockk.mockk diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/ExampleInstrumentedTest.kt index b01c62b3..993fff47 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/ExampleInstrumentedTest.kt @@ -1,11 +1,13 @@ package ch.epfl.sweng.rps -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals +import androidx.test.ext.junit.runners.AndroidJUnit4 + import org.junit.Test import org.junit.runner.RunWith +import org.junit.Assert.* + /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/LeaderBoardFragmentTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/LeaderBoardFragmentTest.kt index 4129ed2b..4c833914 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/LeaderBoardFragmentTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/LeaderBoardFragmentTest.kt @@ -121,4 +121,3 @@ class LeaderBoardFragmentTest { } } - diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/SettingsPageTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/SettingsPageTest.kt index 001fe7c2..c4268c43 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/SettingsPageTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/SettingsPageTest.kt @@ -92,6 +92,7 @@ class SettingsPageTest { } + @Test fun testSettingsPage() { onView(withId(R.id.settings)).check(matches(isDisplayed())) diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/StatisticsFragmentTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/StatisticsFragmentTest.kt index a80efd64..b6bb5081 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/StatisticsFragmentTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/StatisticsFragmentTest.kt @@ -179,5 +179,4 @@ class StatisticsFragmentTest { } } -} - +} \ No newline at end of file diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/services/FirebaseGameServiceTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/services/FirebaseGameServiceTest.kt index 60934243..92337c9d 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/services/FirebaseGameServiceTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/services/FirebaseGameServiceTest.kt @@ -1,11 +1,11 @@ package ch.epfl.sweng.rps.services import ch.epfl.sweng.rps.TestUtils.initializeForTest -import ch.epfl.sweng.rps.models.remote.Game +import ch.epfl.sweng.rps.remote.FirebaseReferences import ch.epfl.sweng.rps.models.remote.GameMode +import ch.epfl.sweng.rps.models.remote.Game import ch.epfl.sweng.rps.models.remote.Hand import ch.epfl.sweng.rps.models.remote.Round -import ch.epfl.sweng.rps.remote.FirebaseReferences import ch.epfl.sweng.rps.remote.FirebaseRepository import ch.epfl.sweng.rps.utils.consume import com.google.firebase.Timestamp diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/ui/friends/FriendsFragmentTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/ui/friends/FriendsFragmentTest.kt index f93879cf..dfae56d9 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/ui/friends/FriendsFragmentTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/ui/friends/FriendsFragmentTest.kt @@ -1,71 +1,118 @@ package ch.epfl.sweng.rps.ui.friends +import android.content.Intent +import android.service.autofill.Validators.not import android.view.View import android.widget.ImageButton import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction -import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.platform.app.InstrumentationRegistry +import ch.epfl.sweng.rps.* +import ch.epfl.sweng.rps.TestUtils.initializeForTest +import ch.epfl.sweng.rps.remote.Env +import ch.epfl.sweng.rps.models.remote.FriendRequest +import ch.epfl.sweng.rps.models.remote.User +import com.google.firebase.Timestamp import ch.epfl.sweng.rps.ActivityScenarioRuleWithSetup -import ch.epfl.sweng.rps.ActivityScenarioRuleWithSetup.Companion.defaultTestFlow import ch.epfl.sweng.rps.MainActivity import ch.epfl.sweng.rps.R -import ch.epfl.sweng.rps.TestFlow -import ch.epfl.sweng.rps.TestUtils.initializeForTest -import ch.epfl.sweng.rps.models.remote.User -import ch.epfl.sweng.rps.models.ui.FakeFriendsData -import ch.epfl.sweng.rps.persistence.Cache -import ch.epfl.sweng.rps.persistence.PrivateStorage -import ch.epfl.sweng.rps.persistence.Storage -import ch.epfl.sweng.rps.remote.Env import ch.epfl.sweng.rps.remote.LocalRepository import ch.epfl.sweng.rps.services.ServiceLocator import com.google.firebase.ktx.Firebase -import kotlinx.coroutines.runBlocking import org.hamcrest.Matcher import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import kotlin.test.assertIsNot class FriendsFragmentTest { - val LIST_ITEM = FakeFriendsData.myFriendsData.size - 1 - val thisFriend = FakeFriendsData.myFriendsData[LIST_ITEM] - lateinit var cache: Cache - lateinit var storage: Storage - + private fun createIntent(): Intent { + Firebase.initializeForTest() + val i: Intent = Intent( + InstrumentationRegistry.getInstrumentation().targetContext, + MainActivity::class.java + ) + return i + } @get:Rule - val activityRule = ActivityScenarioRuleWithSetup(MainActivity::class.java, - defaultTestFlow then TestFlow.onlySetup { ServiceLocator.localRepository.setCurrentUid("test") } - ) + val testRule = ActivityScenarioRuleWithSetup.default(createIntent()) @Before fun setUp() { ServiceLocator.setCurrentEnv(Env.Test) - ServiceLocator.localRepository.setCurrentUid("test") - Firebase.initializeForTest() - cache = Cache.initialize(InstrumentationRegistry.getInstrumentation().targetContext) - storage = PrivateStorage(InstrumentationRegistry.getInstrumentation().targetContext) - runBlocking { - cache.setUserDetails(User(uid = "user_test_uid", username = "test name")) - } + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + + val repo = ServiceLocator.getInstance().repository as LocalRepository + + repo.setCurrentUid("player1") + repo.users.clear() + repo.friendRequests.clear() + repo.gamesMap.clear() + + + repo.users["player1"] = User( + "player1", + "player1", + "public", + true, + "p1@example.com" + ) + repo.users["player2"] = User( + "player2", + "player2", + "public", + true, + "p2@example.com" + ) + repo.users["player3"] = User( + "player3", + "player3", + "public", + true, + "p3@example.com" + ) + repo.users["player4"] = User( + "player4", + "player4", + "public", + true, + "p4@example.com" + ) + + val friends: List = listOf( + FriendRequest(listOf("player2","player1"), Timestamp.now() , FriendRequest.Status.ACCEPTED, "player2"), + FriendRequest(listOf("player3","player1"), Timestamp.now(), FriendRequest.Status.PENDING, "player3"), + FriendRequest(listOf("player1","player4"), Timestamp.now(), FriendRequest.Status.ACCEPTED,"player4") + ) + + repo.friendRequests.addAll(friends) } @After fun tearDown() { - val repo = ServiceLocator.getInstance().repository as LocalRepository - repo.setCurrentUid(null) ServiceLocator.setCurrentEnv(Env.Prod) + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + fun testEnv() { + assert(ServiceLocator.getCurrentEnv() == Env.Test) } @Test - fun checkFriendsFragment() { + fun checkFriendsFragment(){ onView(withId(R.id.nav_friends)).perform(click()) onView(withId(R.id.fragment_friends)).check(matches(isDisplayed())) } @@ -77,171 +124,134 @@ class FriendsFragmentTest { } @Test - fun test_FriendInfoFragmentShown_onInfoButtonClick() { + fun test_CheckFriendInfo_onInfoButtonClick() { + val gamesPlayed = 0 + val gamesWon = 0 + val winRate ="0.0" + onView(withId(R.id.nav_friends)).perform(click()) onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) - - onView(withId(R.id.fragment_info_page)).check(matches(isDisplayed())) - } + .perform(actionOnItemAtPosition(0,ClickButtonAction.clickInfoButton(R.id.infoButton))) - @Test - fun test_GameFragmentShown_onPlayButtonClick() { - onView(withId(R.id.nav_friends)).perform(click()) + onView(withId(R.id.userName_infoPage)).check(matches(withText("player2"))) + onView(withId(R.id.gamesPlayedText_infoPage)).check(matches(withText("Games Played: $gamesPlayed"))) + onView(withId(R.id.gamesWonText_infoPage)).check(matches(withText("Games Won: $gamesWon"))) + onView(withId(R.id.winRateText_infoPage)).check(matches(withText("Win Rate: $winRate%"))) - onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickPlayButton(R.id.playButton) - ) - ) - - onView(withId(R.id.fragment_game)).check(matches(isDisplayed())) } @Test - fun test_CorrectGamesPlayedShows_onInfoButtonClick() { - val gamesPlayed = thisFriend.gamesPlayed + fun test_goesToGameFragment_onChoosing1GameMode(){ onView(withId(R.id.nav_friends)).perform(click()) onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) + .perform(actionOnItemAtPosition(0,ClickButtonAction.clickPlayButton(R.id.playButton))) - onView(withId(R.id.gamesPlayedText_infoPage)).check(matches(withText("Games Played: $gamesPlayed"))) + onView(withId(R.id.oneGameRadioBtn)).inRoot(isDialog()).perform(click()) + onView(withId(R.id.confirmButton)).inRoot(isDialog()).perform(click()) + + onView(withId(R.id.matchmaking_fragment)).check(matches(isDisplayed())) } @Test - fun test_CorrectGamesWonShows_onInfoButtonClick() { - val gamesWon = thisFriend.gamesWon + fun test_goesToGameFragment_onChoosing5GameMode(){ onView(withId(R.id.nav_friends)).perform(click()) onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) + .perform(actionOnItemAtPosition(0,ClickButtonAction.clickPlayButton(R.id.playButton))) - onView(withId(R.id.gamesWonText_infoPage)).check(matches(withText("Games Won: $gamesWon"))) + onView(withId(R.id.fiveGameRadioBtn)).inRoot(isDialog()).perform(click()) + onView(withId(R.id.confirmButton)).inRoot(isDialog()).perform(click()) + + onView(withId(R.id.matchmaking_fragment)).check(matches(isDisplayed())) } @Test - fun test_CorrectUserNameShows_onInfoButtonClick() { - + fun test_dialogFragmentCloses_onClickingCancel(){ onView(withId(R.id.nav_friends)).perform(click()) onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) - - onView(withId(R.id.userName_infoPage)).check(matches(withText(thisFriend.username))) + .perform(actionOnItemAtPosition(0,ClickButtonAction.clickPlayButton(R.id.playButton))) + + onView(withId(R.id.cancelButton)).inRoot(isDialog()).perform(click()) + + onView(withId(R.id.fragment_game_mode_dialog)).check(doesNotExist()) } @Test - fun test_CorrectWinRateShows_onInfoButtonClick() { - val winRate = thisFriend.winRate + fun test_nothingHappens_onClickingSubmitWithoutSelection(){ onView(withId(R.id.nav_friends)).perform(click()) onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) + .perform(actionOnItemAtPosition(0,ClickButtonAction.clickPlayButton(R.id.playButton))) + + onView(withId(R.id.confirmButton)).inRoot(isDialog()).perform(click()) + + onView(withId(R.id.fragment_game_mode_dialog)).check(matches(isDisplayed())) - onView(withId(R.id.winRateText_infoPage)).check(matches(withText("Win Rate: $winRate%"))) } + @Test - fun test_offlineStatusShows_onInfoButtonClick() { + fun test_returnsToFriendFragment_onBackButtonClick(){ onView(withId(R.id.nav_friends)).perform(click()) onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) - - onView(withId(R.id.offlineImage_infoPage)).check(matches(isDisplayed())) + .perform(actionOnItemAtPosition(0,ClickButtonAction.clickInfoButton(R.id.infoButton))) + + onView(withId(R.id.infoPage_backButton)).perform(click()) + + onView(withId(R.id.fragment_friends)).check(matches(isDisplayed())) } @Test - fun test_onlineStatusShows_onInfoButtonClick() { + fun test_showsMyFriendReqs_onMyRequestButtonClicked(){ onView(withId(R.id.nav_friends)).perform(click()) - onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - 0, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) - - onView(withId(R.id.onlineImage_infoPage)).check(matches(isDisplayed())) + onView(withId(R.id.requestButton)).perform(click()) + onView(withId(R.id.myFriendReqButton)).perform(click()) + + onView(withId(R.id.myReqsRecyclerView)).check(matches(isDisplayed())) } @Test - fun test_returnsToFriendFragment_onBackButtonClick() { + fun test_showsAddFriendFragment_onAddFriendButtonClicked(){ onView(withId(R.id.nav_friends)).perform(click()) - onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) - - onView(withId(R.id.infoPage_backButton)).perform(click()) + onView(withId(R.id.requestButton)).perform(click()) + onView(withId(R.id.addFriendsButton)).perform(click()) - onView(withId(R.id.fragment_friends)).check(matches(isDisplayed())) + onView(withId(R.id.addFriendFragment)).check(matches(isDisplayed())) } @Test - fun test_goesToGameFragment_onPlayButtonClick() { + fun test_FriendAdded_onAcceptedRequest(){ onView(withId(R.id.nav_friends)).perform(click()) + onView(withId(R.id.requestButton)).perform(click()) + onView(withId(R.id.myFriendReqButton)).perform(click()) - onView(withId(R.id.friendListRecyclerView)) - .perform( - actionOnItemAtPosition( - LIST_ITEM, - ClickButtonAction.clickInfoButton(R.id.infoButton) - ) - ) + onView(withId(R.id.myReqsRecyclerView)) + .perform(actionOnItemAtPosition(0,ClickButtonAction.clickInfoButton(R.id.acceptButton))) - onView(withId(R.id.infoPage_playButton)).perform(click()) + pressBack() - onView(withId(R.id.fragment_game)).check(matches(isDisplayed())) + onView(withId(R.id.friendListRecyclerView)).check(matches(isDisplayed())) + onView(withId(R.id.friendListRecyclerView)) + .perform(actionOnItemAtPosition(1,ClickButtonAction.clickInfoButton(R.id.infoButton))) + + onView(withId(R.id.userName_infoPage)).check(matches(withText("player3"))) } -} + +} class ClickButtonAction { companion object { fun clickInfoButton(childId: Int): ViewAction { return object : ViewAction { override fun getConstraints(): Matcher { - return isAssignableFrom(ImageButton::class.java) + return isAssignableFrom(ImageButton::class.java) } override fun getDescription(): String { diff --git a/app/src/credentials.json b/app/src/credentials.json index 5ec6e134..5b7d8ef9 100644 --- a/app/src/credentials.json +++ b/app/src/credentials.json @@ -1,10 +1 @@ -{ - "web": { - "client_id": "896605409351-sfmsu9jivh9dg2rn9ciu9jevv3van4l1.apps.googleusercontent.com", - "project_id": "rps-sdp", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_secret": "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" - } -} \ No newline at end of file +{"web":{"client_id":"896605409351-sfmsu9jivh9dg2rn9ciu9jevv3van4l1.apps.googleusercontent.com","project_id":"rps-sdp","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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"}} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5128b9d6..c2268418 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,7 +44,9 @@ + android:theme="@style/Theme.RockPaperScissors.NoActionBar" + android:windowSoftInputMode="adjustNothing" + /> ? = null private var leaderBoardData: List? = null + private lateinit var myFriends: List + private lateinit var myFriendReqs: List private val repo get() = repoOverride ?: ServiceLocator.getInstance().repository private var repoOverride: Repository? = null @@ -95,6 +100,7 @@ class Cache private constructor(ctx: Context, val preferFresh: Boolean = false) storage.writeBackUser(user) } + /** * Updates the stats data in both firebase and local storage. * @param statsData The stats data to be updated @@ -122,8 +128,9 @@ class Cache private constructor(ctx: Context, val preferFresh: Boolean = false) if (!isInternetAvailable()) { return getUserPicture() } - if (user == null) + if(user == null) return null + userPicture = repo.getUserProfilePictureImage(user!!.uid) Log.d("UserPic", userPicture.toString()) userPicture?.let { storage.writeBackUserPicture(it) } @@ -140,6 +147,7 @@ class Cache private constructor(ctx: Context, val preferFresh: Boolean = false) storage.writeBackUserPicture(bitmap) } + /** * Retrieves the stats data from cache or local storage. * @return the stats data for the user. @@ -175,16 +183,19 @@ class Cache private constructor(ctx: Context, val preferFresh: Boolean = false) } } + /** * MAINLY FOR TESTING * Updates the leaderboard data in cache and localStorage * @param lBData the new leaderboard data */ fun updateLeaderBoardData(lBData: List) { + leaderBoardData = lBData storage.writeBackLeaderBoardData(lBData) } + /** * Gets leaderboard data from cache or local storage * @return A list of LeaderBoardInfo fetched from local storage or cache. @@ -195,6 +206,7 @@ class Cache private constructor(ctx: Context, val preferFresh: Boolean = false) return leaderBoardData!! } + /** * Gets leaderboard data firebase. * @param position the position of the leaderboard to load. @@ -220,6 +232,50 @@ class Cache private constructor(ctx: Context, val preferFresh: Boolean = false) } } + fun getFriends():List { + if(::myFriends.isInitialized) return myFriends + myFriends = storage.getFriends() ?: listOf() + return myFriends + } + + suspend fun getFriendsAsync():List{ + if(!isInternetAvailable()) { + Log.d("CACHE", "INTERNET NOT AVAILABLE") + return getFriends() + } + myFriends = FirebaseHelper.getFriends() + Log.d("Cache", myFriends.size.toString()) + storage.writeBackFriends(myFriends) + return myFriends + } + + fun getFriendReqs():List { + if(::myFriendReqs.isInitialized) return myFriendReqs + myFriendReqs = storage.getFriendReqs() ?: listOf() + return myFriendReqs + } + + suspend fun getFriendReqsAsync():List{ + if(!isInternetAvailable()) { + Log.d("CACHE", "INTERNET NOT AVAILABLE") + return getFriendReqs() + } + myFriendReqs = FirebaseHelper.getFriendReqs() + Log.d("Cache", myFriends.size.toString()) + storage.writeBackFriendReqs(myFriendReqs) + return myFriendReqs + } + + fun isInternetAvailable(): Boolean { + return try { + val ipAddr: InetAddress = InetAddress.getByName("www.google.com") + //You can replace it with your name + !ipAddr.equals("") + } catch (e: Exception) { + Log.d("Cache", e.toString()) + false + } + } /** * Clears the cache and local storage. */ @@ -259,5 +315,4 @@ class Cache private constructor(ctx: Context, val preferFresh: Boolean = false) } } - } \ No newline at end of file diff --git a/app/src/main/java/ch/epfl/sweng/rps/persistence/PrivateStorage.kt b/app/src/main/java/ch/epfl/sweng/rps/persistence/PrivateStorage.kt index 756b0797..3a184984 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/persistence/PrivateStorage.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/persistence/PrivateStorage.kt @@ -3,9 +3,13 @@ package ch.epfl.sweng.rps.persistence import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import ch.epfl.sweng.rps.models.* +import com.google.gson.Gson import android.net.Uri import ch.epfl.sweng.rps.models.remote.LeaderBoardInfo import ch.epfl.sweng.rps.models.remote.User +import ch.epfl.sweng.rps.models.ui.FriendRequestInfo +import ch.epfl.sweng.rps.models.ui.FriendsInfo import ch.epfl.sweng.rps.models.ui.UserStat import com.google.gson.* import java.io.ByteArrayOutputStream @@ -50,6 +54,47 @@ class PrivateStorage constructor(val context: Context) : Storage { return arr.toList() } + override fun removeFile(file: Storage.FILES): Boolean { + return false + } + + override fun getUserDetails(): User? { + return null + } + + override fun getFriends(): List? { + val statsFile = getFile(Storage.FILES.FRIENDS) + if (!statsFile.exists()) + return null + val json = statsFile.readText() + val arr = Gson().fromJson(json, Array::class.java) + return arr.toList() + } + + override fun getFriendReqs(): List? { + val statsFile = getFile(Storage.FILES.REQUESTS) + if (!statsFile.exists()) + return null + val json = statsFile.readText() + val arr = Gson().fromJson(json, Array::class.java) + return arr.toList() + } + + override fun writeBackFriends(data : List){ + val gson = Gson() + val json = gson.toJson(data.toTypedArray(), Array::class.java) + val f = getFile(Storage.FILES.FRIENDS) + f.writeText(json) + } + + override fun writeBackFriendReqs(data : List){ + val gson = Gson() + val json = gson.toJson(data.toTypedArray(), Array::class.java) + val f = getFile(Storage.FILES.REQUESTS) + f.writeText(json) + } + + override fun getLeaderBoardData(): List? { val leaderBoardFile = getFile(Storage.FILES.LEADERBOARDDATA) if (!leaderBoardFile.exists()) diff --git a/app/src/main/java/ch/epfl/sweng/rps/persistence/Storage.kt b/app/src/main/java/ch/epfl/sweng/rps/persistence/Storage.kt index 70912a10..f546bbe7 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/persistence/Storage.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/persistence/Storage.kt @@ -1,27 +1,37 @@ package ch.epfl.sweng.rps.persistence - import android.graphics.Bitmap import ch.epfl.sweng.rps.models.remote.LeaderBoardInfo import ch.epfl.sweng.rps.models.remote.User +import ch.epfl.sweng.rps.models.ui.FriendRequestInfo +import ch.epfl.sweng.rps.models.ui.FriendsInfo import ch.epfl.sweng.rps.models.ui.UserStat import java.io.File interface Storage { + fun removeFile(file : FILES) : Boolean + fun getUserDetails() : User? + fun getFriends(): List? + fun getFriendReqs(): List? fun getFile(file: FILES): File fun deleteFile(file: FILES): Boolean fun getUser(): User? fun getStatsData(): List? fun getLeaderBoardData(): List? fun writeBackUser(user: User) - fun writeBackStatsData(data: List) - fun writeBackLeaderBoardData(data: List) + fun writeBackStatsData(data : List) + fun writeBackLeaderBoardData(data : List) fun writeBackUserPicture(bitmap: Bitmap) fun getUserPicture(): Bitmap? - enum class FILES(val file: String) { + fun writeBackFriends(friends: List) + fun writeBackFriendReqs(reqs: List) + + enum class FILES(val file:String){ USERINFO("USER"), LEADERBOARDDATA("LBDATA"), USERPICTURE("USERPICTURE"), - STATSDATA("STATSDATA") + STATSDATA("STATSDATA"), + FRIENDS("FRIENDS"), + REQUESTS("REQUESTS") } enum class Origin { diff --git a/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseHelper.kt b/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseHelper.kt index f53dc3fc..ad2efdd0 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseHelper.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseHelper.kt @@ -1,12 +1,11 @@ package ch.epfl.sweng.rps.remote + import android.net.Uri import ch.epfl.sweng.rps.R -import ch.epfl.sweng.rps.models.remote.Hand -import ch.epfl.sweng.rps.models.remote.LeaderBoardInfo -import ch.epfl.sweng.rps.models.remote.User -import ch.epfl.sweng.rps.models.ui.RoundStat -import ch.epfl.sweng.rps.models.ui.UserStat +import ch.epfl.sweng.rps.models.* +import ch.epfl.sweng.rps.models.remote.* +import ch.epfl.sweng.rps.models.ui.* import ch.epfl.sweng.rps.services.ServiceLocator import ch.epfl.sweng.rps.utils.Option import java.text.SimpleDateFormat @@ -68,6 +67,7 @@ object FirebaseHelper { gameRounds.map { it.value.computeScores() }.filter { it.isNotEmpty() } val userScore = allRoundScores.sumOf { scores -> val max = scores.maxOf { it.points } + if (scores.any { it.points == max && it.uid == userid && !scores.all { score -> score.points == max } }) 1L else @@ -177,6 +177,7 @@ object FirebaseHelper { repo.getUserProfilePictureUrl(score.uid).asData?.value.let { Uri.parse(it.toString()) } if (leaderBoardInfo.userProfilePictureUrl == null) { leaderBoardInfo.userProfilePictureUrl = + Uri.parse("android.resource://ch.epfl.sweng.rps/" + R.drawable.profile_img) } @@ -185,6 +186,78 @@ object FirebaseHelper { } return allPlayers } + /** + * This function returns the your friends. + */ + suspend fun getFriends(): List { + val fbRepo = ServiceLocator.getInstance().repository + val userId = fbRepo.rawCurrentUid() + val friends = fbRepo.friends.getFriends() + val friendList = mutableListOf() + + for (friend in friends) { + val user = fbRepo.getUser(friend) + val userStats = fbRepo.games.statsOfUser(friend) + val friendsInfo = userId?.let { + FriendsInfo( + username = user.asData?.value?.username?: "UsernameEmpty", + uid = it, + gamesPlayed = userStats.total_games, + gamesWon = userStats.wins, + winRate = userStats.winRate, + isOnline = true) + } + + if (friendsInfo != null) { + friendList.add(friendsInfo) + } + } + return friendList + } + /** + * This function returns your friend requests. + */ + suspend fun getFriendReqs(): List { + val fbRepo = ServiceLocator.getInstance().repository + val friendRequest = fbRepo.friends.listFriendRequests() + val reqList = mutableListOf() + val uid = fbRepo.rawCurrentUid() + + for (req in friendRequest) { + if (req.from != uid && req.status == FriendRequest.Status.PENDING) { + val user = fbRepo.getUser(req.from) + val friendsReq = FriendRequestInfo( + username = user.asData?.value?.username?: "UsernameEmpty", + uid = req.from + ) + reqList.add(friendsReq) + } + } + return reqList + } + + /* suspend fun getInvites(): List { + val fbRepo = ServiceLocator.getInstance().repository + val uid = fbRepo.rawCurrentUid() + val friendInvite = uid?.let { fbRepo.friends.listInvitations(it) } + val myInvites = mutableListOf() + + + if (friendInvite != null) { + for (invite in friendInvite) { + if (invite.from != uid && invite.id == uid){ + val user = fbRepo.getUser(invite.from) + val invitation = InvitationInfo( + username = user.asData?.value?.username?: "UsernameEmpty", + uid = invite.from + ) + myInvites.add(invitation) + } + } + } + return myInvites + } */ + } diff --git a/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseReferences.kt b/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseReferences.kt index 03b65335..fa6981d4 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseReferences.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/remote/FirebaseReferences.kt @@ -7,8 +7,8 @@ import com.google.firebase.storage.FirebaseStorage * Instantiating this class necessitates the Firebase SDK to be initialized. */ class FirebaseReferences { - private val root = FirebaseFirestore.getInstance() - private val storageRoot = FirebaseStorage.getInstance().reference + val root = FirebaseFirestore.getInstance() + val storageRoot = FirebaseStorage.getInstance().reference /** * Thefriend request collection. diff --git a/app/src/main/java/ch/epfl/sweng/rps/remote/LocalRepository.kt b/app/src/main/java/ch/epfl/sweng/rps/remote/LocalRepository.kt index 5bb5ef04..e2e8a028 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/remote/LocalRepository.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/remote/LocalRepository.kt @@ -21,7 +21,7 @@ class LocalRepository(private var uid: String? = null) : Repository, GamesReposi @VisibleForTesting internal val users = mutableMapOf() - private val friendRequests = mutableMapOf>() + val friendRequests = mutableListOf() override val friends: FriendsRepository get() = this override val games: GamesRepository @@ -79,32 +79,28 @@ class LocalRepository(private var uid: String? = null) : Repository, GamesReposi } override suspend fun sendFriendRequestTo(uid: String) { - val map = (friendRequests[uid] ?: mutableMapOf()) - map[getCurrentUid()] = FriendRequest.build(getCurrentUid(), uid, Timestamp.now()) - friendRequests[uid] = map + val fr = FriendRequest.build(getCurrentUid(), uid, Timestamp.now()) + friendRequests.add(fr) } override suspend fun listFriendRequests(): List { - return friendRequests[getCurrentUid()]?.entries?.map { it.value } ?: emptyList() + return friendRequests } override suspend fun getFriends(): List { - return friendRequests[getCurrentUid()] - ?.filter { it.value.status == FriendRequest.Status.ACCEPTED } - ?.map { entry -> entry.value.users.first { it != getCurrentUid() } } - ?.toList() ?: emptyList() + return friendRequests.filter { it.status == FriendRequest.Status.ACCEPTED } + .map { it.users.first { it != getCurrentUid() } } } override suspend fun changeFriendRequestToStatus( userUid: String, status: FriendRequest.Status ) { - friendRequests[getCurrentUid()]?.set( - userUid, - friendRequests[getCurrentUid()]!![userUid]!!.copy(status = status) - ) + val i = friendRequests.indexOfFirst { it.users.contains(userUid) } + friendRequests[i] = friendRequests[i].copy(status = status) } + override suspend fun getGame(gameId: String): Game? { return gamesMap[gameId] } diff --git a/app/src/main/java/ch/epfl/sweng/rps/remote/games/FirebaseGamesRepository.kt b/app/src/main/java/ch/epfl/sweng/rps/remote/games/FirebaseGamesRepository.kt index 87950d79..703d9d1a 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/remote/games/FirebaseGamesRepository.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/remote/games/FirebaseGamesRepository.kt @@ -46,7 +46,7 @@ class FirebaseGamesRepository(internal val repository: FirebaseRepository) : Gam override suspend fun statsOfUser(uid: String): UserStats { return firebase.usersCollection.document(uid).collection("stats").document("games").get() - .await().toObject()!! + .await().toObject()?: UserStats(uid,0,0) } override suspend fun listInvitations(): List { diff --git a/app/src/main/java/ch/epfl/sweng/rps/services/OfflineGameService.kt b/app/src/main/java/ch/epfl/sweng/rps/services/OfflineGameService.kt index f5915811..1ed8d9bb 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/services/OfflineGameService.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/services/OfflineGameService.kt @@ -1,11 +1,12 @@ package ch.epfl.sweng.rps.services +import ch.epfl.sweng.rps.remote.Repository +import ch.epfl.sweng.rps.models.* import ch.epfl.sweng.rps.models.remote.Game import ch.epfl.sweng.rps.models.remote.GameMode import ch.epfl.sweng.rps.models.remote.Hand import ch.epfl.sweng.rps.models.remote.Round import ch.epfl.sweng.rps.models.xbstract.ComputerPlayer -import ch.epfl.sweng.rps.remote.Repository import com.google.firebase.Timestamp import kotlinx.coroutines.delay @@ -18,6 +19,9 @@ class OfflineGameService( val artificialMovesDelay: Long = DEFAULT_DELAY ) : GameService() { + companion object { + private const val DEFAULT_DELAY = 100L + } private var _disposed = false @@ -31,26 +35,8 @@ class OfflineGameService( get() { return game?.done ?: return false } - private val currentHands get() = currentRound.hands as MutableMap - override val currentGame: Game - get() { - checkNotDisposed() - if (game == null) { - throw error - ?: GameServiceException("Game service has not received a game yet") - } else { - return game!! - } - } - override val currentRound: Round - get() = game!!.rounds[game!!.current_round.toString()]!! - override val isGameFull: Boolean - get() = true - override val isDisposed: Boolean - get() = _disposed - override val started: Boolean - get() = game != null - override val imTheOwner get() = true + + override suspend fun addRound(): Round { checkNotDisposed() val round = Round.Rps( @@ -62,10 +48,13 @@ class OfflineGameService( return round } + private fun setRound(round: Round) { (currentGame.rounds as MutableMap)[game!!.current_round.toString()] = round } + private val currentHands get() = currentRound.hands as MutableMap + override suspend fun playHand(hand: Hand) { checkNotDisposed() val me: String = repository.getCurrentUid() @@ -92,6 +81,24 @@ class OfflineGameService( } } + override val currentGame: Game + get() { + checkNotDisposed() + if (game == null) { + throw error + ?: GameServiceException("Game service has not received a game yet") + } else { + return game!! + } + } + + override val currentRound: Round + get() = game!!.rounds[game!!.current_round.toString()]!! + + override val isGameFull: Boolean + get() = true + + override fun dispose() { checkNotDisposed() _disposed = true @@ -101,7 +108,14 @@ class OfflineGameService( override fun stopListening() { } + override val isDisposed: Boolean + get() = _disposed + + override val started: Boolean + get() = game != null + override suspend fun refreshGame(): Game = game!! + override fun startListening(): GameService { checkNotDisposed() val round = Round.Rps( @@ -127,6 +141,8 @@ class OfflineGameService( } } + override val imTheOwner get() = true + override suspend fun awaitForAllHands() { awaitFor { currentRound.hands.size == 2 }// 2 is the number of players, for now hardcoded } @@ -134,8 +150,4 @@ class OfflineGameService( override suspend fun awaitForRoundAdded() { awaitFor { true } } - - companion object { - private const val DEFAULT_DELAY = 100L - } } \ No newline at end of file diff --git a/app/src/main/java/ch/epfl/sweng/rps/services/OfflineTicTacToe.kt b/app/src/main/java/ch/epfl/sweng/rps/services/OfflineTicTacToe.kt index b40e5f02..35525166 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/services/OfflineTicTacToe.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/services/OfflineTicTacToe.kt @@ -23,9 +23,9 @@ class OfflineTicTacToe(val view: TicTacToeFragment, player: MOVES) : TicTacToeGa if (calculate()) return val opponent = if (player == MOVES.CROSS) MOVES.CIRCLE else MOVES.CROSS while (true) { - val oppChoice = player2.makeMove() - val cell = oppChoice.second - val row = oppChoice.first + var oppChoice = player2.makeMove() + var cell = oppChoice.second + var row = oppChoice.first if (matrix[row][cell] == MOVES.EMPTY) { matrix[row][cell] = opponent view.updateUI((row * 3) + cell, opponent) diff --git a/app/src/main/java/ch/epfl/sweng/rps/services/ProdServiceLocator.kt b/app/src/main/java/ch/epfl/sweng/rps/services/ProdServiceLocator.kt index d93b6833..7c1f59a7 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/services/ProdServiceLocator.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/services/ProdServiceLocator.kt @@ -3,7 +3,6 @@ package ch.epfl.sweng.rps.services import ch.epfl.sweng.rps.remote.Env import ch.epfl.sweng.rps.remote.FirebaseReferences import ch.epfl.sweng.rps.remote.FirebaseRepository -import ch.epfl.sweng.rps.remote.LocalRepository /** * ServiceLocator is a singleton class that provides access to the different services @@ -14,18 +13,18 @@ import ch.epfl.sweng.rps.remote.LocalRepository * */ class ProdServiceLocator : ServiceLocator { - val firebaseReferences by lazy { FirebaseReferences() } + override val repository by lazy { FirebaseRepository.createInstance(firebaseReferences) } - override val env: Env = Env.Prod - private val gameServices = mutableMapOf() - override val cachedGameServices: List - get() = gameServices.keys.toList() - override val matchmakingService: MatchmakingService by lazy { MatchmakingService() } + override fun dispose() { disposeAllGameServices() } + override val env: Env = Env.Prod + + private val gameServices = mutableMapOf() + override fun getGameServiceForGame(gameId: String, start: Boolean): FirebaseGameService { cleanUpServices() val service = gameServices.getOrPut(gameId) { @@ -58,4 +57,10 @@ class ProdServiceLocator : ServiceLocator { } + override val cachedGameServices: List + get() = gameServices.keys.toList() + + override val matchmakingService: MatchmakingService by lazy { MatchmakingService() } + + } \ No newline at end of file diff --git a/app/src/main/java/ch/epfl/sweng/rps/services/TicTacToeOpponent.kt b/app/src/main/java/ch/epfl/sweng/rps/services/TicTacToeOpponent.kt index 95d56038..707d2ac7 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/services/TicTacToeOpponent.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/services/TicTacToeOpponent.kt @@ -1,5 +1,5 @@ package ch.epfl.sweng.rps.services interface TicTacToeOpponent { - fun makeMove(): Pair + fun makeMove():Pair } \ No newline at end of file diff --git a/app/src/main/java/ch/epfl/sweng/rps/ui/friends/AddFriendFragment.kt b/app/src/main/java/ch/epfl/sweng/rps/ui/friends/AddFriendFragment.kt new file mode 100644 index 00000000..ccadc186 --- /dev/null +++ b/app/src/main/java/ch/epfl/sweng/rps/ui/friends/AddFriendFragment.kt @@ -0,0 +1,59 @@ +package ch.epfl.sweng.rps.ui.friends + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.EditText +import android.widget.Toast +import androidx.lifecycle.lifecycleScope +import ch.epfl.sweng.rps.R +import ch.epfl.sweng.rps.services.ServiceLocator +import kotlinx.coroutines.launch + + +class AddFriendFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_add_friend, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val sendReqBtn = view.findViewById