diff --git a/app/build.gradle b/app/build.gradle index e5484a4b..4e84460a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.application' + id 'com.google.android.gms.oss-licenses-plugin' id 'kotlin-android' id 'jacoco' } @@ -11,6 +12,14 @@ jacoco { } android { + signingConfigs { + release { + storeFile file('key.jks') + storePassword '123456' + keyPassword '123456' + keyAlias 'key0' + } + } compileSdk 31 defaultConfig { @@ -21,12 +30,14 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + signingConfig signingConfigs.debug } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release } debug { testCoverageEnabled true @@ -69,10 +80,11 @@ dependencies { implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1' implementation 'androidx.navigation:navigation-ui-ktx:2.4.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.preference:preference-ktx:1.2.0' - androidTestImplementation 'org.jetbrains.kotlin:kotlin-test' - androidTestImplementation(platform("org.junit:junit-bom:5.8.2")) - androidTestImplementation("org.junit.jupiter:junit-jupiter") + // androidTestImplementation 'org.jetbrains.kotlin:kotlin-test' + // androidTestImplementation(platform("org.junit:junit-bom:5.8.2")) + // androidTestImplementation("org.junit.jupiter:junit-jupiter") implementation 'org.jetbrains.kotlin:kotlin-test' implementation(platform("org.junit:junit-bom:5.8.2")) @@ -106,6 +118,10 @@ dependencies { androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0-native-mt' testImplementation 'org.mockito:mockito-core:4.4.0' + + implementation 'com.google.android.material:material:1.5.0' + + implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' } diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 00000000..689981a6 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "ch.epfl.sweng.rps", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File" +} \ No newline at end of file diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/GameButtonsTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/GameButtonsTest.kt index cfcb0903..e89d495e 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/GameButtonsTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/GameButtonsTest.kt @@ -1,45 +1,46 @@ package ch.epfl.sweng.rps -import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest import org.hamcrest.Matchers.not import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@SmallTest class GameButtonsTest { @get:Rule val testRule = ActivityScenarioRule(MainActivity::class.java) + @Test - fun pressedRock(){ + fun pressedRock() { checkPressedButton(R.id.rockRB) } + @Test - fun pressedPaper(){ + fun pressedPaper() { checkPressedButton(R.id.paperRB) } + @Test fun pressedScissors() { checkPressedButton(R.id.scissorsRB) } - private fun checkPressedButton(radioButtonId: Int){ - onView(withId(R.id.button_play_one_offline_game)).perform(click()) + + private fun checkPressedButton(radioButtonId: Int) { + onView(withId(R.id.button_play_1_games_offline)).perform(click()) onView(withId(radioButtonId)).perform(click()) onView(withId(radioButtonId)).check(matches(isChecked())) } + @Test - fun pressedRockPaperRock(){ - onView(withId(R.id.button_play_one_offline_game)).perform(click()) + fun pressedRockPaperRock() { + onView(withId(R.id.button_play_1_games_offline)).perform(click()) onView(withId(R.id.rockRB)).perform(click()) onView(withId(R.id.paperRB)).perform(click()) onView(withId(R.id.rockRB)).perform(click()) @@ -47,6 +48,4 @@ class GameButtonsTest { onView(withId(R.id.paperRB)).check(matches(not(isChecked()))) onView(withId(R.id.scissorsRB)).check(matches(not(isChecked()))) } - - } \ No newline at end of file diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/MainActivityTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/MainActivityTest.kt index 65f8637c..79e4217b 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/MainActivityTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/MainActivityTest.kt @@ -1,13 +1,9 @@ package ch.epfl.sweng.rps -import android.view.Gravity import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.DrawerActions -import androidx.test.espresso.contrib.DrawerMatchers.isClosed -import androidx.test.espresso.contrib.NavigationViewActions import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.rules.ActivityScenarioRule diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/ProfileFragmentTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/ProfileFragmentTest.kt index 51b6d466..e57595fb 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/ProfileFragmentTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/ProfileFragmentTest.kt @@ -2,49 +2,60 @@ package ch.epfl.sweng.rps import android.content.Intent import android.os.Bundle -import android.view.Gravity -import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.DrawerActions -import androidx.test.espresso.contrib.DrawerMatchers -import androidx.test.espresso.contrib.NavigationViewActions import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.rule.ActivityTestRule +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.platform.app.InstrumentationRegistry import ch.epfl.sweng.rps.models.User -import org.junit.Before import org.junit.Rule import org.junit.Test + class ProfileFragmentTest { - val i: Intent = Intent() - val b: Bundle = Bundle() - val data = mapOf( - "email" to "asd@gmail.com", - "display_name" to "asdino", - "uid" to "123", - "privacy" to User.Privacy.PUBLIC.toString() - ) - - @Before - fun setUpIntent() { - data.forEach { b.putString(it.key, it.value) } - i.putExtra("User", b) + + private val bundle = run { + val b: Bundle = Bundle() + val data = mapOf( + "email" to "asd@gmail.com", + "display_name" to "asdino", + "uid" to "123", + "privacy" to User.Privacy.PUBLIC.toString() + ) + data.forEach { (k, v) -> b.putString(k, v) } + b + } + + + private fun createIntent(): Intent { + val i: Intent = Intent( + InstrumentationRegistry.getInstrumentation().targetContext, + MainActivity::class.java + ) + i.putExtra("User", bundle) + return i } @get:Rule - val testRule = ActivityTestRule(MainActivity::class.java, false, false) + val testRule = ActivityScenarioRule(createIntent()) @Test fun testFields() { - testRule.launchActivity(i) onView(withId(R.id.nav_profile)).perform(click()) - onView(withId(R.id.TextDisplayName)).check(matches(withText(b.getString("display_name")))) - onView(withId(R.id.TextEmail)).check(matches(withText(b.getString("email")))) - onView(withId(R.id.TextPrivacy)).check(matches(withText(b.getString("privacy")))) + onView(withId(R.id.TextDisplayName)).check(matches(withText(bundle.getString("display_name")))) + onView(withId(R.id.TextEmail)).check(matches(withText(bundle.getString("email")))) + onView(withId(R.id.TextPrivacy)).check(matches(withText(bundle.getString("privacy")))) + } + + @Test + fun tapSettings() { + onView(withId(R.id.nav_profile)).perform(click()) + onView(withId(R.id.profile_appbar_settings_btn)).perform(click()) + onView(withId(R.id.settings)).check(matches(isDisplayed())) + pressBack() + onView(withId(R.id.profile_appbar_settings_btn)).check(matches(isDisplayed())) } } \ No newline at end of file diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/SettingsPageTest.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/SettingsPageTest.kt new file mode 100644 index 00000000..88cf479e --- /dev/null +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/SettingsPageTest.kt @@ -0,0 +1,105 @@ +package ch.epfl.sweng.rps + +import android.util.Log +import androidx.appcompat.app.AppCompatDelegate +import androidx.lifecycle.Lifecycle +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import ch.epfl.sweng.rps.TestUtils.getActivityInstance +import ch.epfl.sweng.rps.TestUtils.waitFor +import ch.epfl.sweng.rps.ui.settings.SettingsActivity +import com.google.android.gms.oss.licenses.OssLicensesMenuActivity +import org.hamcrest.Matchers.instanceOf +import org.junit.Rule +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.runner.RunWith +import java.util.concurrent.FutureTask + + +@RunWith(AndroidJUnit4::class) +class SettingsPageTest { + @get:Rule + val scenarioRule = ActivityScenarioRule(SettingsActivity::class.java) + + private fun computeThemeMap(): List> { + val targetContext = getInstrumentation().targetContext + val entries = targetContext.resources.getStringArray(R.array.theme_entries) + val values = targetContext.resources.getStringArray(R.array.theme_values) + val l = entries.zip(values) + return (l + l).toMap().entries.toList() + } + + private val themeIdToAppCompatThemeId = mapOf( + "light" to AppCompatDelegate.MODE_NIGHT_NO, + "dark" to AppCompatDelegate.MODE_NIGHT_YES, + "system" to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + ) + + private fun getAppCompatThemeFromThemeId(themeId: String): Int { + return themeIdToAppCompatThemeId[themeId]!! + } + + private fun getThemeIdFromAppCompatTheme(appCompatThemeId: Int): String { + val filtered = themeIdToAppCompatThemeId.entries.filter { it.value == appCompatThemeId } + if (filtered.isEmpty()) { + throw IllegalArgumentException("No theme id found for appCompatThemeId $appCompatThemeId") + } + return filtered.first().key + } + + + private fun getCurrentNightMode(): Int { + val activity = getActivityInstance() + + val futureResult = FutureTask { AppCompatDelegate.getDefaultNightMode() } + + activity.runOnUiThread(futureResult) + + return futureResult.get() + } + + @Test + fun testSettingsPage() { + onView(withId(R.id.settings)).check(matches(isDisplayed())) + + // the activity's onCreate, onStart and onResume methods have been called at this point + scenarioRule.scenario.moveToState(Lifecycle.State.STARTED) + // the activity's onPause method has been called at this point + scenarioRule.scenario.moveToState(Lifecycle.State.RESUMED) + + for (entry in computeThemeMap()) { + Log.i("SettingsPageTest", "Testing theme ${entry.key}") + onView(withText(R.string.theme_mode)).perform(click()) + onView(withText(entry.key)).perform(click()) + onView(isRoot()).perform(waitFor(1000)) + + val appCompatThemeId = getCurrentNightMode() + + assertEquals( + getAppCompatThemeFromThemeId(entry.value), + appCompatThemeId, + "Theme should be ${entry.value} (${getAppCompatThemeFromThemeId(entry.value)}) after clicking ${entry.key}, but is $appCompatThemeId (${ + getThemeIdFromAppCompatTheme(appCompatThemeId) + })" + ) + + } + } + + + @Test + fun testSettingsPageLicense() { + onView(withId(R.id.settings)).check(matches(isDisplayed())) + onView(withText(R.string.license_title)).perform(click()) + + val currentActivity = getActivityInstance() + assertThat(currentActivity, instanceOf(OssLicensesMenuActivity::class.java)) + onView(withText(R.string.oss_license_title)).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 03af5fc3..ee3e4c32 100644 --- a/app/src/androidTest/java/ch/epfl/sweng/rps/StatisticsFragmentTest.kt +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/StatisticsFragmentTest.kt @@ -1,24 +1,16 @@ package ch.epfl.sweng.rps -import android.view.Gravity import android.view.View -import androidx.test.espresso.Espresso import androidx.test.espresso.PerformException import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.contrib.DrawerActions -import androidx.test.espresso.contrib.DrawerMatchers -import androidx.test.espresso.contrib.NavigationViewActions -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.util.HumanReadables import androidx.test.espresso.util.TreeIterables import androidx.test.ext.junit.rules.ActivityScenarioRule import org.hamcrest.Matcher import org.junit.Rule -import org.junit.Test import java.util.concurrent.TimeoutException diff --git a/app/src/androidTest/java/ch/epfl/sweng/rps/TestUtils.kt b/app/src/androidTest/java/ch/epfl/sweng/rps/TestUtils.kt new file mode 100644 index 00000000..e206fb54 --- /dev/null +++ b/app/src/androidTest/java/ch/epfl/sweng/rps/TestUtils.kt @@ -0,0 +1,45 @@ +package ch.epfl.sweng.rps + +import android.app.Activity +import android.util.Log +import android.view.View +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry +import androidx.test.runner.lifecycle.Stage +import org.hamcrest.Matcher +import java.util.concurrent.TimeoutException + +object TestUtils { + inline fun getActivityInstance(): T { + var activity: Activity? = null + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val resumedActivities: Collection = + ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED) + Log.i("SettingsPageTest", "resumedActivities: $resumedActivities") + if (resumedActivities.iterator().hasNext()) { + activity = resumedActivities.iterator().next() + } + } + if (activity == null) { + throw TimeoutException("No activity found") + } + if (activity !is T) { + throw IllegalStateException("Activity is not of type ${T::class.java.simpleName}") + } + return activity!! as T + } + + fun waitFor(delay: Long): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher = ViewMatchers.isRoot() + override fun getDescription(): String = "wait for $delay milliseconds" + override fun perform(uiController: UiController, v: View?) { + uiController.loopMainThreadForAtLeast(delay) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c29b93f2..4ad1f845 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,11 +8,19 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.RockPaperScissors" > + android:theme="@style/Theme.RockPaperScissors"> + + @@ -20,13 +28,6 @@ - - - \ No newline at end of file diff --git a/app/src/main/java/ch/epfl/sweng/rps/MainActivity.kt b/app/src/main/java/ch/epfl/sweng/rps/MainActivity.kt index e0070a30..3875bfe5 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/MainActivity.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/MainActivity.kt @@ -8,42 +8,47 @@ import androidx.appcompat.app.AppCompatActivity import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController +import androidx.preference.PreferenceManager import ch.epfl.sweng.rps.databinding.ActivityMainBinding import com.google.android.material.bottomnavigation.BottomNavigationView import ch.epfl.sweng.rps.models.User +import ch.epfl.sweng.rps.ui.settings.SettingsActivity import com.google.android.material.navigation.NavigationView class MainActivity : AppCompatActivity() { + private val sharedPreferences by lazy { + PreferenceManager.getDefaultSharedPreferences(this) + } + private lateinit var binding: ActivityMainBinding private lateinit var currentUser: User - override fun onCreate(savedInstanceState: Bundle?) { - var userData: Bundle? = intent.extras?.getBundle("User") - if (userData != null) { - currentUser = User( - userData.getString("display_name"), - userData.getString("uid")!!, - userData.getString("privacy")!!, - false, - userData.getString("email") - ) - } - super.onCreate(savedInstanceState) - - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - - val navView: BottomNavigationView = binding.navView + override fun onCreate(savedInstanceState: Bundle?) { + var userData: Bundle? = intent.extras?.getBundle("User") + if (userData != null) { + currentUser = User( + userData.getString("display_name"), + userData.getString("uid")!!, + userData.getString("privacy")!!, + false, + userData.getString("email") + ) + } + super.onCreate(savedInstanceState) - val navController = findNavController(R.id.nav_host_fragment_activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + val navView: BottomNavigationView = binding.navView + val navController = findNavController(R.id.nav_host_fragment_activity_main) + navView.setupWithNavController(navController) - navView.setupWithNavController(navController) - } + SettingsActivity.applyTheme(getString(R.string.theme_pref_key), sharedPreferences) + } - fun getUserDetails() : User{ + fun getUserDetails(): User { return currentUser } } diff --git a/app/src/main/java/ch/epfl/sweng/rps/ui/home/HomeFragment.kt b/app/src/main/java/ch/epfl/sweng/rps/ui/home/HomeFragment.kt index 4c520744..0acbc479 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/ui/home/HomeFragment.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/ui/home/HomeFragment.kt @@ -27,7 +27,7 @@ class HomeFragment : Fragment() { ViewModelProvider(this)[HomeViewModel::class.java] _binding = FragmentHomeBinding.inflate(inflater, container, false) - _binding!!.buttonPlayOneOfflineGame.setOnClickListener { view: View -> + _binding!!.buttonPlay1GamesOffline.setOnClickListener { view: View -> Navigation.findNavController(view).navigate(R.id.gameFragment) } diff --git a/app/src/main/java/ch/epfl/sweng/rps/ui/profile/profileFragment.kt b/app/src/main/java/ch/epfl/sweng/rps/ui/profile/ProfileFragment.kt similarity index 55% rename from app/src/main/java/ch/epfl/sweng/rps/ui/profile/profileFragment.kt rename to app/src/main/java/ch/epfl/sweng/rps/ui/profile/ProfileFragment.kt index e771354d..1eadbb06 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/ui/profile/profileFragment.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/ui/profile/ProfileFragment.kt @@ -1,44 +1,52 @@ package ch.epfl.sweng.rps.ui.profile -import androidx.lifecycle.ViewModelProvider +import android.content.Intent import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Button import android.widget.TextView +import android.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import ch.epfl.sweng.rps.MainActivity import ch.epfl.sweng.rps.R import ch.epfl.sweng.rps.models.User +import ch.epfl.sweng.rps.ui.settings.SettingsActivity +import com.google.android.material.appbar.MaterialToolbar -class profileFragment : Fragment() { - companion object { - fun newInstance() = profileFragment() - } +class ProfileFragment : Fragment() { private lateinit var viewModel: ProfileViewModel - private lateinit var user:User + private lateinit var user: User override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { user = (activity as MainActivity).getUserDetails() + viewModel = ViewModelProvider(this)[ProfileViewModel::class.java] return inflater.inflate(R.layout.profile_fragment, container, false) } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - viewModel = ViewModelProvider(this).get(ProfileViewModel::class.java) - // TODO: Use the ViewModel - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.findViewById(R.id.TextEmail).text = user.email view.findViewById(R.id.TextDisplayName).text = user.username - view.findViewById(R.id.TextPrivacy).text = user.games_history_privacy.toString() + view.findViewById(R.id.TextPrivacy).text = user.games_history_privacy + + view.findViewById(R.id.profile_top_toolbar) + .setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.profile_appbar_settings_btn -> { + val intent = Intent(activity, SettingsActivity::class.java) + startActivity(intent) + true + } + else -> false + } + } } } \ No newline at end of file diff --git a/app/src/main/java/ch/epfl/sweng/rps/ui/settings/SettingsActivity.kt b/app/src/main/java/ch/epfl/sweng/rps/ui/settings/SettingsActivity.kt new file mode 100644 index 00000000..d502c165 --- /dev/null +++ b/app/src/main/java/ch/epfl/sweng/rps/ui/settings/SettingsActivity.kt @@ -0,0 +1,145 @@ +package ch.epfl.sweng.rps.ui.settings + +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import ch.epfl.sweng.rps.R +import com.google.android.gms.oss.licenses.OssLicensesMenuActivity + + +class SettingsActivity : AppCompatActivity(), + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, + SharedPreferences.OnSharedPreferenceChangeListener { + + companion object { + private const val TITLE_TAG = "settingsActivityTitle" + + fun applyTheme( + themeKey: String?, + sharedPreferences: SharedPreferences? + ) { + when (sharedPreferences?.getString(themeKey, "system")) { + "light" -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + "dark" -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + } + "system" -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } + } + + } + } + + private val sharedPreferences by lazy { + PreferenceManager.getDefaultSharedPreferences(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.settings_activity) + if (savedInstanceState == null) { + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, HeaderFragment()) + .commit() + } else { + title = savedInstanceState.getCharSequence(TITLE_TAG) + } + supportFragmentManager.addOnBackStackChangedListener { + if (supportFragmentManager.backStackEntryCount == 0) { + setTitle(R.string.title_activity_settings) + } + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + // Save current activity title so we can set it again after a configuration change + outState.putCharSequence(TITLE_TAG, title) + } + + override fun onSupportNavigateUp(): Boolean { + if (supportFragmentManager.popBackStackImmediate()) { + return true + } + finish() + return true + } + + + override fun onPreferenceStartFragment( + caller: PreferenceFragmentCompat, pref: Preference + ): Boolean { + // Instantiate the new Fragment + val args = pref.extras + val fragment = supportFragmentManager.fragmentFactory.instantiate( + classLoader, + pref.fragment!! + ).apply { + arguments = args + setTargetFragment(caller, 0) + } + // Replace the existing Fragment with the new Fragment + supportFragmentManager.beginTransaction() + .replace(R.id.settings, fragment) + .addToBackStack(null) + .commit() + title = pref.title + return true + } + + class HeaderFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.header_preferences, rootKey) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val button: Preference = findPreference(getString(R.string.show_license_key))!! + button.setOnPreferenceClickListener { + startActivity(Intent(view.context, OssLicensesMenuActivity::class.java)) + true + } + } + } + + class AppearanceFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.appearance_preferences, rootKey) + } + } + + class SyncFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.sync_preferences, rootKey) + } + } + + override fun onResume() { + super.onResume() + sharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + val themeKey = getString(R.string.theme_pref_key) + if (key == themeKey) applyTheme(themeKey, sharedPreferences) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ch/epfl/sweng/rps/ui/statistics/StatisticsFragment.kt b/app/src/main/java/ch/epfl/sweng/rps/ui/statistics/StatisticsFragment.kt index acbd0565..1227b7ee 100644 --- a/app/src/main/java/ch/epfl/sweng/rps/ui/statistics/StatisticsFragment.kt +++ b/app/src/main/java/ch/epfl/sweng/rps/ui/statistics/StatisticsFragment.kt @@ -71,7 +71,7 @@ class StatisticsFragment : Fragment() { val statsTable = view.findViewById(R.id.statsTable) val row = TableRow(activity) row.setBackgroundColor( - Color.parseColor("#F0F7F7")) + Color.parseColor("#0FF0F7F7")) val scale = resources.displayMetrics.density val dpAsPixels = (sizeInDp * scale + 0.5f) diff --git a/app/src/main/res/drawable/ic_baseline_dark_mode_24.xml b/app/src/main/res/drawable/ic_baseline_dark_mode_24.xml new file mode 100644 index 00000000..e5391167 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_dark_mode_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_palette_24.xml b/app/src/main/res/drawable/ic_baseline_palette_24.xml new file mode 100644 index 00000000..4bf15508 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_palette_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_person_24.xml b/app/src/main/res/drawable/ic_baseline_person_24.xml new file mode 100644 index 00000000..6bdced2d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_person_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_settings_24.xml b/app/src/main/res/drawable/ic_baseline_settings_24.xml new file mode 100644 index 00000000..41a82ede --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_smartphone_24.xml b/app/src/main/res/drawable/ic_baseline_smartphone_24.xml new file mode 100644 index 00000000..bd22f2e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_smartphone_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_text_snippet_24.xml b/app/src/main/res/drawable/ic_baseline_text_snippet_24.xml new file mode 100644 index 00000000..074fe0f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_text_snippet_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/messages.xml b/app/src/main/res/drawable/messages.xml new file mode 100644 index 00000000..e5e8533c --- /dev/null +++ b/app/src/main/res/drawable/messages.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/sync.xml b/app/src/main/res/drawable/sync.xml new file mode 100644 index 00000000..3c9008b4 --- /dev/null +++ b/app/src/main/res/drawable/sync.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 07365a13..16e3a61e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,28 +4,21 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" - android:layout_height="match_parent" - > + android:layout_height="match_parent"> + tools:context=".ui.home.HomeFragment"> + android:textSize="20sp" />