diff --git a/starter/.idea/.name b/starter/.idea/.name
new file mode 100644
index 000000000..38403ec62
--- /dev/null
+++ b/starter/.idea/.name
@@ -0,0 +1 @@
+Project4
\ No newline at end of file
diff --git a/starter/.idea/androidTestResultsUserPreferences.xml b/starter/.idea/androidTestResultsUserPreferences.xml
new file mode 100644
index 000000000..8327fb590
--- /dev/null
+++ b/starter/.idea/androidTestResultsUserPreferences.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/.idea/compiler.xml b/starter/.idea/compiler.xml
new file mode 100644
index 000000000..b589d56e9
--- /dev/null
+++ b/starter/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/.idea/gradle.xml b/starter/.idea/gradle.xml
new file mode 100644
index 000000000..a0de2a152
--- /dev/null
+++ b/starter/.idea/gradle.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/.idea/kotlinc.xml b/starter/.idea/kotlinc.xml
new file mode 100644
index 000000000..2b8a50fc2
--- /dev/null
+++ b/starter/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/.idea/misc.xml b/starter/.idea/misc.xml
new file mode 100644
index 000000000..8d25e2666
--- /dev/null
+++ b/starter/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/.idea/vcs.xml b/starter/.idea/vcs.xml
new file mode 100644
index 000000000..6c0b86358
--- /dev/null
+++ b/starter/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/app/build.gradle b/starter/app/build.gradle
index 73b7fbf4e..fa7b9f581 100644
--- a/starter/app/build.gradle
+++ b/starter/app/build.gradle
@@ -1,20 +1,23 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-kapt'
-apply plugin: "androidx.navigation.safeargs.kotlin"
-apply plugin: 'kotlin-android-extensions'
-apply plugin: 'com.google.gms.google-services'
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'androidx.navigation.safeargs.kotlin'
+ id 'com.google.gms.google-services'
+ id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
+}
android {
- compileSdkVersion rootProject.compileSdkVersion
- buildToolsVersion "29.0.2"
+ namespace 'com.udacity.project4'
+ compileSdk 33
defaultConfig {
applicationId "com.udacity.project4"
- minSdkVersion rootProject.minSdkVersion
- targetSdkVersion rootProject.targetSdkVersion
+ minSdk rootProject.minSdkVersion
+ targetSdk rootProject.targetSdkVersion
versionCode 1
versionName "1.0"
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -26,15 +29,18 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
- testOptions.unitTests {
- includeAndroidResources = true
- returnDefaultValues = true
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ returnDefaultValues = true
+ }
+ animationsDisabled = true
}
- //dataBinding {
- // enabled = true
- // enabledForTests = true
- //}
+// dataBinding {
+// enabled = true
+// enabledForTests = true
+// }
buildFeatures {
dataBinding true
}
@@ -42,47 +48,44 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
+
// App dependencies
implementation "androidx.appcompat:appcompat:$appCompatVersion"
- implementation "androidx.legacy:legacy-support-v4:$androidXLegacySupport"
- implementation "androidx.annotation:annotation:$androidXAnnotations"
-
- implementation "androidx.cardview:cardview:$cardVersion"
+// implementation "androidx.legacy:legacy-support-v4:$androidXLegacySupport"
+// implementation "androidx.annotation:annotation:$androidXAnnotations"
+ implementation "androidx.swiperefreshlayout:swiperefreshlayout:$swipeRefreshVersion"
+// implementation "androidx.cardview:cardview:$cardVersion"
implementation "com.google.android.material:material:$materialVersion"
- implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
+// implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintVersion"
- implementation 'com.google.code.gson:gson:2.8.5'
+ implementation "com.google.code.gson:gson:$gsonVersion"
- // Architecture Components
- //Navigation dependencies
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
- kapt "androidx.lifecycle:lifecycle-compiler:$archLifecycleVersion"
- implementation "androidx.lifecycle:lifecycle-extensions:$archLifecycleVersion"
- implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion"
+ // Architecture Components & Navigation dependencies
+// kapt "androidx.lifecycle:lifecycle-compiler:$archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$archLifecycleVersion"
- implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archLifecycleVersion"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleKtxVersion"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archLifecycleKtxVersion"
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion"
+ implementation "androidx.work:work-runtime-ktx:$workManagerVersion"
- //Room dependencies
- implementation "androidx.room:room-ktx:$roomVersion"
+ // Room dependencies
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
+ // Kotlin Extensions and Coroutines support for Room
+ implementation "androidx.room:room-ktx:$roomVersion"
- //Coroutines Dependencies
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
-
- //Koin
- implementation "org.koin:koin-android:$koinVersion"
- implementation "org.koin:koin-androidx-viewmodel:$koinVersion"
+ // Coroutines Dependencies
+// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
+ // Koin
+ implementation "io.insert-koin:koin-android:$koinVersion"
+ implementation "io.insert-koin:koin-core:$koinVersion"
// Dependencies for local unit tests
- testImplementation "junit:junit:$junitVersion"
- testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
+// testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
@@ -91,9 +94,11 @@ dependencies {
testImplementation "org.mockito:mockito-core:$mockitoVersion"
// AndroidX Test - JVM testing
+ testImplementation "junit:junit:$junitVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:rules:$androidXTestRulesVersion"
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// AndroidX Test - Instrumented testing
androidTestImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
@@ -104,23 +109,27 @@ dependencies {
androidTestImplementation "androidx.arch.core:core-testing:$archTestingVersion"
androidTestImplementation "org.robolectric:annotations:$robolectricVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
+ // Testing code for more advanced views such as the DatePicker and RecyclerView.
+ // It also contains accessibility checks and a class called CountingIdlingResource.
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion"
androidTestImplementation "junit:junit:$junitVersion"
- // Once https://issuetracker.google.com/127986458 is fixed this can be testImplementation
- implementation "androidx.fragment:fragment-testing:$fragmentVersion"
+ // Testing code should not be included in the main code.
+ // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
+ debugImplementation "androidx.fragment:fragment-testing:$fragmentTestingVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
- implementation "androidx.fragment:fragment:$fragmentVersion"
+// implementation "androidx.fragment:fragment:$fragmentVersion"
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
- androidTestImplementation('org.koin:koin-test:2.0.1') { exclude group: 'org.mockito' }
+// androidTestImplementation('org.koin:koin-test:2.0.1') { exclude group: 'org.mockito' }
+ androidTestImplementation "io.insert-koin:koin-test:$koinVersion"
+ // Maps & Geofencing
+ implementation "com.google.android.gms:play-services-location:$playServicesLocationVersion"
+ implementation "com.google.android.gms:play-services-maps:$playServicesMapsVersion"
-
- //Maps & Geofencing
- implementation "com.google.android.gms:play-services-location:$playServicesVersion"
- implementation "com.google.android.gms:play-services-maps:$playServicesVersion"
-
-
-}
+ // Firebase
+ implementation "com.firebaseui:firebase-ui-auth:$firebaseUiAuthVersion"
+ implementation "com.google.firebase:firebase-auth-ktx:$firebaseAuthKtxVersion"
+}
\ No newline at end of file
diff --git a/starter/app/google-services.json b/starter/app/google-services.json
new file mode 100644
index 000000000..7d893818a
--- /dev/null
+++ b/starter/app/google-services.json
@@ -0,0 +1,47 @@
+{
+ "project_info": {
+ "project_number": "423316326245",
+ "project_id": "locationreminder-80efc",
+ "storage_bucket": "locationreminder-80efc.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:423316326245:android:a33b5cc75ea0375d7a838c",
+ "android_client_info": {
+ "package_name": "com.udacity.project4"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "423316326245-vg0vfgi6s4avapcmptbj3l314s7l4o66.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "com.udacity.project4",
+ "certificate_hash": "8daf07abce85f500329c4babccc42573b5ffeed4"
+ }
+ },
+ {
+ "client_id": "423316326245-8fjtl5af57sanqo7cvf9850cigltbkb5.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyB9Y3uogCpRJbOC-bliKYlzjhL-__57OZY"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "423316326245-0gidq5ki1qov5ms3cnsoa50obfd3v3p3.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/starter/app/src/androidTest/java/com/udacity/project4/MainCoroutineRule.kt b/starter/app/src/androidTest/java/com/udacity/project4/MainCoroutineRule.kt
new file mode 100644
index 000000000..273229e9e
--- /dev/null
+++ b/starter/app/src/androidTest/java/com/udacity/project4/MainCoroutineRule.kt
@@ -0,0 +1,27 @@
+package com.udacity.project4
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.*
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Source:
+ * https://stackoverflow.com/a/73300292
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class MainCoroutineRule(
+ val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
+) : TestWatcher() {
+
+ override fun starting(description: Description) {
+ super.starting(description)
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ override fun finished(description: Description) {
+ super.finished(description)
+ Dispatchers.resetMain()
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/androidTest/java/com/udacity/project4/RemindersActivityTest.kt b/starter/app/src/androidTest/java/com/udacity/project4/RemindersActivityTest.kt
index 44f979063..fffa5b960 100644
--- a/starter/app/src/androidTest/java/com/udacity/project4/RemindersActivityTest.kt
+++ b/starter/app/src/androidTest/java/com/udacity/project4/RemindersActivityTest.kt
@@ -1,71 +1,203 @@
package com.udacity.project4
import android.app.Application
+import android.os.Build
+import android.view.View
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.espresso.Espresso.closeSoftKeyboard
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.action.GeneralClickAction
+import androidx.test.espresso.action.Press
+import androidx.test.espresso.action.Tap
+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.RootMatchers.withDecorView
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import com.udacity.project4.locationreminders.RemindersActivity
import com.udacity.project4.locationreminders.data.ReminderDataSource
import com.udacity.project4.locationreminders.data.local.LocalDB
import com.udacity.project4.locationreminders.data.local.RemindersLocalRepository
import com.udacity.project4.locationreminders.reminderslist.RemindersListViewModel
import com.udacity.project4.locationreminders.savereminder.SaveReminderViewModel
+import com.udacity.project4.util.DataBindingIdlingResource
+import com.udacity.project4.util.monitorActivity
+import com.udacity.project4.utils.EspressoIdlingResource
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
+import org.hamcrest.CoreMatchers
+import org.junit.After
import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
import org.junit.runner.RunWith
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
-import org.koin.test.AutoCloseKoinTest
+import org.koin.test.KoinTest
import org.koin.test.get
+/**
+ * END TO END test to black box test the app.
+ *
+ * Source:
+ * https://medium.com/koin-developers/unboxing-koin-2-1-7f1133ebb790
+ */
+@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@LargeTest
-//END TO END test to black box test the app
-class RemindersActivityTest :
- AutoCloseKoinTest() {// Extended Koin Test - embed autoclose @after method to close Koin after every test
-
- private lateinit var repository: ReminderDataSource
- private lateinit var appContext: Application
-
- /**
- * As we use Koin as a Service Locator Library to develop our code, we'll also use Koin to test our code.
- * at this step we will initialize Koin related code to be able to use it in out testing.
- */
- @Before
- fun init() {
- stopKoin()//stop the original app koin
- appContext = getApplicationContext()
- val myModule = module {
- viewModel {
- RemindersListViewModel(
- appContext,
- get() as ReminderDataSource
- )
- }
- single {
- SaveReminderViewModel(
- appContext,
- get() as ReminderDataSource
- )
- }
- single { RemindersLocalRepository(get()) as ReminderDataSource }
- single { LocalDB.createRemindersDao(appContext) }
- }
- //declare a new koin module
- startKoin {
- modules(listOf(myModule))
- }
- //Get our real repository
- repository = get()
-
- //clear the data to start fresh
- runBlocking {
- repository.deleteAllReminders()
- }
- }
-
-
-// TODO: add End to End testing to the app
-
-}
+class RemindersActivityTest : KoinTest {
+
+ // Executes each task synchronously using Architecture Components.
+ @get:Rule
+ var instantExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ var activityScenarioRule = ActivityScenarioRule(RemindersActivity::class.java)
+
+ // An Idling Resource that waits for Data Binding to have no pending bindings.
+ private val dataBindingIdlingResource = DataBindingIdlingResource()
+
+ private lateinit var decorView: View
+ private lateinit var repository: ReminderDataSource
+ private lateinit var appContext: Application
+
+ /**
+ * As we use Koin as a Service Locator Library to develop our code, we'll also use Koin to test our code.
+ * At this step we will initialize Koin related code to be able to use it in our testing.
+ */
+ @Before
+ fun init() {
+ // Stop the original app koin.
+ stopKoin()
+ appContext = getApplicationContext()
+
+ val myModule = module {
+ viewModel { RemindersListViewModel(appContext, get() as ReminderDataSource) }
+ single { SaveReminderViewModel(appContext, get() as ReminderDataSource) }
+ single { RemindersLocalRepository(get()) as ReminderDataSource }
+ single { LocalDB.createRemindersDao(appContext) }
+ }
+
+ // Declare a new koin module.
+ startKoin { modules(listOf(myModule)) }
+
+ // Get our real repository.
+ repository = get()
+
+ // Clear the data to start fresh.
+ runBlocking {
+ repository.deleteAllReminders()
+ }
+
+ activityScenarioRule.scenario.onActivity { activity ->
+ decorView = activity.window.decorView
+ }
+ }
+
+ /**
+ * Idling resources tell Espresso that the app is idle or busy. This is needed when operations
+ * are not scheduled in the main Looper (for example when executed on a different thread).
+ */
+ @Before
+ fun registerIdlingResource() {
+ IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
+ IdlingRegistry.getInstance().register(dataBindingIdlingResource)
+ }
+
+ /**
+ * Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
+ */
+ @After
+ fun unregisterIdlingResource() {
+ IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
+ IdlingRegistry.getInstance().unregister(dataBindingIdlingResource)
+ }
+
+ @Test
+ fun clickingOnFloatingActionButton_checkIfFieldsAreDisplayed() = runBlocking {
+ val activityScenario = ActivityScenario.launch(RemindersActivity::class.java)
+ dataBindingIdlingResource.monitorActivity(activityScenario)
+
+ onView(withId(R.id.addReminderFAB)).perform(click())
+ onView(withId(R.id.reminderTitle)).check(matches(isDisplayed()))
+ onView(withId(R.id.reminderDescription)).check(matches(isDisplayed()))
+ onView(withId(R.id.selectLocation)).check(matches(isDisplayed()))
+
+ activityScenario.close()
+ }
+
+ @Test
+ fun addingReminderWithoutTitle_checkIfPreventMessageIsShowed() = runBlocking {
+ val activityActivityScenario = ActivityScenario.launch(RemindersActivity::class.java)
+ dataBindingIdlingResource.monitorActivity(activityActivityScenario)
+
+ onView(withId(R.id.addReminderFAB)).perform(click())
+ onView(withId(R.id.saveReminder)).perform(click())
+ onView(withText("Please enter title")).check(matches(isDisplayed()))
+
+ activityActivityScenario.close()
+ }
+
+ /**
+ * Toast Assertions doesn't working in versions below 30.
+ * https://github.com/android/android-test/issues/803
+ */
+ @Test
+ fun addingReminder_verifyIfToastIsCalled() {
+ // Verifying Exception because Toast Assertions doesn't work in versions above 30.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ throw Exception("Toast Assertions doesn't work in versions above 30.")
+ }
+
+ // Setup for this test.
+ val typingTitle = "Title"
+ val typingDescription = "Description"
+ val activityScenario = ActivityScenario.launch(RemindersActivity::class.java)
+ dataBindingIdlingResource.monitorActivity(activityScenario)
+
+ // GIVEN - Clicking to add a new task on Save Reminder screen.
+ onView(withId(R.id.addReminderFAB)).perform(click())
+ onView(withId(R.id.reminderTitle)).perform(ViewActions.typeText(typingTitle))
+ onView(withId(R.id.reminderDescription)).perform(ViewActions.typeText(typingDescription))
+ closeSoftKeyboard()
+
+ // WHEN - Clicking in any position in the map, and saving the location.
+ (appContext as MyApp).testingMode = true
+ onView(withId(R.id.selectLocation)).perform(click())
+ onView(withId(R.id.map_fragment)).perform(simulateClickOnMap(500, 500))
+ // Wait for map to load.
+ Thread.sleep(2000)
+
+ // THEN - Save Reminder and check if Toast is displayed.
+ onView(withId(R.id.saveReminder)).perform(click())
+ onView(withText(R.string.reminder_saved)).inRoot(
+ withDecorView(CoreMatchers.not(decorView))
+ ).check(matches(isDisplayed()))
+
+ activityScenario.close()
+ }
+
+ private fun simulateClickOnMap(x: Int, y: Int): ViewAction {
+ return GeneralClickAction(
+ Tap.SINGLE, { view ->
+ val screenPos = IntArray(2)
+ view.getLocationOnScreen(screenPos)
+ val screenX = (screenPos[0] + x).toFloat()
+ val screenY = (screenPos[1] + y).toFloat()
+ floatArrayOf(screenX, screenY)
+ },
+ Press.FINGER, 0, 0
+ )
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/data/local/RemindersDaoTest.kt b/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/data/local/RemindersDaoTest.kt
index 6e674abfb..7c02d2024 100644
--- a/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/data/local/RemindersDaoTest.kt
+++ b/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/data/local/RemindersDaoTest.kt
@@ -2,29 +2,90 @@ package com.udacity.project4.locationreminders.data.local
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
import com.udacity.project4.locationreminders.data.dto.ReminderDTO
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.runner.RunWith;
-
-import kotlinx.coroutines.ExperimentalCoroutinesApi;
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.After
+import org.junit.Before
+import org.junit.Rule
import org.junit.Test
+import org.junit.runner.RunWith
+/**
+ * Unit test the DAO.
+ */
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
-//Unit test the DAO
@SmallTest
class RemindersDaoTest {
-// TODO: Add testing implementation to the RemindersDao.kt
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private lateinit var database: RemindersDatabase
+
+ private val item1 = ReminderDTO("1", "first", "loc", 122.6, 122.7)
+ private val item2 = ReminderDTO("2", "second", "loc2", 0.0, 0.0)
+
+ @Before
+ fun initDb() {
+ database = Room.inMemoryDatabaseBuilder(
+ getApplicationContext(), RemindersDatabase::class.java
+ ).allowMainThreadQueries().build()
+ }
+
+ @After
+ fun closeDb() {
+ database.close()
+ }
+
+ @Test
+ fun insertAReminder_thenGettingItById_returnsThisSameReminder() = runBlocking {
+ // GIVEN - Insert a Reminder.
+ database.reminderDao().saveReminder(item1)
+
+ // WHEN - Getting a Reminder by ID.
+ val check = database.reminderDao().getReminderById(item1.id)
+
+ // THEN - The loaded data contains the expected values.
+ assertThat(check as ReminderDTO, notNullValue())
+ assertThat(check.latitude, `is`(item1.latitude))
+ assertThat(check.location, `is`(item1.location))
+ assertThat(check.id, `is`(item1.id))
+ assertThat(check.longitude, `is`(item1.longitude))
+ assertThat(check.description, `is`(item1.description))
+ assertThat(check.title, `is`(item1.title))
+ }
+
+ @Test
+ fun insertAReminder_thenDeletingAllReminders_returnsNullForThisInsertedReminder() = runBlocking {
+ // GIVEN - Insert a Reminder.
+ database.reminderDao().saveReminder(item1)
+
+ // WHEN - Deleting all Reminders.
+ database.reminderDao().deleteAllReminders()
+
+ // THEN - Getting the inserted Reminder returns null.
+ assertThat(database.reminderDao().getReminderById(item1.id), `is`(nullValue()))
+ }
+
+ @Test
+ fun insertTwoReminders_thenGetAllReminders_returnsANonEmptyListOfReminders() = runBlocking {
+ // GIVEN - Inserting two Reminders.
+ database.reminderDao().saveReminder(item1)
+ database.reminderDao().saveReminder(item2)
+
+ // WHEN - Getting all Reminders.
+ val list = database.reminderDao().getReminders()
+ // THEN - Check if list of Reminders is not null.
+ assertThat(list.isNotEmpty(), `is`(true))
+ }
}
\ No newline at end of file
diff --git a/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/data/local/RemindersLocalRepositoryTest.kt b/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/data/local/RemindersLocalRepositoryTest.kt
index 97d6b48e0..ea99a762f 100644
--- a/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/data/local/RemindersLocalRepositoryTest.kt
+++ b/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/data/local/RemindersLocalRepositoryTest.kt
@@ -1,30 +1,86 @@
package com.udacity.project4.locationreminders.data.local
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.udacity.project4.locationreminders.data.dto.ReminderDTO
import com.udacity.project4.locationreminders.data.dto.Result
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.MatcherAssert.assertThat
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+/**
+ * Medium Test to test the repository.
+ */
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
-//Medium Test to test the repository
@MediumTest
class RemindersLocalRepositoryTest {
-// TODO: Add testing implementation to the RemindersLocalRepository.kt
+ private lateinit var repository: RemindersLocalRepository
+ private lateinit var database: RemindersDatabase
+ private val item1 = ReminderDTO("1", "2", "3", 100.0, 120.0)
+ private val item2 = ReminderDTO("2", "second", "loc2", 0.0, 0.0)
+
+ @Before
+ fun setup() {
+ database = Room.inMemoryDatabaseBuilder(
+ ApplicationProvider.getApplicationContext(),
+ RemindersDatabase::class.java
+ ).allowMainThreadQueries().build()
+ repository = RemindersLocalRepository(database.reminderDao(), UnconfinedTestDispatcher())
+ }
+
+ @After
+ fun cleanUp() = database.close()
+
+ @Test
+ fun gettingANewReminderWithoutSavingItOnDatabase_returnsNoReminderFound() = runBlocking {
+ // GIVEN - Trying to get a new Reminder without saving it on database.
+ val result = repository.getReminder(item1.id)
+
+ // THEN - No Reminder was found.
+ assertThat(result is Result.Error, `is`(true))
+ result as Result.Error
+ assertThat(result.message, `is`("Reminder not found!"))
+ }
+
+ @Test
+ fun gettingANewReminderAfterSavingItOnDatabase_returnsThisSameReminder() = runBlocking {
+ // GIVEN - A new Reminder saved in the database.
+ repository.saveReminder(item1)
+
+ // WHEN - Reminder retrieved by ID.
+ val result = repository.getReminder(item1.id) as Result.Success
+ val loaded = result.data
+
+ // THEN - Same Reminder is returned.
+ assertThat(loaded.longitude, `is`(item1.longitude))
+ assertThat(loaded.latitude, `is`(item1.latitude))
+ assertThat(loaded, CoreMatchers.notNullValue())
+ assertThat(loaded.id, `is`(item1.id))
+ assertThat(loaded.description, `is`(item1.description))
+ assertThat(loaded.location, `is`(item1.location))
+ assertThat(loaded.title, `is`(item1.title))
+ }
+
+ @Test
+ fun deletingAllReminders_returnsARepoWithNoReminders() = runBlocking {
+ // GIVEN - Deleting all Reminders.
+ repository.deleteAllReminders()
+
+ // THEN - There are no Reminders in our repo.
+ val result = repository.getReminders() as Result.Success
+ val data = result.data
+ assertThat(data, `is`(emptyList()))
+ }
}
\ No newline at end of file
diff --git a/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/reminderslist/ReminderListFragmentTest.kt b/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/reminderslist/ReminderListFragmentTest.kt
index 6ba9a278a..60e398098 100644
--- a/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/reminderslist/ReminderListFragmentTest.kt
+++ b/starter/app/src/androidTest/java/com/udacity/project4/locationreminders/reminderslist/ReminderListFragmentTest.kt
@@ -1,31 +1,120 @@
package com.udacity.project4.locationreminders.reminderslist
-import android.content.Context
import android.os.Bundle
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.navigation.NavController
import androidx.navigation.Navigation
-import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.action.ViewActions.click
-import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.action.ViewActions.*
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.udacity.project4.R
+import com.udacity.project4.locationreminders.data.ReminderDataSource
+import com.udacity.project4.locationreminders.data.dto.ReminderDTO
+import com.udacity.project4.locationreminders.data.local.LocalDB
+import com.udacity.project4.locationreminders.data.local.RemindersLocalRepository
+import com.udacity.project4.locationreminders.savereminder.SaveReminderViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.dsl.module
+import org.koin.test.KoinTest
+import org.koin.test.get
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
+/**
+ * UI Testing.
+ */
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
-//UI Testing
@MediumTest
-class ReminderListFragmentTest {
+class ReminderListFragmentTest : KoinTest {
-// TODO: test the navigation of the fragments.
-// TODO: test the displayed data on the UI.
-// TODO: add testing for the error messages.
+ private val mockNavController = mock(NavController::class.java)
+ private lateinit var remindersListViewModel: RemindersListViewModel
+ private lateinit var repository: ReminderDataSource
+
+ @Before
+ fun init() {
+ stopKoin()
+ val module = module {
+ single {
+ SaveReminderViewModel(
+ ApplicationProvider.getApplicationContext(),
+ get() as ReminderDataSource
+ )
+ }
+ viewModel {
+ RemindersListViewModel(
+ ApplicationProvider.getApplicationContext(),
+ get() as ReminderDataSource
+ )
+ }
+ single {
+ LocalDB.createRemindersDao(ApplicationProvider.getApplicationContext())
+ }
+ single { RemindersLocalRepository(get()) as ReminderDataSource }
+
+ }
+ startKoin {
+ modules(listOf(module))
+ }
+ repository = get()
+ runBlocking {
+ repository.deleteAllReminders()
+ }
+ remindersListViewModel =
+ RemindersListViewModel(ApplicationProvider.getApplicationContext(), repository)
+ }
+
+ @Test
+ fun homeScreen_afterOpeningApp_checkNoDataMessageIsDisplayed() {
+ // GIVEN - On the home screen.
+ launchFragmentInContainer(Bundle(), R.style.AppTheme)
+
+ // THEN - Verify that we have No Data in our RecyclerView.
+ onView(withText("No Data")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun homeScreen_onClick_navigateToSaveReminderScreen() {
+ // GIVEN - On the home screen.
+ val sec = launchFragmentInContainer(Bundle(), R.style.AppTheme)
+ sec.onFragment { Navigation.setViewNavController(it.view!!, mockNavController) }
+
+ // WHEN - Click on the FAB.
+ onView(withId(R.id.addReminderFAB)).perform(click())
+
+ // THEN - Verify that we navigate to the first Save Reminder screen.
+ verify(mockNavController).navigate(
+ ReminderListFragmentDirections.actionReminderListFragmentToSaveReminderFragment()
+ )
+ }
+
+ @Test
+ fun homeScreen_savingAReminderon_reminderInfoIsDisplayed() {
+ // GIVEN - Saving a Reminder.
+ val remind = ReminderDTO("1", "2", "3", 0.0, 0.9)
+ runBlocking {
+ repository.saveReminder(remind)
+ }
+
+ // WHEN - On the home screen.
+ launchFragmentInContainer(Bundle(), R.style.AppTheme)
+
+ // THEN - Verify that the title, description and location are displayed.
+ onView(withText(remind.title)).check(matches(isDisplayed()))
+ onView(withText(remind.description)).check(matches(isDisplayed()))
+ onView(withText(remind.location)).check(matches(isDisplayed()))
+ }
}
\ No newline at end of file
diff --git a/starter/app/src/androidTest/java/com/udacity/project4/util/DataBindingIdlingResource.kt b/starter/app/src/androidTest/java/com/udacity/project4/util/DataBindingIdlingResource.kt
index 1f1711e06..9a88bdf51 100644
--- a/starter/app/src/androidTest/java/com/udacity/project4/util/DataBindingIdlingResource.kt
+++ b/starter/app/src/androidTest/java/com/udacity/project4/util/DataBindingIdlingResource.kt
@@ -18,12 +18,11 @@ package com.udacity.project4.util
import android.view.View
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
-import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.testing.FragmentScenario
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.IdlingResource
-import java.util.UUID
+import java.util.*
/**
* An espresso idling resource implementation that reports idle status for all data binding
@@ -104,8 +103,10 @@ fun DataBindingIdlingResource.monitorActivity(
/**
* Sets the fragment from a [FragmentScenario] to be used from [DataBindingIdlingResource].
*/
+/*
fun DataBindingIdlingResource.monitorFragment(fragmentScenario: FragmentScenario) {
fragmentScenario.onFragment {
this.activity = it.requireActivity()
}
}
+ */
diff --git a/starter/app/src/debug/res/values/google_maps_api.xml b/starter/app/src/debug/res/values/google_maps_api.xml
index 4f597c14c..141af24c0 100644
--- a/starter/app/src/debug/res/values/google_maps_api.xml
+++ b/starter/app/src/debug/res/values/google_maps_api.xml
@@ -1,4 +1,7 @@
-
-
-
+
+ AIzaSyC6hBgGRJHLlKoosvbR-nPUg0w5Lg3cbWs
+
\ No newline at end of file
diff --git a/starter/app/src/main/AndroidManifest.xml b/starter/app/src/main/AndroidManifest.xml
index db5d2ec2a..ed9b4778e 100644
--- a/starter/app/src/main/AndroidManifest.xml
+++ b/starter/app/src/main/AndroidManifest.xml
@@ -1,19 +1,28 @@
-
+
+
+
-
-
+
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/MyApp.kt b/starter/app/src/main/java/com/udacity/project4/MyApp.kt
index 15c18be55..0cfcec1a1 100644
--- a/starter/app/src/main/java/com/udacity/project4/MyApp.kt
+++ b/starter/app/src/main/java/com/udacity/project4/MyApp.kt
@@ -1,47 +1,52 @@
package com.udacity.project4
import android.app.Application
+import android.content.Intent
import com.udacity.project4.locationreminders.data.ReminderDataSource
import com.udacity.project4.locationreminders.data.local.LocalDB
import com.udacity.project4.locationreminders.data.local.RemindersLocalRepository
import com.udacity.project4.locationreminders.reminderslist.RemindersListViewModel
import com.udacity.project4.locationreminders.savereminder.SaveReminderViewModel
+import com.udacity.project4.locationreminders.savereminder.selectreminderlocation.SelectLocationViewModel
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.dsl.module
class MyApp : Application() {
+ lateinit var broadcastIntent: Intent
+ var hasNotificationPermission = true
+ var testingMode = false
- override fun onCreate() {
- super.onCreate()
+ override fun onCreate() {
+ super.onCreate()
- /**
- * use Koin Library as a service locator
- */
- val myModule = module {
- //Declare a ViewModel - be later inject into Fragment with dedicated injector using by viewModel()
- viewModel {
- RemindersListViewModel(
- get(),
- get() as ReminderDataSource
- )
- }
- //Declare singleton definitions to be later injected using by inject()
- single {
- //This view model is declared singleton to be used across multiple fragments
- SaveReminderViewModel(
- get(),
- get() as ReminderDataSource
- )
- }
- single { RemindersLocalRepository(get()) as ReminderDataSource }
- single { LocalDB.createRemindersDao(this@MyApp) }
- }
+ /**
+ * use Koin Library as a service locator
+ */
+ val myModule = module {
+ // Declare a ViewModel - be later inject into Fragment with dedicated injector using by viewModel()
+ viewModel {
+ RemindersListViewModel(
+ get(),
+ get() as ReminderDataSource
+ )
+ }
+ // Declare singleton definitions to be later injected using by inject()
+ single {
+ //This view model is declared singleton to be used across multiple fragments
+ SaveReminderViewModel(
+ get(),
+ get() as ReminderDataSource
+ )
+ }
+ single { RemindersLocalRepository(get()) as ReminderDataSource }
+ single { LocalDB.createRemindersDao(this@MyApp) }
+ }
- startKoin {
- androidContext(this@MyApp)
- modules(listOf(myModule))
- }
- }
+ startKoin {
+ androidContext(this@MyApp)
+ modules(listOf(myModule))
+ }
+ }
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/TrackingActivity.kt b/starter/app/src/main/java/com/udacity/project4/TrackingActivity.kt
new file mode 100644
index 000000000..29d84ab6c
--- /dev/null
+++ b/starter/app/src/main/java/com/udacity/project4/TrackingActivity.kt
@@ -0,0 +1,234 @@
+package com.udacity.project4
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Looper
+import android.provider.Settings
+import android.util.Log
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.gms.location.FusedLocationProviderClient
+import com.google.android.gms.location.Granularity
+import com.google.android.gms.location.LocationCallback
+import com.google.android.gms.location.LocationRequest
+import com.google.android.gms.location.LocationResult
+import com.google.android.gms.location.LocationServices
+import com.google.android.gms.location.Priority
+import com.udacity.project4.utils.permissionGranted
+import com.udacity.project4.utils.requestTrackingPermissions
+import com.udacity.project4.utils.showRequestPermissionRationale
+import com.udacity.project4.utils.toast
+
+/**
+ * This Activity shows how to track the user Location
+ *
+ * Sources:
+ * https://stackoverflow.com/questions/60384554/access-background-location-not-working-on-lower-than-q-29-android-versions
+ * https://stackoverflow.com/questions/40142331/how-to-request-location-permission-at-runtime/40142454#40142454
+ * https://github.com/zoontek/react-native-permissions/issues/620
+ * -> https://github.com/zoontek/react-native-permissions/issues/620#issuecomment-866690012
+ */
+class TrackingActivity : AppCompatActivity() {
+
+ companion object {
+ private const val MY_PERMISSIONS_REQUEST_LOCATION = 99
+ private const val MY_PERMISSIONS_REQUEST_BACKGROUND_LOCATION = 66
+ private const val UPDATE_INTERVAL = (10 * 1000).toLong() // 10 secs
+ private const val FASTEST_INTERVAL: Long = 2000 // 2 secs
+ }
+
+ private var fusedLocationProvider: FusedLocationProviderClient? = null
+ private val locationRequest: LocationRequest = LocationRequest.Builder(
+ Priority.PRIORITY_BALANCED_POWER_ACCURACY, UPDATE_INTERVAL
+ ).apply {
+ setMinUpdateDistanceMeters(5F)
+ setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL)
+ setWaitForAccurateLocation(true)
+ setMinUpdateIntervalMillis(FASTEST_INTERVAL)
+ }.build()
+
+ /*
+ private val locationRequest: LocationRequest = LocationRequest.create().apply {
+ interval = 30
+ fastestInterval = 10
+ priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
+ maxWaitTime = 60
+ }
+ */
+
+ private var locationCallback: LocationCallback = object : LocationCallback() {
+ override fun onLocationResult(locationResult: LocationResult) {
+ val locationList = locationResult.locations
+ if (locationList.isNotEmpty()) {
+ // The last location in the list is the newest.
+ val location = locationList.last()
+ toast("Got Location: $location")
+ }
+ }
+ }
+
+ //--------------------------------------------------
+ // Lifecycle Methods
+ //--------------------------------------------------
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_tracking)
+
+ fusedLocationProvider = LocationServices.getFusedLocationProviderClient(this)
+ checkLocationPermission()
+ }
+
+ @SuppressLint("MissingPermission")
+ override fun onResume() {
+ super.onResume()
+ if (permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ fusedLocationProvider?.requestLocationUpdates(
+ locationRequest,
+ locationCallback,
+ Looper.getMainLooper()
+ )
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ if (permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ fusedLocationProvider?.removeLocationUpdates(locationCallback)
+ }
+ }
+
+ //--------------------------------------------------
+ // Permission Methods
+ //--------------------------------------------------
+
+ private fun checkLocationPermission() {
+ if (!permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ // Should we show an explanation?
+ if (showRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ // Show an explanation to the user *asynchronously* -- don't block this thread
+ // waiting for the user's response! After the user sees the explanation, try again
+ // to request the permission.
+ val title = this.getString(R.string.location_permission_needed)
+ val message = this.getString(R.string.location_permission_explanation)
+ AlertDialog.Builder(parent)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton("OK") { _, _ ->
+ // Prompt the user once explanation has been shown.
+ requestLocationPermission()
+ }
+ .create()
+ .show()
+ } else {
+ // No explanation needed, we can request the permission.
+ requestLocationPermission()
+ }
+ } else {
+ checkBackgroundLocationPermission()
+ }
+ }
+
+ private fun checkBackgroundLocationPermission() {
+ if (!permissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {
+ requestBackgroundLocationPermission()
+ }
+ }
+
+ private fun requestLocationPermission() {
+ requestTrackingPermissions(
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ MY_PERMISSIONS_REQUEST_LOCATION
+ )
+ }
+
+ private fun requestBackgroundLocationPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ requestTrackingPermissions(
+ arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
+ MY_PERMISSIONS_REQUEST_BACKGROUND_LOCATION
+ )
+ } else {
+ requestTrackingPermissions(
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ MY_PERMISSIONS_REQUEST_LOCATION
+ )
+ }
+ }
+
+ @SuppressLint("MissingSuperCall", "MissingPermission")
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ Log.d("aaa", "bbb")
+ when (requestCode) {
+ MY_PERMISSIONS_REQUEST_LOCATION -> {
+ onRequestLocationPermissionsResult(grantResults)
+ return
+ }
+ MY_PERMISSIONS_REQUEST_BACKGROUND_LOCATION -> {
+ onRequestBackgroundLocationPermissionsResult(grantResults)
+ return
+ }
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun onRequestLocationPermissionsResult(grantResults: IntArray) {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Permission was granted, yay! Do the location-related task you need to do.
+ if (permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ fusedLocationProvider?.requestLocationUpdates(
+ locationRequest,
+ locationCallback,
+ Looper.getMainLooper()
+ )
+
+ // Now check background location.
+ checkBackgroundLocationPermission()
+ }
+
+ } else {
+ // Permission denied, boo! Disable the functionality that depends on this permission.
+ toast("Permission denied")
+
+ // Check if we are in a state where the user has denied the permission and
+ // selected Don't ask again.
+ if (!showRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ startActivity(
+ Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.fromParts("package", this.packageName, null),
+ ),
+ )
+ }
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun onRequestBackgroundLocationPermissionsResult(grantResults: IntArray) {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Permission was granted, yay! Do the location-related task you need to do.
+ if (permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
+ fusedLocationProvider?.requestLocationUpdates(
+ locationRequest,
+ locationCallback,
+ Looper.getMainLooper()
+ )
+ toast("Granted Background Location Permission")
+ }
+ } else {
+ // Permission denied, boo! Disable the functionality that depends on this permission.
+ toast("Permission denied")
+ }
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/authentication/AuthenticationActivity.kt b/starter/app/src/main/java/com/udacity/project4/authentication/AuthenticationActivity.kt
index cdf405c6b..64c49400c 100644
--- a/starter/app/src/main/java/com/udacity/project4/authentication/AuthenticationActivity.kt
+++ b/starter/app/src/main/java/com/udacity/project4/authentication/AuthenticationActivity.kt
@@ -1,8 +1,19 @@
package com.udacity.project4.authentication
+import android.app.Activity
+import android.content.Intent
import android.os.Bundle
+import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+import com.firebase.ui.auth.AuthUI
+import com.firebase.ui.auth.IdpResponse
+import com.google.firebase.auth.FirebaseAuth
import com.udacity.project4.R
+import com.udacity.project4.databinding.ActivityAuthenticationBinding
+import com.udacity.project4.locationreminders.RemindersActivity
/**
* This class should be the starting point of the app, It asks the users to sign in / register, and redirects the
@@ -10,15 +21,121 @@ import com.udacity.project4.R
*/
class AuthenticationActivity : AppCompatActivity() {
+ companion object {
+ const val TAG = "MainFragment"
+ }
+
+ // Get a reference to the ViewModel scoped to this Fragment
+ private val viewModel by viewModels()
+ private lateinit var binding: ActivityAuthenticationBinding
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_authentication)
-// TODO: Implement the create account and sign in using FirebaseUI, use sign in using email and sign in using Google
+ val layoutId = R.layout.activity_authentication
+ binding = DataBindingUtil.setContentView(this, layoutId)
+ binding.authButton.text = getString(R.string.login)
+
+ checkIfUserIsLogged()
+ }
-// TODO: If the user was authenticated, send him to RemindersActivity
+ override fun onResume() {
+ super.onResume()
+ observeAuthenticationState()
-// TODO: a bonus is to customize the sign in flow to look nice using :
- //https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md#custom-layout
+ binding.authButton.setOnClickListener {
+ launchSignInFlow()
+ }
+ }
+
+ /**
+ * Listen to the result of the sign in process by filter for when
+ * SIGN_IN_REQUEST_CODE is passed back. Start by having log statements to know
+ * whether the user has signed in successfully
+ */
+ private val resultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ // There are no request codes
+ val data: Intent? = result.data
+ val response = IdpResponse.fromResultIntent(data)
+ if (result.resultCode == Activity.RESULT_OK) {
+ // User successfully signed in
+ Log.i(TAG, "Successfully signed in user " +
+ "${FirebaseAuth.getInstance().currentUser?.displayName}!"
+ )
+ // If the user was authenticated, send him to RemindersActivity.
+ startActivity(Intent(this, RemindersActivity::class.java))
+ } else {
+ // Sign in failed. If response is null the user canceled the
+ // sign-in flow using the back button. Otherwise check
+ // response.getError().getErrorCode() and handle the error.
+ Log.i(TAG, "Sign in unsuccessful ${response?.error?.errorCode}")
+ }
+ }
+ }
+
+ /**
+ * Observes the authentication state and changes the UI accordingly.
+ * If there is a logged in user: (1) show a logout button and (2) display their name.
+ * If there is no logged in user: show a login button
+ */
+ private fun observeAuthenticationState() {
+ viewModel.authenticationState.observe(this) { authenticationState ->
+ // in LoginViewModel and change the UI accordingly.
+ when (authenticationState) {
+ LoginViewModel.AuthenticationState.AUTHENTICATED -> { logoutFromFirebase() }
+ else -> { loginOnFirebase() }
+ }
+ }
+ }
+
+ private fun loginOnFirebase() {
+ binding.authButton.text = getString(R.string.login)
+ binding.authButton.setOnClickListener {
+ launchSignInFlow()
+ }
+ }
+
+ private fun logoutFromFirebase() {
+ binding.authButton.text = getString(R.string.logout)
+ binding.authButton.setOnClickListener {
+ AuthUI.getInstance().signOut(this)
+ }
+ }
+
+ /**
+ * Implement the create account and sign in using FirebaseUI,
+ * use sign in using email and sign in using Google.
+ */
+ private fun launchSignInFlow() {
+ // Give users the option to sign in / register with their email or Google account.
+ // If users choose to register with their email,
+ // they will need to create a password as well.
+ val providers = arrayListOf(
+ AuthUI.IdpConfig.EmailBuilder().build(), AuthUI.IdpConfig.GoogleBuilder().build()
+ // This is where you can provide more ways for users to register and
+ // sign in.
+ )
+
+ // Create and launch sign-in intent.
+ val intent = AuthUI.getInstance()
+ .createSignInIntentBuilder()
+ .setAvailableProviders(providers)
+ .build()
+ resultLauncher.launch(intent)
+ }
+
+ private fun checkIfUserIsLogged() {
+ if (!userIsLoggedIn()) {
+ AuthUI.getInstance().signOut(this)
+ } else {
+ startActivity(Intent(this, RemindersActivity::class.java))
+ }
+ }
+ private fun userIsLoggedIn(): Boolean {
+ val user = FirebaseAuth.getInstance().currentUser
+ return user != null
}
-}
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/authentication/FirebaseUserLiveData.kt b/starter/app/src/main/java/com/udacity/project4/authentication/FirebaseUserLiveData.kt
new file mode 100644
index 000000000..a866eefd8
--- /dev/null
+++ b/starter/app/src/main/java/com/udacity/project4/authentication/FirebaseUserLiveData.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.udacity.project4.authentication
+
+import androidx.lifecycle.LiveData
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.auth.FirebaseUser
+
+/**
+ * This class observes the current FirebaseUser. If there is no logged in user, FirebaseUser will
+ * be null.
+ *
+ * Note that onActive() and onInactive() will get triggered when the configuration changes (for
+ * example when the device is rotated). This may be undesirable or expensive depending on the
+ * nature of your LiveData object, but is okay for this purpose since we are only adding and
+ * removing the authStateListener.
+ */
+class FirebaseUserLiveData : LiveData() {
+ private val firebaseAuth = FirebaseAuth.getInstance()
+
+ // Set the value of this FireUserLiveData object by hooking it up to equal the value of the
+ // current FirebaseUser. You can utilize the FirebaseAuth.AuthStateListener callback to get
+ // updates on the current Firebase user logged into the app.
+ private val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
+ // Use the FirebaseAuth instance instantiated at the beginning of the class to get an
+ // entry point into the Firebase Authentication SDK the app is using.
+ // With an instance of the FirebaseAuth class, you can now query for the current user.
+ value = firebaseAuth.currentUser
+ }
+
+ // When this object has an active observer, start observing the FirebaseAuth state to see if
+ // there is currently a logged in user.
+ override fun onActive() {
+ firebaseAuth.addAuthStateListener(authStateListener)
+ }
+
+ // When this object no longer has an active observer, stop observing the FirebaseAuth state to
+ // prevent memory leaks.
+ override fun onInactive() {
+ firebaseAuth.removeAuthStateListener(authStateListener)
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/authentication/LoginViewModel.kt b/starter/app/src/main/java/com/udacity/project4/authentication/LoginViewModel.kt
new file mode 100644
index 000000000..b8a6a20ed
--- /dev/null
+++ b/starter/app/src/main/java/com/udacity/project4/authentication/LoginViewModel.kt
@@ -0,0 +1,22 @@
+package com.udacity.project4.authentication
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.map
+
+class LoginViewModel : ViewModel() {
+
+ enum class AuthenticationState {
+ AUTHENTICATED, UNAUTHENTICATED, INVALID_AUTHENTICATION
+ }
+
+ // Create an authenticationState variable based off the FirebaseUserLiveData object. By
+ // creating this variable, other classes will be able to query for whether the user is logged
+ // in or not
+ val authenticationState = FirebaseUserLiveData().map { user ->
+ if (user != null) {
+ AuthenticationState.AUTHENTICATED
+ } else {
+ AuthenticationState.UNAUTHENTICATED
+ }
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/base/BaseFragment.kt b/starter/app/src/main/java/com/udacity/project4/base/BaseFragment.kt
index 805ac3622..6b92451e3 100644
--- a/starter/app/src/main/java/com/udacity/project4/base/BaseFragment.kt
+++ b/starter/app/src/main/java/com/udacity/project4/base/BaseFragment.kt
@@ -10,6 +10,7 @@ import com.google.android.material.snackbar.Snackbar
* Base Fragment to observe on the common LiveData objects
*/
abstract class BaseFragment : Fragment() {
+
/**
* Every fragment has to have an instance of a view model that extends from the BaseViewModel
*/
@@ -24,10 +25,10 @@ abstract class BaseFragment : Fragment() {
Toast.makeText(activity, it, Toast.LENGTH_LONG).show()
})
_viewModel.showSnackBar.observe(this, Observer {
- Snackbar.make(this.view!!, it, Snackbar.LENGTH_LONG).show()
+ Snackbar.make(this.requireView(), it, Snackbar.LENGTH_LONG).show()
})
_viewModel.showSnackBarInt.observe(this, Observer {
- Snackbar.make(this.view!!, getString(it), Snackbar.LENGTH_LONG).show()
+ Snackbar.make(this.requireView(), getString(it), Snackbar.LENGTH_LONG).show()
})
_viewModel.navigationCommand.observe(this, Observer { command ->
diff --git a/starter/app/src/main/java/com/udacity/project4/base/BaseRecyclerViewAdapter.kt b/starter/app/src/main/java/com/udacity/project4/base/BaseRecyclerViewAdapter.kt
index ee2cbd3a8..0abc392fb 100644
--- a/starter/app/src/main/java/com/udacity/project4/base/BaseRecyclerViewAdapter.kt
+++ b/starter/app/src/main/java/com/udacity/project4/base/BaseRecyclerViewAdapter.kt
@@ -23,12 +23,9 @@ abstract class BaseRecyclerViewAdapter(private val callback: ((item: T) -> Un
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
-
val binding = DataBindingUtil
.inflate(layoutInflater, getLayoutRes(viewType), parent, false)
-
binding.lifecycleOwner = getLifecycleOwner()
-
return DataBindingViewHolder(binding)
}
@@ -66,5 +63,4 @@ abstract class BaseRecyclerViewAdapter(private val callback: ((item: T) -> Un
open fun getLifecycleOwner(): LifecycleOwner? {
return null
}
-}
-
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/base/BaseViewModel.kt b/starter/app/src/main/java/com/udacity/project4/base/BaseViewModel.kt
index 5e109543a..97602b499 100644
--- a/starter/app/src/main/java/com/udacity/project4/base/BaseViewModel.kt
+++ b/starter/app/src/main/java/com/udacity/project4/base/BaseViewModel.kt
@@ -9,7 +9,6 @@ import com.udacity.project4.utils.SingleLiveEvent
* Base class for View Models to declare the common LiveData objects in one place
*/
abstract class BaseViewModel(app: Application) : AndroidViewModel(app) {
-
val navigationCommand: SingleLiveEvent = SingleLiveEvent()
val showErrorMessage: SingleLiveEvent = SingleLiveEvent()
val showSnackBar: SingleLiveEvent = SingleLiveEvent()
@@ -17,5 +16,4 @@ abstract class BaseViewModel(app: Application) : AndroidViewModel(app) {
val showToast: SingleLiveEvent = SingleLiveEvent()
val showLoading: SingleLiveEvent = SingleLiveEvent()
val showNoData: MutableLiveData = MutableLiveData()
-
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/base/DataBindingViewHolder.kt b/starter/app/src/main/java/com/udacity/project4/base/DataBindingViewHolder.kt
index ab63e4eb3..9b7681578 100644
--- a/starter/app/src/main/java/com/udacity/project4/base/DataBindingViewHolder.kt
+++ b/starter/app/src/main/java/com/udacity/project4/base/DataBindingViewHolder.kt
@@ -11,7 +11,7 @@ class DataBindingViewHolder(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: T) {
- binding.setVariable(BR.item , item)
+ binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/ReminderDescriptionActivity.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/ReminderDescriptionActivity.kt
index 6cf227bcf..bc6166443 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/ReminderDescriptionActivity.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/ReminderDescriptionActivity.kt
@@ -3,35 +3,60 @@ package com.udacity.project4.locationreminders
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.udacity.project4.R
import com.udacity.project4.databinding.ActivityReminderDescriptionBinding
import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem
+import com.udacity.project4.utils.TAG
/**
* Activity that displays the reminder details after the user clicks on the notification
*/
class ReminderDescriptionActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityReminderDescriptionBinding
+
companion object {
private const val EXTRA_ReminderDataItem = "EXTRA_ReminderDataItem"
- // receive the reminder object after the user clicks on the notification
+ private var reminderTitle = ""
+ private var description = ""
+ private var location = ""
+ private var latitude = 0.0
+ private var longitude = 0.0
+
+ // Receive the reminder object after the user clicks on the notification
fun newIntent(context: Context, reminderDataItem: ReminderDataItem): Intent {
+ Log.d(TAG, "ReminderDescriptionActivity.newIntent() -> reminderDataItem: $reminderDataItem")
val intent = Intent(context, ReminderDescriptionActivity::class.java)
intent.putExtra(EXTRA_ReminderDataItem, reminderDataItem)
+
+ reminderTitle = reminderDataItem.title.toString()
+ description = reminderDataItem.description.toString()
+ location = reminderDataItem.location.toString()
+ reminderDataItem.latitude?.let {
+ latitude = it
+ }
+ reminderDataItem.longitude?.let {
+ longitude = it
+ }
return intent
}
}
- private lateinit var binding: ActivityReminderDescriptionBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- binding = DataBindingUtil.setContentView(
- this,
- R.layout.activity_reminder_description
- )
-// TODO: Add the implementation of the reminder details
+ Log.d(TAG, "ReminderDescriptionActivity.onCreate().")
+
+ val layoutId = R.layout.activity_reminder_description
+ binding = DataBindingUtil.setContentView(this, layoutId)
+
+ binding.title.text = reminderTitle
+ binding.description.text = description
+ binding.location.text = location
+ binding.latitude.text = latitude.toString()
+ binding.longitude.text = longitude.toString()
}
-}
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/RemindersActivity.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/RemindersActivity.kt
index 271fed105..787f84b26 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/RemindersActivity.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/RemindersActivity.kt
@@ -1,34 +1,31 @@
package com.udacity.project4.locationreminders
-import android.Manifest
-import android.content.pm.PackageManager
import android.os.Bundle
-import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-import androidx.navigation.fragment.NavHostFragment
-import com.udacity.project4.R
-import kotlinx.android.synthetic.main.activity_reminders.*
+import com.udacity.project4.databinding.ActivityRemindersBinding
/**
* The RemindersActivity that holds the reminders fragments
*/
class RemindersActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityRemindersBinding
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_reminders)
-
+ binding = ActivityRemindersBinding.inflate(layoutInflater)
+ setContentView(binding.root)
}
+ /*
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
- (nav_host_fragment as NavHostFragment).navController.popBackStack()
+ (binding.navHostFragment as NavHostFragment).navController.popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
-}
+ */
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/dto/Result.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/dto/Result.kt
index 69099ec11..6c63d0094 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/dto/Result.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/dto/Result.kt
@@ -1,6 +1,5 @@
package com.udacity.project4.locationreminders.data.dto
-
/**
* A sealed class that encapsulates successful outcome with a value of type [T]
* or a failure with message and statusCode
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/LocalDB.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/LocalDB.kt
index 82d2b3cb3..4a482c119 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/LocalDB.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/LocalDB.kt
@@ -3,14 +3,13 @@ package com.udacity.project4.locationreminders.data.local
import android.content.Context
import androidx.room.Room
-
/**
* Singleton class that is used to create a reminder db
*/
object LocalDB {
/**
- * static method that creates a reminder class and returns the DAO of the reminder
+ * Static method that creates a reminder class and returns the DAO of the reminder
*/
fun createRemindersDao(context: Context): RemindersDao {
return Room.databaseBuilder(
@@ -18,5 +17,4 @@ object LocalDB {
RemindersDatabase::class.java, "locationReminders.db"
).build().reminderDao()
}
-
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersDao.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersDao.kt
index 837efecb0..f58990e0d 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersDao.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersDao.kt
@@ -4,7 +4,6 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
-import androidx.room.Update
import com.udacity.project4.locationreminders.data.dto.ReminderDTO
/**
@@ -12,6 +11,7 @@ import com.udacity.project4.locationreminders.data.dto.ReminderDTO
*/
@Dao
interface RemindersDao {
+
/**
* @return all reminders.
*/
@@ -27,7 +27,6 @@ interface RemindersDao {
/**
* Insert a reminder in the database. If the reminder already exists, replace it.
- *
* @param reminder the reminder to be inserted.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -38,5 +37,4 @@ interface RemindersDao {
*/
@Query("DELETE FROM reminders")
suspend fun deleteAllReminders()
-
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersDatabase.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersDatabase.kt
index ac77d06eb..99e360444 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersDatabase.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersDatabase.kt
@@ -9,6 +9,5 @@ import com.udacity.project4.locationreminders.data.dto.ReminderDTO
*/
@Database(entities = [ReminderDTO::class], version = 1, exportSchema = false)
abstract class RemindersDatabase : RoomDatabase() {
-
abstract fun reminderDao(): RemindersDao
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersLocalRepository.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersLocalRepository.kt
index 4ff8d984f..a2d16ec6b 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersLocalRepository.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/data/local/RemindersLocalRepository.kt
@@ -3,11 +3,13 @@ package com.udacity.project4.locationreminders.data.local
import com.udacity.project4.locationreminders.data.ReminderDataSource
import com.udacity.project4.locationreminders.data.dto.ReminderDTO
import com.udacity.project4.locationreminders.data.dto.Result
-import kotlinx.coroutines.*
+import com.udacity.project4.utils.wrapEspressoIdlingResource
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
/**
* Concrete implementation of a data source as a db.
- *
* The repository is implemented so that you can focus on only testing it.
*
* @param remindersDao the dao that does the Room db operations
@@ -22,11 +24,13 @@ class RemindersLocalRepository(
* Get the reminders list from the local db
* @return Result the holds a Success with all the reminders or an Error object with the error message
*/
- override suspend fun getReminders(): Result> = withContext(ioDispatcher) {
- return@withContext try {
- Result.Success(remindersDao.getReminders())
- } catch (ex: Exception) {
- Result.Error(ex.localizedMessage)
+ override suspend fun getReminders(): Result> = wrapEspressoIdlingResource {
+ withContext(ioDispatcher) {
+ return@withContext try {
+ Result.Success(remindersDao.getReminders())
+ } catch (ex: Exception) {
+ Result.Error(ex.localizedMessage)
+ }
}
}
@@ -34,26 +38,29 @@ class RemindersLocalRepository(
* Insert a reminder in the db.
* @param reminder the reminder to be inserted
*/
- override suspend fun saveReminder(reminder: ReminderDTO) =
+ override suspend fun saveReminder(reminder: ReminderDTO) = wrapEspressoIdlingResource {
withContext(ioDispatcher) {
remindersDao.saveReminder(reminder)
}
+ }
/**
* Get a reminder by its id
* @param id to be used to get the reminder
* @return Result the holds a Success object with the Reminder or an Error object with the error message
*/
- override suspend fun getReminder(id: String): Result = withContext(ioDispatcher) {
- try {
- val reminder = remindersDao.getReminderById(id)
- if (reminder != null) {
- return@withContext Result.Success(reminder)
- } else {
- return@withContext Result.Error("Reminder not found!")
+ override suspend fun getReminder(id: String): Result = wrapEspressoIdlingResource {
+ withContext(ioDispatcher) {
+ try {
+ val reminder = remindersDao.getReminderById(id)
+ if (reminder != null) {
+ return@withContext Result.Success(reminder)
+ } else {
+ return@withContext Result.Error("Reminder not found!")
+ }
+ } catch (e: Exception) {
+ return@withContext Result.Error(e.localizedMessage)
}
- } catch (e: Exception) {
- return@withContext Result.Error(e.localizedMessage)
}
}
@@ -61,8 +68,10 @@ class RemindersLocalRepository(
* Deletes all the reminders in the db
*/
override suspend fun deleteAllReminders() {
- withContext(ioDispatcher) {
- remindersDao.deleteAllReminders()
+ wrapEspressoIdlingResource {
+ withContext(ioDispatcher) {
+ remindersDao.deleteAllReminders()
+ }
}
}
}
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceBroadcastReceiver.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceBroadcastReceiver.kt
index 83eb044b4..bcc133995 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceBroadcastReceiver.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceBroadcastReceiver.kt
@@ -3,21 +3,33 @@ package com.udacity.project4.locationreminders.geofence
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.util.Log
+import androidx.work.ExistingWorkPolicy
+import androidx.work.WorkManager
+import com.google.gson.Gson
+import com.udacity.project4.MyApp
+import com.udacity.project4.utils.TAG
/**
- * Triggered by the Geofence. Since we can have many Geofences at once, we pull the request
- * ID from the first Geofence, and locate it within the cached data in our Room DB
+ * Triggered by the Geofence. Since we can have many GeoFences at once, we pull the request
+ * ID from the first Geofence, and locate it within the cached data in our Room DB.
*
- * Or users can add the reminders and then close the app, So our app has to run in the background
+ * Or users can add the reminders and then close the app, so our app has to run in the background
* and handle the geofencing in the background.
- * To do that you can use https://developer.android.com/reference/android/support/v4/app/JobIntentService to do that.
- *
+ * To do that you can use:
+ * https://developer.android.com/reference/android/support/v4/app/JobIntentService to do that.
*/
-
class GeofenceBroadcastReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
-
-//TODO: implement the onReceive method to receive the geofencing events at the background
-
- }
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "GeofenceBroadcastReceiver.onReceive().")
+ (context.applicationContext as MyApp).broadcastIntent = intent
+ val workManager = WorkManager.getInstance(context)
+ val intentToBundle = Gson().toJson(intent)
+ val workRequest = GeofenceTransitionsWorkManager.buildWorkRequest(intentToBundle)
+ workManager.enqueueUniqueWork(
+ UNIQUE_WORK_NAME,
+ ExistingWorkPolicy.KEEP,
+ workRequest
+ )
+ }
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceTransitionsJobIntentService.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceTransitionsJobIntentService.kt
deleted file mode 100644
index 5932cd3ca..000000000
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceTransitionsJobIntentService.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.udacity.project4.locationreminders.geofence
-
-import android.content.Context
-import android.content.Intent
-import androidx.core.app.JobIntentService
-import com.google.android.gms.location.Geofence
-import com.udacity.project4.locationreminders.data.dto.ReminderDTO
-import com.udacity.project4.locationreminders.data.dto.Result
-import com.udacity.project4.locationreminders.data.local.RemindersLocalRepository
-import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem
-import com.udacity.project4.utils.sendNotification
-import kotlinx.coroutines.*
-import org.koin.android.ext.android.inject
-import kotlin.coroutines.CoroutineContext
-
-class GeofenceTransitionsJobIntentService : JobIntentService(), CoroutineScope {
-
- private var coroutineJob: Job = Job()
- override val coroutineContext: CoroutineContext
- get() = Dispatchers.IO + coroutineJob
-
- companion object {
- private const val JOB_ID = 573
-
- // TODO: call this to start the JobIntentService to handle the geofencing transition events
- fun enqueueWork(context: Context, intent: Intent) {
- enqueueWork(
- context,
- GeofenceTransitionsJobIntentService::class.java, JOB_ID,
- intent
- )
- }
- }
-
- override fun onHandleWork(intent: Intent) {
- //TODO: handle the geofencing transition events and
- // send a notification to the user when he enters the geofence area
- //TODO call @sendNotification
- }
-
- //TODO: get the request id of the current geofence
- private fun sendNotification(triggeringGeofences: List) {
- val requestId = ""
-
- //Get the local repository instance
- val remindersLocalRepository: ReminderDataSource by inject()
-// Interaction to the repository has to be through a coroutine scope
- CoroutineScope(coroutineContext).launch(SupervisorJob()) {
- //get the reminder with the request id
- val result = remindersLocalRepository.getReminder(requestId)
- if (result is Result.Success) {
- val reminderDTO = result.data
- //send a notification to the user with the reminder details
- sendNotification(
- this@GeofenceTransitionsJobIntentService, ReminderDataItem(
- reminderDTO.title,
- reminderDTO.description,
- reminderDTO.location,
- reminderDTO.latitude,
- reminderDTO.longitude,
- reminderDTO.id
- )
- )
- }
- }
- }
-
-}
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceTransitionsWorkManager.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceTransitionsWorkManager.kt
new file mode 100644
index 000000000..10be19a09
--- /dev/null
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/geofence/GeofenceTransitionsWorkManager.kt
@@ -0,0 +1,119 @@
+package com.udacity.project4.locationreminders.geofence
+
+import android.Manifest
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import android.widget.Toast
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.google.android.gms.location.Geofence
+import com.google.android.gms.location.GeofencingEvent
+import com.udacity.project4.MyApp
+import com.udacity.project4.R
+import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem
+import com.udacity.project4.utils.TAG
+import com.udacity.project4.utils.errorMessage
+import com.udacity.project4.utils.permissionGranted
+import com.udacity.project4.utils.sendNotification
+
+/**
+ * - Source:
+ * https://www.kodeco.com/7372-geofencing-api-tutorial-for-android
+ * - JobIntentService is deprecated:
+ * https://medium.com/tech-takeaways/how-to-migrate-the-deprecated-jobintentservice-a0071a7957ed
+ */
+const val UNIQUE_WORK_NAME = "GeofenceTransitionsWorkManager"
+
+class GeofenceTransitionsWorkManager(
+ val context: Context, workerParameters: WorkerParameters
+) : Worker(context, workerParameters) {
+
+ companion object {
+ private const val INTENT_PARAM = "INTENT_PARAM"
+
+ fun buildWorkRequest(parameter: String): OneTimeWorkRequest {
+// val data = Data.Builder().putString(INTENT_PARAM, parameter).build()
+ return OneTimeWorkRequestBuilder().apply {
+// setInputData(data)
+ }.build()
+ }
+ }
+
+ override fun doWork(): Result {
+// val parameter: String? = inputData.getString(INTENT_PARAM)
+ // Handle your work.
+ val intent = (context.applicationContext as MyApp).broadcastIntent
+ Log.d(TAG, "GeofenceTransitionsWorkManager.doWork() -> [1]")
+ val geofencingEvent = GeofencingEvent.fromIntent(intent)
+ if (geofencingEvent != null) {
+ Log.d(TAG, "GeofenceTransitionsWorkManager.doWork() -> [2]")
+ if (geofencingEvent.hasError()) {
+ Log.d(TAG, "GeofenceTransitionsWorkManager.doWork() -> [3]")
+ val errorMessage = errorMessage(context, geofencingEvent.errorCode)
+ Log.e(TAG, "GeofenceTransitionsWorkManager.doWork() -> $errorMessage")
+ return Result.failure()
+ } else {
+ Log.d(TAG, "GeofenceTransitionsWorkManager.doWork() -> [4]")
+ }
+ handleEvent(geofencingEvent)
+ } else {
+ Log.d(TAG, "GeofenceTransitionsWorkManager.doWork() -> [5]")
+ }
+ return Result.success()
+ }
+
+ /**
+ * Source:
+ * https://developer.android.com/training/location/geofencing#HandleGeofenceTransitions
+ */
+ private fun handleEvent(event: GeofencingEvent) {
+ Log.d(TAG, "GeofenceTransitionsWorkManager.handleEvent().")
+ val geofenceTransition = event.geofenceTransition
+// if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
+ if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
+ geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
+ Log.d(TAG, "GeofenceTransitionsWorkManager.handleEvent() -> " +
+ "event.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER")
+ event.triggeringGeofences?.let { list ->
+ for (item in list) {
+ val geofenceTransitionDetails = getGeofenceTransitionDetails(item)
+ checkNotification(context, geofenceTransitionDetails)
+ }
+ }
+ } else {
+ Log.d(TAG, "GeofenceTransitionsWorkManager.handleEvent() -> [2]")
+ }
+ }
+
+ private fun getGeofenceTransitionDetails(currentGeoFence: Geofence): ReminderDataItem {
+ val id = currentGeoFence.requestId
+ val lat = currentGeoFence.latitude
+ val lng = currentGeoFence.longitude
+ return ReminderDataItem(
+ title = id,
+ description = "",
+ location = id,
+ latitude = lat,
+ longitude = lng
+ )
+ }
+
+ private fun checkNotification(context: Context, reminderDataItem: ReminderDataItem) {
+ val notificationPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ context.permissionGranted(Manifest.permission.POST_NOTIFICATIONS)
+ } else {
+ true
+ }
+ Log.d(TAG, "GeofenceTransitionsWorkManager.callNotification() -> " +
+ "notificationPermission: $notificationPermission")
+ if (notificationPermission) {
+ sendNotification(context, reminderDataItem)
+ } else {
+ val message = context.getString(R.string.no_notification_permission)
+ Toast.makeText(context, message, Toast.LENGTH_LONG).show()
+ }
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/ReminderListFragment.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/ReminderListFragment.kt
index befe344ce..5d0729c7d 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/ReminderListFragment.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/ReminderListFragment.kt
@@ -1,43 +1,81 @@
package com.udacity.project4.locationreminders.reminderslist
+import android.Manifest
+import android.os.Build
import android.os.Bundle
-import android.view.*
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.view.MenuHost
+import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.Lifecycle
+import com.firebase.ui.auth.AuthUI
+import com.udacity.project4.MyApp
import com.udacity.project4.R
import com.udacity.project4.base.BaseFragment
import com.udacity.project4.base.NavigationCommand
import com.udacity.project4.databinding.FragmentRemindersBinding
+import com.udacity.project4.utils.TAG
+import com.udacity.project4.utils.permissionGranted
import com.udacity.project4.utils.setDisplayHomeAsUpEnabled
import com.udacity.project4.utils.setTitle
import com.udacity.project4.utils.setup
+import com.udacity.project4.utils.toast
import org.koin.androidx.viewmodel.ext.android.viewModel
class ReminderListFragment : BaseFragment() {
- //use Koin to retrieve the ViewModel instance
+
+ companion object {
+ private const val POST = Manifest.permission.POST_NOTIFICATIONS
+ }
+
+ // Use Koin to retrieve the ViewModel instance.
override val _viewModel: RemindersListViewModel by viewModel()
private lateinit var binding: FragmentRemindersBinding
+ private var logoutClicked = false
+ private lateinit var parent: FragmentActivity
+
+ private val requestPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ Log.d(TAG, "ReminderListFragment.requestPermissionLauncher -> isGranted: $isGranted")
+ (parent.applicationContext as MyApp).hasNotificationPermission = isGranted
+ if (isGranted) {
+ Log.d(TAG, "ReminderListFragment.requestPermissionLauncher -> Notification permission is granted.")
+ parent.toast(R.string.notification_permission_granted)
+ } else {
+ Log.d(TAG, "ReminderListFragment.requestPermissionLauncher -> Notification permission is NOT granted.")
+ parent.toast(R.string.no_notification_permission)
+ }
+ }
+
+ //--------------------------------------------------
+ // Lifecycle Methods
+ //--------------------------------------------------
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
- binding =
- DataBindingUtil.inflate(
- inflater,
- R.layout.fragment_reminders, container, false
- )
+ ): View {
+ val layoutId = R.layout.fragment_reminders
+ binding = DataBindingUtil.inflate(inflater, layoutId, container, false)
binding.viewModel = _viewModel
-
- setHasOptionsMenu(true)
- setDisplayHomeAsUpEnabled(false)
- setTitle(getString(R.string.app_name))
-
- binding.refreshLayout.setOnRefreshListener { _viewModel.loadReminders() }
+ init()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ Log.d(TAG, "ReminderListFragment.onViewCreated().")
binding.lifecycleOwner = this
setupRecyclerView()
binding.addReminderFAB.setOnClickListener {
@@ -47,41 +85,105 @@ class ReminderListFragment : BaseFragment() {
override fun onResume() {
super.onResume()
- //load the reminders list on the ui
+ Log.d(TAG, "ReminderListFragment.onResume().")
+
+ logoutClicked = false
+ // Load the reminders list on the ui
_viewModel.loadReminders()
}
+ //--------------------------------------------------
+ // Main Methods
+ //--------------------------------------------------
+
+ private fun init() {
+ Log.d(TAG, "ReminderListFragment.init().")
+ parent = requireActivity()
+ parent.onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler)
+
+ val menuHost: MenuHost = parent
+ addMenu(menuHost)
+
+ setDisplayHomeAsUpEnabled(false)
+ setTitle(getString(R.string.app_name))
+
+ binding.refreshLayout.setOnRefreshListener { _viewModel.loadReminders() }
+
+ requestPermissionListener()
+ }
+
private fun navigateToAddReminder() {
- //use the navigationCommand live data to navigate between the fragments
+ Log.d(TAG, "ReminderListFragment.navigateToAddReminder().")
+ // Use the navigationCommand live data to navigate between the fragments
_viewModel.navigationCommand.postValue(
NavigationCommand.To(
- ReminderListFragmentDirections.toSaveReminder()
+ ReminderListFragmentDirections.actionReminderListFragmentToSaveReminderFragment()
)
)
}
private fun setupRecyclerView() {
- val adapter = RemindersListAdapter {
- }
-
-// setup the recycler view using the extension function
+ Log.d(TAG, "ReminderListFragment.setupRecyclerView().")
+ val adapter = RemindersListAdapter {}
+ // Setup the recycler view using the extension function
binding.reminderssRecyclerView.setup(adapter)
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.logout -> {
-// TODO: add the logout implementation
+ private fun requestPermissionListener() {
+ Log.d(TAG, "ReminderListFragment.requestPermissionListener().")
+ when {
+ parent.permissionGranted(POST) -> {
+ // You can use the API that requires the permission.
+ Log.d(TAG, "ReminderListFragment.requestPermissionListener() -> Permission granted!")
+ }
+ else -> {
+ // The registered ActivityResultCallback gets the result of this request.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ requestPermissionLauncher.launch(POST)
+ }
}
}
- return super.onOptionsItemSelected(item)
-
}
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- super.onCreateOptionsMenu(menu, inflater)
-// display logout as menu item
- inflater.inflate(R.menu.main_menu, menu)
+ //--------------------------------------------------
+ // Menu Methods
+ //--------------------------------------------------
+
+ /**
+ * Source:
+ * https://java73.medium.com/back-press-handling-with-androidx-navigation-component-60bbc0fd169
+ */
+ private val backPressHandler = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (logoutClicked) {
+ AuthUI.getInstance().signOut(requireContext())
+ }
+ activity?.finishAndRemoveTask()
+ }
}
-}
+ private fun addMenu(menuHost: MenuHost) {
+ Log.d(TAG, "ReminderListFragment.addMenu().")
+ // Add menu items without using the Fragment Menu APIs
+ // Note how we can tie the MenuProvider to the viewLifecycleOwner
+ // and an optional Lifecycle.State (here, RESUMED) to indicate when
+ // the menu should be visible
+ menuHost.addMenuProvider(object : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.main_menu, menu)
+ }
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ when (menuItem.itemId) {
+ R.id.logout -> {
+ logoutClicked = true
+ parent.onBackPressedDispatcher.onBackPressed()
+ }
+ android.R.id.home -> {
+ parent.onBackPressedDispatcher.onBackPressed()
+ }
+ }
+ return true
+ }
+ }, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/RemindersListAdapter.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/RemindersListAdapter.kt
index 4cb48f178..846fa82e0 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/RemindersListAdapter.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/RemindersListAdapter.kt
@@ -3,8 +3,7 @@ package com.udacity.project4.locationreminders.reminderslist
import com.udacity.project4.R
import com.udacity.project4.base.BaseRecyclerViewAdapter
-
-//Use data binding to show the reminder on the item
+// Use data binding to show the reminder on the item
class RemindersListAdapter(callBack: (selectedReminder: ReminderDataItem) -> Unit) :
BaseRecyclerViewAdapter(callBack) {
override fun getLayoutRes(viewType: Int) = R.layout.it_reminder
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/RemindersListViewModel.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/RemindersListViewModel.kt
index f76bbd7b7..1c1f538ed 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/RemindersListViewModel.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/reminderslist/RemindersListViewModel.kt
@@ -1,19 +1,21 @@
package com.udacity.project4.locationreminders.reminderslist
import android.app.Application
+import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.udacity.project4.base.BaseViewModel
import com.udacity.project4.locationreminders.data.ReminderDataSource
import com.udacity.project4.locationreminders.data.dto.ReminderDTO
import com.udacity.project4.locationreminders.data.dto.Result
+import com.udacity.project4.utils.TAG
import kotlinx.coroutines.launch
class RemindersListViewModel(
app: Application,
private val dataSource: ReminderDataSource
) : BaseViewModel(app) {
- // list that holds the reminder data to be displayed on the UI
+ // List that holds the reminder data to be displayed on the UI.
val remindersList = MutableLiveData>()
/**
@@ -21,16 +23,20 @@ class RemindersListViewModel(
* or show error if any
*/
fun loadReminders() {
+ Log.d(TAG, "RemindersListViewModel.loadReminders().")
showLoading.value = true
viewModelScope.launch {
- //interacting with the dataSource has to be through a coroutine
+ // Interacting with the dataSource has to be through a coroutine.
val result = dataSource.getReminders()
showLoading.postValue(false)
when (result) {
is Result.Success<*> -> {
+ Log.d(TAG, "RemindersListViewModel.loadReminders() -> Result.Success")
val dataList = ArrayList()
dataList.addAll((result.data as List).map { reminder ->
- //map the reminder data from the DB to the be ready to be displayed on the UI
+ // Map the reminder data from the DB to the be ready to be displayed on the UI.
+ Log.d(TAG, "RemindersListViewModel.loadReminders() -> reminder: $reminder")
+
ReminderDataItem(
reminder.title,
reminder.description,
@@ -42,19 +48,22 @@ class RemindersListViewModel(
})
remindersList.value = dataList
}
- is Result.Error ->
+ is Result.Error -> {
+ Log.d(TAG, "RemindersListViewModel.loadReminders() -> Result.Error")
showSnackBar.value = result.message
+ }
}
- //check if no data has to be shown
+ // Check if no data has to be shown.
invalidateShowNoData()
}
}
/**
- * Inform the user that there's not any data if the remindersList is empty
+ * Inform the user that there's not any data if the remindersList is empty.
*/
private fun invalidateShowNoData() {
+ Log.d(TAG, "RemindersListViewModel.invalidateShowNoData().")
showNoData.value = remindersList.value == null || remindersList.value!!.isEmpty()
}
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderFragment.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderFragment.kt
index 04e7f182d..3bebd887d 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderFragment.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderFragment.kt
@@ -1,61 +1,449 @@
package com.udacity.project4.locationreminders.savereminder
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Build
import android.os.Bundle
+import android.provider.Settings
+import android.util.Log
import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.MenuHost
+import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.Lifecycle
+import com.google.android.gms.location.Geofence
+import com.google.android.gms.location.Geofence.NEVER_EXPIRE
+import com.google.android.gms.location.GeofencingClient
+import com.google.android.gms.location.GeofencingRequest
+import com.google.android.gms.location.LocationServices
+import com.google.android.gms.maps.model.LatLng
+import com.udacity.project4.MyApp
import com.udacity.project4.R
import com.udacity.project4.base.BaseFragment
import com.udacity.project4.base.NavigationCommand
import com.udacity.project4.databinding.FragmentSaveReminderBinding
+import com.udacity.project4.locationreminders.geofence.GeofenceBroadcastReceiver
+import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem
+import com.udacity.project4.locationreminders.savereminder.selectreminderlocation.SelectLocationFragment.Companion.ARGUMENTS
+import com.udacity.project4.utils.GeofencingConstants.GEOFENCE_RADIUS_IN_METERS
+import com.udacity.project4.utils.LandmarkDataObject
+import com.udacity.project4.utils.TAG
+import com.udacity.project4.utils.errorMessage
+import com.udacity.project4.utils.getNavigationResult
+import com.udacity.project4.utils.isGpsEnabled
+import com.udacity.project4.utils.permissionDeniedFeedback
+import com.udacity.project4.utils.permissionGranted
import com.udacity.project4.utils.setDisplayHomeAsUpEnabled
+import com.udacity.project4.utils.toast
import org.koin.android.ext.android.inject
class SaveReminderFragment : BaseFragment() {
- //Get the view model this time as a single to be shared with the another fragment
+
+ //--------------------------------------------------
+ // Attributes
+ //--------------------------------------------------
+
+ companion object {
+ internal const val ACTION_GEOFENCE_EVENT = "SaveReminderFragment.ACTION_GEOFENCE_EVENT"
+ const val GEO_FENCE_ID = "id"
+ const val GEO_FENCE_LAT = "lat"
+ const val GEO_FENCE_LNG = "lng"
+ }
+
+ // Get the view model this time as a single to be shared with the another fragment
override val _viewModel: SaveReminderViewModel by inject()
+
private lateinit var binding: FragmentSaveReminderBinding
+ private lateinit var parent: FragmentActivity
+ private var alertShouldEnableGps: AlertDialog? = null
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- binding =
- DataBindingUtil.inflate(inflater, R.layout.fragment_save_reminder, container, false)
+ private lateinit var geofencingClient: GeofencingClient
+ private lateinit var geofenceData: LandmarkDataObject
- setDisplayHomeAsUpEnabled(true)
+ private var currentGeoFenceLocation = ""
+ private var currentGeoFenceLatitude = 0.0
+ private var currentGeoFenceLongitude = 0.0
+ private lateinit var dataItem: ReminderDataItem
+
+ // A PendingIntent for the Broadcast Receiver that handles geofence transitions.
+ private val geofencePendingIntent: PendingIntent by lazy {
+ val intent = Intent(requireContext(), GeofenceBroadcastReceiver::class.java)
+ intent.action = ACTION_GEOFENCE_EVENT
+ intent.putExtra(GEO_FENCE_ID, geofenceData.id)
+ intent.putExtra(GEO_FENCE_LAT, geofenceData.latLong.latitude)
+ intent.putExtra(GEO_FENCE_LNG, geofenceData.latLong.longitude)
+ // Use FLAG_UPDATE_CURRENT so that you get the same pending intent back when calling
+ // addGeoFences() and removeGeoFences().
+ var intentFlagTypeUpdateCurrent = PendingIntent.FLAG_UPDATE_CURRENT
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ // Source: https://stackoverflow.com/a/74174664/1354788
+ intentFlagTypeUpdateCurrent = PendingIntent.FLAG_MUTABLE
+ }
+ PendingIntent.getBroadcast(requireContext(), 0, intent, intentFlagTypeUpdateCurrent)
+ }
+
+ //--------------------------------------------------
+ // Activity Result Callbacks
+ //--------------------------------------------------
+
+ //
+ private val finePermissionRequest = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isEnabled ->
+ Log.d(TAG, "SaveReminderFragment.finePermissionRequest -> isEnabled: $isEnabled")
+ if (isEnabled) {
+ checkGpsEnabled()
+ }
+ }
+ //
+
+ /*
+ private val finePermissionRequest = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ Log.d(TAG, "SaveReminderFragment.locationPermissionRequest -> permissions: $permissions")
+ val finePermission = permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false)
+ val coarsePermission = permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false)
+ if (finePermission && coarsePermission) {
+ Log.d(TAG, "SaveReminderFragment.finePermissionRequest -> Permissions granted.")
+ checkGpsEnabled()
+ } else {
+ // No location access granted.
+ Log.d(TAG, "SaveReminderFragment.finePermissionRequest -> No location access granted.")
+ }
+ }
+ */
+ private val locationPermissionRequest = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ Log.d(TAG, "SaveReminderFragment.locationPermissionRequest -> permissions: $permissions")
+ val finePermission = permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false)
+ val backgroundPermission = hasBackgroundPermission()
+ if (finePermission && backgroundPermission) {
+ Log.d(TAG, "SaveReminderFragment.locationPermissionRequest -> Permissions granted.")
+ checkGpsEnabled()
+ } else {
+ // No location access granted.
+ Log.d(TAG, "SaveReminderFragment.locationPermissionRequest -> No location access granted.")
+ }
+ }
+
+ /**
+ * Source:
+ * https://www.tothenew.com/blog/android-katha-onactivityresult-is-deprecated-now-what/
+ */
+ private var activityResultLauncher = registerForActivityResult(ActivityResultContracts
+ .StartActivityForResult()) {
+ Log.d(TAG, "SaveReminderFragment.activityResultLauncher.")
+ if (parent.isGpsEnabled()) {
+ Log.d(TAG, "SaveReminderFragment.activityResultLauncher -> GPS enabled.")
+ addGeoFence()
+ } else {
+ Log.d(TAG, "SaveReminderFragment.activityResultLauncher -> GPS NOT enabled.")
+ parent.permissionDeniedFeedback()
+ }
+ }
+
+ //--------------------------------------------------
+ // Lifecycle Methods
+ //--------------------------------------------------
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View {
+ Log.d(TAG, "SaveReminderFragment.onCreateView().")
+
+ val layoutId = R.layout.fragment_save_reminder
+ binding = DataBindingUtil.inflate(inflater, layoutId, container, false)
binding.viewModel = _viewModel
+ init()
return binding.root
}
+ private fun init() {
+ Log.d(TAG, "SaveReminderFragment.init().")
+ setDisplayHomeAsUpEnabled(true)
+ parent = requireActivity()
+ parent.onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler)
+
+ val menuHost: MenuHost = parent
+ addMenu(menuHost)
+
+ geofencingClient = LocationServices.getGeofencingClient(parent)
+ }
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ Log.d(TAG, "SaveReminderFragment.onViewCreated().")
+
binding.lifecycleOwner = this
binding.selectLocation.setOnClickListener {
- // Navigate to another fragment to get the user location
- _viewModel.navigationCommand.value =
- NavigationCommand.To(SaveReminderFragmentDirections.actionSaveReminderFragmentToSelectLocationFragment())
+ Log.d(TAG, "SaveReminderFragment.onViewCreated() -> binding.selectLocation.setOnClickListener")
+ // Navigate to another fragment to get the user location
+ val directions = SaveReminderFragmentDirections
+ .actionSaveReminderFragmentToSelectLocationFragment()
+ _viewModel.navigationCommand.value = NavigationCommand.To(directions)
}
+ // Use the user entered reminder details to:
+ // 1) Check GeoFence permissions.
+ // 2) Add a geofencing request.
+ // 3) Save the reminder to the local db.
binding.saveReminder.setOnClickListener {
- val title = _viewModel.reminderTitle.value
- val description = _viewModel.reminderDescription
- val location = _viewModel.reminderSelectedLocationStr.value
- val latitude = _viewModel.latitude
- val longitude = _viewModel.longitude.value
+ Log.d(TAG, "SaveReminderFragment.onViewCreated() -> binding.saveReminder.setOnClickListener.")
+ val selectedPoi = _viewModel.selectedPOI.value
+ val location = selectedPoi?.name
+ val lat = selectedPoi?.latLng?.latitude
+ val lng = selectedPoi?.latLng?.longitude
-// TODO: use the user entered reminder details to:
-// 1) add a geofencing request
-// 2) save the reminder to the local db
+ if (location != null) {
+ currentGeoFenceLocation = location
+ }
+ if (lat != null) {
+ currentGeoFenceLatitude = lat
+ }
+ if (lng != null) {
+ currentGeoFenceLongitude = lng
+ }
+ saveReminderOnDatabase()
}
}
override fun onDestroy() {
super.onDestroy()
- //make sure to clear the view model after destroy, as it's a single view model.
+ Log.d(TAG, "SaveReminderFragment.onDestroy().")
+ // Make sure to clear the view model after destroy, as it's a single view model.
_viewModel.onClear()
+ disableDialogs()
+ }
+
+ private fun disableDialogs() {
+ Log.d(TAG, "SaveReminderFragment.disableDialogs().")
+ alertShouldEnableGps?.let {
+ if (it.isShowing) {
+ it.cancel()
+ }
+ }
+ }
+
+ //--------------------------------------------------
+ // Database Methods
+ //--------------------------------------------------
+
+ private fun saveReminderOnDatabase() {
+ val app = requireActivity().application as MyApp
+ Log.d(TAG, "SaveReminderFragment.saveReminderOnDatabase().")
+ val title = _viewModel.reminderTitle.value
+ val description = _viewModel.reminderDescription.value
+ dataItem = ReminderDataItem(
+ title = title,
+ description = description,
+ location = currentGeoFenceLocation,
+ latitude = currentGeoFenceLatitude,
+ longitude = currentGeoFenceLongitude
+ )
+
+ if (app.testingMode) {
+ _viewModel.validateAndSaveReminder(dataItem)
+ } else {
+ val paramsValid = _viewModel.validateEnteredData(dataItem)
+// val paramsValid = titleValid &&
+// currentGeoFenceLocation.isNotEmpty() &&
+// currentGeoFenceLatitude != 0.0 &&
+// currentGeoFenceLongitude != 0.0
+ if (paramsValid) {
+ checkGeoFencePermissions()
+ } else {
+ Log.d(TAG, "SaveReminderFragment.saveReminderOnDatabase() -> Couldn't create a Geofence.")
+ }
+ }
+ }
+
+ //--------------------------------------------------
+ // GeoFence Methods
+ //--------------------------------------------------
+
+ /**
+ * When we want to add a Geofence, the flow should be as follows:
+ *
+ * 1.First check if both the required permissions (foreground and background) have been granted.
+ * If there is any ungranted permission, request it properly.
+ *
+ * 2.If all the required permissions have been granted, then we should proceed to check if the
+ * device location is on. If the device location is not on, show the location settings dialog
+ * and ask the user to enable it.
+ *
+ * 3. We should automatically attempt to add a Geofence ONLY IF we are certain that the required
+ * permissions have been granted and the device location is on.
+ */
+ private fun checkGeoFencePermissions() {
+ Log.d(TAG, "SaveReminderFragment.checkGeoFencePermissions().")
+ val backgroundPermission = hasBackgroundPermission()
+ val finePermission = parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
+ // Source: https://developer.android.com/training/location/geofencing#RequestGeofences
+ val geofencePermissions = backgroundPermission && finePermission
+ if (geofencePermissions) {
+ Log.d(TAG, "SaveReminderFragment.checkGeoFencePermissions() -> GeoFence permissions enabled.")
+ checkGpsEnabled()
+ } else {
+ Log.d(TAG, "SaveReminderFragment.checkGeoFencePermissions() -> GeoFence permissions NOT enabled.")
+ parent.toast(R.string.location_permission_needed)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Log.d(TAG, "SaveReminderFragment.checkGeoFencePermissions() -> " +
+ "Asking for 'Manifest.permission.ACCESS_BACKGROUND_LOCATION' and " +
+ "'Manifest.permission.ACCESS_FINE_LOCATION' permissions.")
+ locationPermissionRequest.launch(arrayOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION)
+ )
+ } else {
+ Log.d(TAG, "SaveReminderFragment.checkGeoFencePermissions() -> " +
+ "Asking for 'Manifest.permission.ACCESS_FINE_LOCATION' permission.")
+ finePermissionRequest.launch(Manifest.permission.ACCESS_FINE_LOCATION)
+ }
+ }
+ }
+
+ private fun hasBackgroundPermission(): Boolean =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ parent.permissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
+ } else {
+ true
+ }
+
+ private fun checkGpsEnabled() {
+ Log.d(TAG, "SaveReminderFragment.checkGpsEnabled().")
+ if (!parent.isGpsEnabled()) {
+ Log.d(TAG, "SaveReminderFragment.checkGpsEnabled() -> GPS is NOT enabled.")
+ buildAlertMessageNoGps()
+ } else {
+ Log.d(TAG, "SaveReminderFragment.checkGpsEnabled() -> GPS is enabled.")
+ addGeoFence()
+ }
+ }
+
+ /**
+ * Source:
+ * https://stackoverflow.com/a/25175756/1354788
+ */
+ private fun buildAlertMessageNoGps() {
+ Log.d(TAG, "SaveReminderFragment.buildAlertMessageNoGps().")
+ val builder = AlertDialog.Builder(parent)
+ builder.setMessage(R.string.should_enable_gps)
+ .setCancelable(false)
+ .setPositiveButton("Yes") { _, _ ->
+ activityResultLauncher.launch(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
+ }
+ .setNegativeButton("No") { dialog, _ ->
+ dialog.cancel()
+ parent.permissionDeniedFeedback()
+ }
+ alertShouldEnableGps = builder.create()
+ alertShouldEnableGps?.show()
+ }
+
+ private fun addGeoFence() {
+ Log.d(TAG, "SaveReminderFragment.addGeoFence().")
+ // Build the Geofence Object.
+ val currentGeofenceData = LandmarkDataObject(currentGeoFenceLocation,
+ LatLng(currentGeoFenceLatitude, currentGeoFenceLongitude))
+ geofenceData = currentGeofenceData
+ val geofence = buildGeoFence(currentGeofenceData)
+
+ // Build the geofence request.
+ val geofencingRequest = GeofencingRequest.Builder()
+ // The INITIAL_TRIGGER_ENTER flag indicates that geofencing service should trigger a
+ // GEOFENCE_TRANSITION_ENTER notification when the geofence is added and if the device
+ // is already inside that geofence.
+ .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
+ // Add the GeoFences to be monitored by geofencing service.
+ .addGeofence(geofence)
+ .build()
+
+ addGeoFenceRequest(geofencingRequest, geofence)
+ }
+
+ private fun buildGeoFence(currentGeofenceData: LandmarkDataObject) : Geofence {
+ Log.d(TAG, "SaveReminderFragment.buildGeoFence().")
+ return Geofence.Builder()
+ // Set the request ID, string to identify the geofence.
+ .setRequestId(currentGeofenceData.id)
+ // Set the circular region of this geofence.
+ .setCircularRegion(
+ currentGeofenceData.latLong.latitude,
+ currentGeofenceData.latLong.longitude,
+ GEOFENCE_RADIUS_IN_METERS
+ )
+ // Set the expiration duration of the geofence.
+ .setExpirationDuration(NEVER_EXPIRE)
+ // Set the transition types of interest. Alerts are only generated for these
+ // transitions. We track entry and exit transitions in this sample.
+ .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
+ .build()
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun addGeoFenceRequest(geofencingRequest: GeofencingRequest, geofence: Geofence) {
+ Log.d(TAG, "SaveReminderFragment.addGeoFenceRequest().")
+ geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent).run {
+ addOnSuccessListener {
+ // GeoFences added.
+ parent.toast(R.string.geofences_added)
+ Log.d(TAG, "SaveReminderFragment.addGeoFenceRequest() -> SUCCESS!! Adding GeoFence: ${geofence.requestId}")
+ _viewModel.validateAndSaveReminder(dataItem)
+ }
+ addOnFailureListener {
+ // Failed to add GeoFences.
+ parent.toast(R.string.geofences_not_added)
+ val message = it.message
+ if (message != null) {
+ val errorMessage = errorMessage(parent, message.toInt())
+ Log.d(TAG, "SaveReminderFragment.addGeoFenceRequest() -> ERROR!! Error message: $errorMessage")
+ }
+ }
+ }
+ }
+
+ //--------------------------------------------------
+ // Menu Methods
+ //--------------------------------------------------
+
+ private fun addMenu(menuHost: MenuHost) {
+ Log.d(TAG, "SaveReminderFragment.addMenu().")
+ // Add menu items without using the Fragment Menu APIs. Note how we can tie the MenuProvider
+ // to the viewLifecycleOwner and an optional Lifecycle.State (here, RESUMED) to indicate
+ // when the menu should be visible.
+ menuHost.addMenuProvider(object : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {}
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ when (menuItem.itemId) {
+ android.R.id.home -> {
+ parent.onBackPressedDispatcher.onBackPressed()
+ }
+ }
+ return true
+ }
+ }, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ }
+
+ private val backPressHandler = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ _viewModel.navigationCommand.postValue(
+ NavigationCommand.Back
+ )
+ }
}
-}
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderViewModel.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderViewModel.kt
index 6e78845b1..f7bb3e526 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderViewModel.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderViewModel.kt
@@ -1,6 +1,7 @@
package com.udacity.project4.locationreminders.savereminder
import android.app.Application
+import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.android.gms.maps.model.PointOfInterest
@@ -10,42 +11,49 @@ import com.udacity.project4.base.NavigationCommand
import com.udacity.project4.locationreminders.data.ReminderDataSource
import com.udacity.project4.locationreminders.data.dto.ReminderDTO
import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem
+import com.udacity.project4.utils.TAG
import kotlinx.coroutines.launch
-class SaveReminderViewModel(val app: Application, val dataSource: ReminderDataSource) :
- BaseViewModel(app) {
- val reminderTitle = MutableLiveData()
- val reminderDescription = MutableLiveData()
- val reminderSelectedLocationStr = MutableLiveData()
- val selectedPOI = MutableLiveData()
- val latitude = MutableLiveData()
- val longitude = MutableLiveData()
+class SaveReminderViewModel(
+ val app: Application, val dataSource: ReminderDataSource
+): BaseViewModel(app) {
+ val reminderTitle = MutableLiveData()
+ val reminderDescription = MutableLiveData()
+ val selectedPOI = MutableLiveData()
+
+ val reminderSelectedLocationStr = MutableLiveData()
+
+ init {
+ reminderDescription.postValue("")
+ }
/**
- * Clear the live data objects to start fresh next time the view model gets called
+ * Clear the live data objects to start fresh next time the view model gets called.
*/
fun onClear() {
reminderTitle.value = null
reminderDescription.value = null
reminderSelectedLocationStr.value = null
selectedPOI.value = null
- latitude.value = null
- longitude.value = null
}
/**
- * Validate the entered data then saves the reminder data to the DataSource
+ * Validate the entered data then saves the reminder data to the DataSource.
*/
- fun validateAndSaveReminder(reminderData: ReminderDataItem) {
+ fun validateAndSaveReminder(reminderData: ReminderDataItem) : Boolean {
+ Log.d(TAG, "SaveReminderViewModel.validateAndSaveReminder().")
if (validateEnteredData(reminderData)) {
saveReminder(reminderData)
+ return true
}
+ return false
}
/**
- * Save the reminder to the data source
+ * Save the reminder to the data source.
*/
fun saveReminder(reminderData: ReminderDataItem) {
+ Log.d(TAG, "SaveReminderViewModel.saveReminder().")
showLoading.value = true
viewModelScope.launch {
dataSource.saveReminder(
@@ -65,9 +73,10 @@ class SaveReminderViewModel(val app: Application, val dataSource: ReminderDataSo
}
/**
- * Validate the entered data and show error to the user if there's any invalid data
+ * Validate the entered data and show error to the user if there's any invalid data.
*/
fun validateEnteredData(reminderData: ReminderDataItem): Boolean {
+ Log.d(TAG, "SaveReminderViewModel.validateEnteredData().")
if (reminderData.title.isNullOrEmpty()) {
showSnackBarInt.value = R.string.err_enter_title
return false
@@ -77,6 +86,21 @@ class SaveReminderViewModel(val app: Application, val dataSource: ReminderDataSo
showSnackBarInt.value = R.string.err_select_location
return false
}
+
+ if (
+ (reminderData.latitude.toString().isEmpty()) ||
+ (reminderData.longitude.toString().isEmpty())
+ ) {
+ showSnackBarInt.value = R.string.err_select_latitude_longitude
+ return false
+ }
+
return true
}
+
+ fun onSaveLocation(poi: PointOfInterest?) {
+ Log.d(TAG, "SaveReminderViewModel.onSaveLocation().")
+ selectedPOI.value = poi
+ navigationCommand.value = NavigationCommand.Back
+ }
}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/selectreminderlocation/SelectLocationFragment.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/selectreminderlocation/SelectLocationFragment.kt
index 3c27e92fa..16311dcf7 100644
--- a/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/selectreminderlocation/SelectLocationFragment.kt
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/selectreminderlocation/SelectLocationFragment.kt
@@ -1,91 +1,476 @@
package com.udacity.project4.locationreminders.savereminder.selectreminderlocation
-
import android.Manifest
-import android.app.Activity
+import android.annotation.SuppressLint
import android.content.Intent
-import android.content.IntentSender
import android.content.pm.PackageManager
import android.content.res.Resources
+import android.location.Geocoder
+import android.os.Build
import android.os.Bundle
+import android.provider.Settings
import android.util.Log
import android.view.*
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
+import androidx.core.view.MenuHost
+import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
-import com.google.android.gms.common.api.ApiException
-import com.google.android.gms.common.api.ResolvableApiException
-import com.google.android.gms.location.*
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.*
-import com.google.android.material.snackbar.Snackbar
+import com.udacity.project4.MyApp
import com.udacity.project4.R
import com.udacity.project4.base.BaseFragment
import com.udacity.project4.base.NavigationCommand
import com.udacity.project4.databinding.FragmentSelectLocationBinding
import com.udacity.project4.locationreminders.savereminder.SaveReminderViewModel
+import com.udacity.project4.utils.GeofencingConstants.GEOFENCE_RADIUS_IN_METERS
+import com.udacity.project4.utils.TAG
+import com.udacity.project4.utils.isGpsEnabled
+import com.udacity.project4.utils.permissionDeniedFeedback
import com.udacity.project4.utils.setDisplayHomeAsUpEnabled
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
+import java.io.IOException
+import java.util.*
+
+class SelectLocationFragment : BaseFragment(), OnMapReadyCallback {
-class SelectLocationFragment : BaseFragment() {
+ //--------------------------------------------------
+ // Attributes
+ //--------------------------------------------------
- //Use Koin to get the view model of the SaveReminder
+ companion object {
+ const val ARGUMENTS = "args"
+ }
+
+ // Use Koin to get the view model of the SaveReminder
override val _viewModel: SaveReminderViewModel by inject()
+ // Use Koin to get the view model of the SelectLocation
+
+ private var map: GoogleMap? = null
+
+ private var currentMarker: Marker? = null
+ private var currentCircleMarker: Circle? = null
+ private var currentPOI: PointOfInterest? = null
+ private var poiLatLng = LatLng(-34.0, 151.0)
+ private var poiLocation = "Sydney"
+
+ private var alertShouldEnableGps: AlertDialog? = null
+
private lateinit var binding: FragmentSelectLocationBinding
+ private lateinit var parent: FragmentActivity
+
+ //--------------------------------------------------
+ // Activity Result Callbacks
+ //--------------------------------------------------
+
+ /**
+ * Source:
+ * https://developer.android.com/training/location/permissions#user-choice-affects-permission-grants
+ */
+
+ private val locationPermissionRequest = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ Log.d(TAG, "SelectLocationFragment.locationPermissionRequest() -> permissions: $permissions")
+ when {
+ permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> {
+ // Precise location access granted.
+ Log.d(TAG, "SelectLocationFragment.locationPermissionRequest() -> Precise location access granted.")
+ enableMyLocation()
+ }
+ permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> {
+ // Only approximate location access granted.
+ Log.d(TAG, "SelectLocationFragment.locationPermissionRequest() -> Only approximate location access granted.")
+ } else -> {
+ // No location access granted.
+ Log.d(TAG, "SelectLocationFragment.locationPermissionRequest() -> No location access granted.")
+ }
+ }
+ }
+
+ /**
+ * Source:
+ * https://www.tothenew.com/blog/android-katha-onactivityresult-is-deprecated-now-what/
+ */
+ private var activityResultLauncher = registerForActivityResult(ActivityResultContracts
+ .StartActivityForResult()) {
+ Log.d(TAG, "SelectLocationFragment.activityResultLauncher.")
+ if (parent.isGpsEnabled()) {
+ enableMyLocation()
+ } else {
+ parent.permissionDeniedFeedback()
+ }
+ }
+
+ //--------------------------------------------------
+ // Lifecycle Methods
+ //--------------------------------------------------
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
- ): View? {
- binding =
- DataBindingUtil.inflate(inflater, R.layout.fragment_select_location, container, false)
+ ): View {
+ val layoutId = R.layout.fragment_select_location
+ binding = DataBindingUtil.inflate(inflater, layoutId, container, false)
binding.viewModel = _viewModel
binding.lifecycleOwner = this
+ init()
+
+ return binding.root
+ }
+
+ private fun init() {
+ parent = requireActivity()
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler)
- setHasOptionsMenu(true)
setDisplayHomeAsUpEnabled(true)
-// TODO: add the map setup implementation
-// TODO: zoom to the user location after taking his permission
-// TODO: add style to the map
-// TODO: put a marker to location that the user selected
+ startMapFeature()
+ initMenus()
+ binding.saveButton.setOnClickListener {
+ onLocationSelected()
+ }
+ }
-// TODO: call this function after the user confirms on the selected location
- onLocationSelected()
+ override fun onDestroy() {
+ super.onDestroy()
+ Log.d(TAG, "SelectLocationFragment.onDestroy().")
+ disableDialogs()
+ }
- return binding.root
+ private fun disableDialogs() {
+ alertShouldEnableGps?.let {
+ if (it.isShowing) {
+ it.cancel()
+ }
+ }
}
- private fun onLocationSelected() {
- // TODO: When the user confirms on the selected location,
- // send back the selected location details to the view model
- // and navigate back to the previous fragment to save the reminder and add the geofence
+ //--------------------------------------------------
+ // Menu Methods
+ //--------------------------------------------------
+
+ private fun initMenus() {
+ val menuHost: MenuHost = requireActivity()
+ addMenu(menuHost)
+ }
+
+ @Suppress("UNUSED_EXPRESSION")
+ private fun addMenu(menuHost: MenuHost) {
+ // Add menu items without using the Fragment Menu APIs. Note how we can tie the
+ // MenuProvider to the viewLifecycleOwner and an optional Lifecycle.State (here, RESUMED)
+ // to indicate when the menu should be visible.
+ menuHost.addMenuProvider(object : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.map_options, menu)
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ when (menuItem.itemId) {
+ R.id.normal_map -> {
+ map?.mapType = GoogleMap.MAP_TYPE_NORMAL
+ true
+ }
+ R.id.hybrid_map -> {
+ map?.mapType = GoogleMap.MAP_TYPE_HYBRID
+ true
+ }
+ R.id.satellite_map -> {
+ map?.mapType = GoogleMap.MAP_TYPE_SATELLITE
+ true
+ }
+ R.id.terrain_map -> {
+ map?.mapType = GoogleMap.MAP_TYPE_TERRAIN
+ true
+ }
+ android.R.id.home -> {
+ requireActivity().onBackPressedDispatcher.onBackPressed()
+ true
+ }
+ }
+ return true
+ }
+ }, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ }
+
+ private val backPressHandler = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ _viewModel.navigationCommand.postValue(
+ NavigationCommand.Back
+ )
+ }
+ }
+
+ //--------------------------------------------------
+ // Maps Methods
+ //--------------------------------------------------
+
+ private fun startMapFeature() {
+ Log.d(TAG, "SelectLocationFragment.startMapFeature().")
+ val mapFragment = childFragmentManager
+ .findFragmentById(R.id.map_fragment) as SupportMapFragment
+ mapFragment.getMapAsync(this)
}
+ /**
+ * Manipulates the map once available. This callback is triggered when the map is ready to
+ * be used. This is where we can add markers or lines, add listeners or move the camera.
+ * In this case, we just add a marker near Sydney, Australia.
+ * If Google Play services is not installed on the device, the user will be prompted to install
+ * it inside the SupportMapFragment. This method will only be triggered once the user has
+ * installed Google Play services and returned to the app.
+ */
+ override fun onMapReady(googleMap: GoogleMap) {
+ val app = requireActivity().application as MyApp
+ Log.d(TAG, "----- SelectLocationFragment.onMapReady() -> testingMode = ${app.testingMode}")
+ map = googleMap
+
+ // Add a marker in Sydney and move the camera
+ val sydney = LatLng(-34.0, 151.0)
+ map?.let {
+ it.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
+ it.moveCamera(CameraUpdateFactory.newLatLng(sydney))
+ setMapClick(it)
+
+ if (!app.testingMode) {
+// if (_viewModel.testing.value == false) {
+ // Put a marker to location that the user selected.
+ setPoiClick(it)
+ // Add style to the map.
+ setMapStyle(it)
+ enableMyLocation()
+ }
+ }
+ }
+
+ private fun setMapClick(map: GoogleMap?) {
+ val app = requireActivity().application as MyApp
+ Log.d(TAG, "----- SelectLocationFragment.setMapClick() -> testingMode = ${app.testingMode}")
+ map?.setOnMapClickListener { latLng ->
+// lifecycleScope.launch(Dispatchers.IO) {
+ if (app.testingMode) {
+ Log.d(TAG, "SelectLocationFragment.setMapClick() -> _viewModel.testing.value == true")
+ currentPOI = PointOfInterest(poiLatLng, "", poiLocation)
+ onLocationSelected()
+ } else {
+ Log.d(TAG, "SelectLocationFragment.setMapClick() -> _viewModel.testing.value == false")
+ val geocoder = Geocoder(requireContext(), Locale.getDefault())
+ fetchGeocode(geocoder, latLng)
+ }
+// }
+ }
+ }
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.map_options, menu)
+ @Suppress("DEPRECATION")
+ private fun fetchGeocode(geocoder: Geocoder, latLng: LatLng) {
+ val app = requireActivity().application as MyApp
+ Log.d(TAG, "----- SelectLocationFragment.fetchGeocode() -> testingMode = ${app.testingMode}")
+ lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Log.d(TAG, "SelectLocationFragment.fetchGeocode() -> Version >= Android 13.")
+ geocoder.getFromLocation(latLng.latitude, latLng.longitude, 1) { addresses ->
+ val poi = PointOfInterest(latLng, "", addresses[0].featureName)
+ updateCurrentPoi(poi)
+ }
+ } else {
+ Log.d(TAG, "SelectLocationFragment.fetchGeocode() -> Version < Android 13.")
+ val addresses = geocoder.getFromLocation(latLng.latitude, latLng.longitude, 1)
+ addresses?.let {
+ val poi = PointOfInterest(latLng, "", it[0].featureName)
+ updateCurrentPoi(poi)
+ }
+ }
+ } catch (e: IOException) {
+ Log.d(TAG, "SelectLocationFragment.fetchGeocode() -> Failed to fetch address list.")
+ Log.e(TAG, "Failed to fetch address list: ${e.message}")
+ }
+ }
}
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- // TODO: Change the map type based on the user's selection.
- R.id.normal_map -> {
- true
+ private fun putMarkerOnMap(poi: PointOfInterest) {
+ Log.d(TAG, "SelectLocationFragment.putMarkerOnMap().")
+ try {
+ poiLatLng = poi.latLng
+ poiLocation = poi.name
+ currentMarker = map?.addMarker(MarkerOptions().position(poi.latLng).title(poi.name))
+ currentMarker?.showInfoWindow()
+ } catch (e: Exception) {
+ Log.d(TAG, "SelectLocationFragment.putMarkerOnMap() -> Failed to put Marker on map.")
+ Log.e(TAG, "Failed to put Marker on map: ${e.message}")
}
- R.id.hybrid_map -> {
- true
+ }
+
+ private fun putCircleOnMap(poi: PointOfInterest) {
+ Log.d(TAG, "SelectLocationFragment.putCircleOnMap().")
+ try {
+ // Add circle range.
+ currentCircleMarker = map?.addCircle(CircleOptions()
+ .center(poi.latLng)
+ .radius(GEOFENCE_RADIUS_IN_METERS.toDouble())
+ .fillColor(
+ ContextCompat.getColor(
+ requireContext(),
+ R.color.geofencing_circle_fill_color
+ )
+ )
+ .strokeColor(
+ ContextCompat.getColor(
+ requireContext(),
+ R.color.geofencing_circle_stroke_color
+ )
+ )
+ )
+ } catch (e: Exception) {
+ Log.d(TAG, "SelectLocationFragment.putCircleOnMap() -> Failed to put Circle on map.")
+ Log.e(TAG, "Failed to put Circle on map: ${e.message}")
}
- R.id.satellite_map -> {
- true
+ }
+
+ private fun removeMarkers() {
+ Log.d(TAG, "SelectLocationFragment.removeMarkers().")
+ try {
+ currentMarker?.remove()
+ currentCircleMarker?.remove()
+ } catch (e: Exception) {
+ Log.d(TAG, "SelectLocationFragment.removeMarkers() -> Failed to remove Marker and Circle Marker on map.")
+ Log.e(TAG, "Failed to remove Marker and Circle Marker on map: ${e.message}")
}
- R.id.terrain_map -> {
- true
+ }
+
+ private fun updateCurrentPoi(poi: PointOfInterest) {
+ Log.d(TAG, "SelectLocationFragment.updateCurrentPoi().")
+ lifecycleScope.launch(Dispatchers.Main) {
+ binding.saveButton.visibility = View.VISIBLE
+ removeMarkers()
+ currentPOI = poi
+ putMarkerOnMap(poi)
+ putCircleOnMap(poi)
}
- else -> super.onOptionsItemSelected(item)
}
+ private fun setPoiClick(map: GoogleMap?) {
+ Log.d(TAG, "SelectLocationFragment.setPoiClick().")
+ map?.setOnPoiClickListener { poi ->
+ updateCurrentPoi(poi)
+ }
+ }
-}
+ private fun setMapStyle(map: GoogleMap) {
+ Log.d(TAG, "SelectLocationFragment.setMapStyle().")
+ try {
+ // Customize the styling of the base map using a JSON object defined in a raw res file.
+ val success = map.setMapStyle(
+ MapStyleOptions.loadRawResourceStyle(
+ requireActivity(),
+ R.raw.map_style
+ )
+ )
+ if (!success) {
+ Log.e(TAG, "Style parsing failed.")
+ }
+ } catch (e: Resources.NotFoundException) {
+ Log.e(TAG, "Can't find style. Error: ", e)
+ }
+ }
+
+ //--------------------------------------------------
+ // Location Methods
+ //--------------------------------------------------
+
+ @SuppressLint("MissingPermission")
+ private fun enableMyLocation() {
+ Log.d(TAG, "SelectLocationFragment.enableMyLocation().")
+ if (isPermissionGranted()) {
+ Log.d(TAG, "SelectLocationFragment.enableMyLocation() -> isPermissionGranted? true")
+ map?.isMyLocationEnabled = true
+ checkGpsEnabled()
+ } else {
+ Log.d(TAG, "SelectLocationFragment.enableMyLocation() -> isPermissionGranted? false")
+ locationPermissionRequest.launch(arrayOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION)
+ )
+ }
+ }
+ private fun isPermissionGranted(): Boolean {
+ return ContextCompat.checkSelfPermission(
+ requireActivity(),
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun checkGpsEnabled() {
+ Log.d(TAG, "SelectLocationFragment.checkGpsEnabled().")
+ if (!parent.isGpsEnabled()) {
+ Log.d(TAG, "SelectLocationFragment.checkGpsEnabled() -> [1]")
+ buildAlertMessageNoGps()
+ }
+ }
+
+ /**
+ * Source:
+ * https://stackoverflow.com/a/25175756/1354788
+ */
+ private fun buildAlertMessageNoGps() {
+ Log.d(TAG, "SelectLocationFragment.buildAlertMessageNoGps().")
+ val builder = AlertDialog.Builder(parent)
+ builder.setMessage(R.string.should_enable_gps)
+ .setCancelable(false)
+ .setPositiveButton("Yes") { _, _ ->
+ activityResultLauncher.launch(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
+ }
+ .setNegativeButton("No") { dialog, _ ->
+ dialog.cancel()
+ parent.permissionDeniedFeedback()
+ }
+ alertShouldEnableGps = builder.create()
+ alertShouldEnableGps?.show()
+ }
+
+ /**
+ * When the user confirms on the selected location, send back the selected location details
+ * to the view model and navigate back to the previous fragment to save the reminder and add
+ * the geofence.
+ */
+ private fun onLocationSelected() {
+ Log.d(TAG, "SelectLocationFragment.onLocationSelected().")
+ currentPOI?.let {
+ Log.d(TAG, "SelectLocationFragment.onLocationSelected() -> " +
+ "location: ${it.name}, latitude: $${it.latLng.latitude}, longitude: ${it.latLng.longitude}")
+ }
+ _viewModel.onSaveLocation(currentPOI)
+
+// parent.toast(R.string.poi_selected)
+// lifecycleScope.launch {
+// delay(1000)
+ /*
+ val triple = Triple(location, latitude, longitude)
+ setNavigationResult(triple, ARGUMENTS)
+ findNavController().popBackStack()
+ */
+// }
+
+ /*
+ // Use the navigationCommand live data to navigate between the fragments
+ _viewModel.navigationCommand.postValue(
+ NavigationCommand.To(
+ SelectLocationFragmentDirections.actionSelectLocationFragmentToSaveReminderFragment(
+ )
+ )
+ )
+ */
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/selectreminderlocation/SelectLocationViewModel.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/selectreminderlocation/SelectLocationViewModel.kt
new file mode 100644
index 000000000..bbf4a003f
--- /dev/null
+++ b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/selectreminderlocation/SelectLocationViewModel.kt
@@ -0,0 +1,20 @@
+package com.udacity.project4.locationreminders.savereminder.selectreminderlocation
+
+import android.app.Application
+import androidx.lifecycle.MutableLiveData
+import com.udacity.project4.base.BaseViewModel
+
+class SelectLocationViewModel(
+ val app: Application
+): BaseViewModel(app) {
+
+ val testing = MutableLiveData()
+
+ init {
+ testing.postValue(false)
+ }
+
+ fun enableTestingMode() {
+ testing.postValue(true)
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/utils/BindingAdapters.kt b/starter/app/src/main/java/com/udacity/project4/utils/BindingAdapters.kt
index e6cc157dc..7fbf97511 100644
--- a/starter/app/src/main/java/com/udacity/project4/utils/BindingAdapters.kt
+++ b/starter/app/src/main/java/com/udacity/project4/utils/BindingAdapters.kt
@@ -6,7 +6,6 @@ import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
import com.udacity.project4.base.BaseRecyclerViewAdapter
-
object BindingAdapters {
/**
diff --git a/starter/app/src/main/java/com/udacity/project4/utils/EspressoIdlingResource.kt b/starter/app/src/main/java/com/udacity/project4/utils/EspressoIdlingResource.kt
new file mode 100644
index 000000000..3262f00d2
--- /dev/null
+++ b/starter/app/src/main/java/com/udacity/project4/utils/EspressoIdlingResource.kt
@@ -0,0 +1,32 @@
+package com.udacity.project4.utils
+
+import androidx.test.espresso.idling.CountingIdlingResource
+
+object EspressoIdlingResource {
+
+ private const val RESOURCE = "GLOBAL"
+
+ @JvmField
+ val countingIdlingResource = CountingIdlingResource(RESOURCE)
+
+ fun increment() {
+ countingIdlingResource.increment()
+ }
+
+ fun decrement() {
+ if (!countingIdlingResource.isIdleNow) {
+ countingIdlingResource.decrement()
+ }
+ }
+}
+
+inline fun wrapEspressoIdlingResource(function: () -> T): T {
+ // Espresso does not work well with coroutines yet.
+ // See // https://github.com/Kotlin/kotlinx.coroutines/issues/982
+ EspressoIdlingResource.increment() // Set app as busy.
+ return try {
+ function()
+ } finally {
+ EspressoIdlingResource.decrement() // Set app as idle.
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/utils/Extensions.kt b/starter/app/src/main/java/com/udacity/project4/utils/Extensions.kt
index 6e03d1f0a..546762558 100644
--- a/starter/app/src/main/java/com/udacity/project4/utils/Extensions.kt
+++ b/starter/app/src/main/java/com/udacity/project4/utils/Extensions.kt
@@ -3,25 +3,28 @@ package com.udacity.project4.utils
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
-import android.net.ConnectivityManager
+import android.content.pm.PackageManager
+import android.location.LocationManager
import android.util.Log
import android.view.View
+import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
-import androidx.recyclerview.widget.GridLayoutManager
+import androidx.fragment.app.FragmentActivity
+import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.gms.maps.model.LatLng
+import com.udacity.project4.R
import com.udacity.project4.base.BaseRecyclerViewAdapter
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import java.io.IOException
-import java.net.HttpURLConnection
-import java.net.URL
+import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem
+const val TAG = "Project4"
/**
- * Extension function to setup the RecyclerView
+ * Extension function to setup the RecyclerView.
*/
fun RecyclerView.setup(
adapter: BaseRecyclerViewAdapter
@@ -46,7 +49,9 @@ fun Fragment.setDisplayHomeAsUpEnabled(bool: Boolean) {
}
}
-//animate changing the view visibility
+/**
+ * Animate changing the view visibility.
+ */
fun View.fadeIn() {
this.visibility = View.VISIBLE
this.alpha = 0f
@@ -57,7 +62,9 @@ fun View.fadeIn() {
})
}
-//animate changing the view visibility
+/**
+ * Animate changing the view visibility.
+ */
fun View.fadeOut() {
this.animate().alpha(0f).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
@@ -66,3 +73,65 @@ fun View.fadeOut() {
}
})
}
+
+fun Context.toast(textId: Int) {
+ Toast.makeText(this, textId, Toast.LENGTH_LONG).show()
+}
+
+fun Context.toast(text: String) {
+ Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
+}
+
+fun Context.permissionGranted(permission: String): Boolean {
+ return ContextCompat.checkSelfPermission(
+ this, permission
+ ) == PackageManager.PERMISSION_GRANTED
+}
+
+fun FragmentActivity.requestTrackingPermissions(array: Array, requestCode: Int) {
+ ActivityCompat.requestPermissions(
+ this,
+ array,
+ requestCode
+ )
+}
+
+fun FragmentActivity.showRequestPermissionRationale(permission: String): Boolean =
+ ActivityCompat.shouldShowRequestPermissionRationale(
+ this,
+ permission
+ )
+
+/**
+ * Source:
+ * https://stackoverflow.com/a/60757744/1354788
+ */
+fun Fragment.setNavigationResult(result: Any, key: String = "key") {
+ this.findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
+}
+
+fun Fragment.getNavigationResult(key: String = "key") =
+ findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData(key)
+
+fun getFakeReminderItem() : ReminderDataItem {
+ return ReminderDataItem(
+ title = "some title",
+ description = "some description",
+ location = "some location",
+ latitude = -34.0, // Sydney, Australia
+ longitude = 151.0 // Sydney, Australia
+ )
+}
+
+fun FragmentActivity.isGpsEnabled(): Boolean {
+ Log.d(TAG, "Extensions.isGpsEnabled().")
+ val manager = this.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ return manager.isProviderEnabled(LocationManager.GPS_PROVIDER)
+}
+
+fun FragmentActivity.permissionDeniedFeedback() {
+ Log.d(TAG, "Extensions.permissionDeniedFeedback().")
+ this.toast(R.string.allow_all_time_did_not_accepted)
+}
+
+data class LandmarkDataObject(val id: String, val latLong: LatLng)
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/utils/GeofenceUtils.kt b/starter/app/src/main/java/com/udacity/project4/utils/GeofenceUtils.kt
new file mode 100644
index 000000000..fc58e9a0e
--- /dev/null
+++ b/starter/app/src/main/java/com/udacity/project4/utils/GeofenceUtils.kt
@@ -0,0 +1,35 @@
+package com.udacity.project4.utils
+
+import android.content.Context
+import com.google.android.gms.location.GeofenceStatusCodes
+import com.udacity.project4.R
+import java.util.concurrent.TimeUnit
+
+/**
+ * Returns the error string for a geofencing error code.
+ */
+fun errorMessage(context: Context, errorCode: Int): String {
+ val resources = context.resources
+ return when (errorCode) {
+ GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE -> resources.getString(
+ R.string.geofence_not_available
+ )
+ GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES -> resources.getString(
+ R.string.geofence_too_many_geofences
+ )
+ GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS -> resources.getString(
+ R.string.geofence_too_many_pending_intents
+ )
+ else -> resources.getString(R.string.unknown_geofence_error)
+ }
+}
+
+internal object GeofencingConstants {
+ /**
+ * Used to set an expiration time for a geofence. After this amount of time, Location services
+ * stops tracking the geofence. For this sample, geofences expire after one hour.
+ */
+ val GEOFENCE_EXPIRATION_IN_MILLISECONDS: Long = TimeUnit.HOURS.toMillis(1)
+ const val GEOFENCE_RADIUS_IN_METERS = 100f
+ const val EXTRA_GEOFENCE_INDEX = "GEOFENCE_INDEX"
+}
\ No newline at end of file
diff --git a/starter/app/src/main/java/com/udacity/project4/utils/NotificationUtils.kt b/starter/app/src/main/java/com/udacity/project4/utils/NotificationUtils.kt
index 5cfc0102c..c5551ca9f 100644
--- a/starter/app/src/main/java/com/udacity/project4/utils/NotificationUtils.kt
+++ b/starter/app/src/main/java/com/udacity/project4/utils/NotificationUtils.kt
@@ -1,10 +1,14 @@
package com.udacity.project4.utils
+import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
+import android.graphics.BitmapFactory
+import android.graphics.Color
import android.os.Build
+import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.TaskStackBuilder
import com.udacity.project4.BuildConfig
@@ -12,40 +16,67 @@ import com.udacity.project4.R
import com.udacity.project4.locationreminders.ReminderDescriptionActivity
import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem
-private const val NOTIFICATION_CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel"
+private const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel"
-fun sendNotification(context: Context, reminderDataItem: ReminderDataItem) {
- val notificationManager = context
- .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- // We need to create a NotificationChannel associated with our CHANNEL_ID before sending a notification.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
- && notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null
- ) {
- val name = context.getString(R.string.app_name)
- val channel = NotificationChannel(
- NOTIFICATION_CHANNEL_ID,
- name,
- NotificationManager.IMPORTANCE_DEFAULT
- )
- notificationManager.createNotificationChannel(channel)
+/**
+ * We need to create a NotificationChannel associated with our CHANNEL_ID before sending a
+ * notification.
+ */
+@SuppressLint("NewApi")
+fun createChannel(context: Context, notificationManager: NotificationManager) {
+ Log.d(TAG, "NotificationUtils.createChannel().")
+ val sdkAboveOreo = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+ val channelNull = notificationManager.getNotificationChannel(CHANNEL_ID) == null
+ if (sdkAboveOreo && channelNull) {
+ val notificationChannel = NotificationChannel(
+ CHANNEL_ID,
+ context.getString(R.string.channel_name),
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ setShowBadge(false)
+ }
+ notificationChannel.enableLights(true)
+ notificationChannel.lightColor = Color.RED
+ notificationChannel.enableVibration(true)
+ notificationChannel.description = context.getString(R.string.notification_channel_description)
+ notificationManager.createNotificationChannel(notificationChannel)
}
+}
+fun sendNotification(context: Context, reminderDataItem: ReminderDataItem) {
+ Log.d(TAG, "NotificationUtils.sendNotification().")
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ createChannel(context, notificationManager)
val intent = ReminderDescriptionActivity.newIntent(context.applicationContext, reminderDataItem)
- //create a pending intent that opens ReminderDescriptionActivity when the user clicks on the notification
+ // Create a pending intent that opens ReminderDescriptionActivity when the user clicks on the
+ // notification.
+ var intentFlagTypeUpdateCurrent = PendingIntent.FLAG_UPDATE_CURRENT
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ intentFlagTypeUpdateCurrent = PendingIntent.FLAG_IMMUTABLE
+ }
val stackBuilder = TaskStackBuilder.create(context)
.addParentStack(ReminderDescriptionActivity::class.java)
.addNextIntent(intent)
- val notificationPendingIntent = stackBuilder
- .getPendingIntent(getUniqueId(), PendingIntent.FLAG_UPDATE_CURRENT)
+ val notificationPendingIntent = stackBuilder.getPendingIntent(
+ getUniqueId(), intentFlagTypeUpdateCurrent
+ )
+
+ val mapIcon = R.drawable.map
+ val mapImage = BitmapFactory.decodeResource(context.resources, mapIcon)
+ val bigPicStyle = NotificationCompat.BigPictureStyle()
+ .bigPicture(mapImage)
+ .bigLargeIcon(null)
-// build the notification object with the data to be shown
- val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
- .setSmallIcon(R.mipmap.ic_launcher)
+ // Build the notification object with the data to be shown.
+ val notification = NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.map_small)
.setContentTitle(reminderDataItem.title)
.setContentText(reminderDataItem.location)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(notificationPendingIntent)
+ .setStyle(bigPicStyle)
+ .setLargeIcon(mapImage)
.setAutoCancel(true)
.build()
diff --git a/starter/app/src/main/java/com/udacity/project4/utils/SingleLiveEvent.kt b/starter/app/src/main/java/com/udacity/project4/utils/SingleLiveEvent.kt
index c561da259..08dce7fe2 100644
--- a/starter/app/src/main/java/com/udacity/project4/utils/SingleLiveEvent.kt
+++ b/starter/app/src/main/java/com/udacity/project4/utils/SingleLiveEvent.kt
@@ -42,7 +42,6 @@ class SingleLiveEvent : MutableLiveData() {
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer) {
-
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
@@ -70,7 +69,6 @@ class SingleLiveEvent : MutableLiveData() {
}
companion object {
-
private val TAG = "SingleLiveEvent"
}
}
\ No newline at end of file
diff --git a/starter/app/src/main/res/drawable/android.png b/starter/app/src/main/res/drawable/android.png
new file mode 100644
index 000000000..8d73f092c
Binary files /dev/null and b/starter/app/src/main/res/drawable/android.png differ
diff --git a/starter/app/src/main/res/drawable/ic_no_data.xml b/starter/app/src/main/res/drawable/ic_no_data.xml
index c4f47af01..33fc6d7ab 100644
--- a/starter/app/src/main/res/drawable/ic_no_data.xml
+++ b/starter/app/src/main/res/drawable/ic_no_data.xml
@@ -3,7 +3,7 @@
android:height="80dp"
android:viewportWidth="404.688"
android:viewportHeight="404.688">
-
+
diff --git a/starter/app/src/main/res/drawable/map_small.xml b/starter/app/src/main/res/drawable/map_small.xml
new file mode 100644
index 000000000..3c5dac8b3
--- /dev/null
+++ b/starter/app/src/main/res/drawable/map_small.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/starter/app/src/main/res/layout/activity_authentication.xml b/starter/app/src/main/res/layout/activity_authentication.xml
index 898573e26..4a5973f4a 100644
--- a/starter/app/src/main/res/layout/activity_authentication.xml
+++ b/starter/app/src/main/res/layout/activity_authentication.xml
@@ -1,20 +1,35 @@
-
+ xmlns:tools="http://schemas.android.com/tools">
-
+
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/layout/activity_maps.xml b/starter/app/src/main/res/layout/activity_maps.xml
new file mode 100644
index 000000000..9bc2727ae
--- /dev/null
+++ b/starter/app/src/main/res/layout/activity_maps.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/layout/activity_reminder_description.xml b/starter/app/src/main/res/layout/activity_reminder_description.xml
index 8498b3206..fdedb6e93 100644
--- a/starter/app/src/main/res/layout/activity_reminder_description.xml
+++ b/starter/app/src/main/res/layout/activity_reminder_description.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools">
-
+
@@ -13,21 +13,87 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/layout/activity_reminders.xml b/starter/app/src/main/res/layout/activity_reminders.xml
index 3be22fd67..e09480bc4 100644
--- a/starter/app/src/main/res/layout/activity_reminders.xml
+++ b/starter/app/src/main/res/layout/activity_reminders.xml
@@ -1,17 +1,19 @@
-
+ xmlns:tools="http://schemas.android.com/tools">
-
+
+
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/layout/activity_tracking.xml b/starter/app/src/main/res/layout/activity_tracking.xml
new file mode 100644
index 000000000..ed83acbb7
--- /dev/null
+++ b/starter/app/src/main/res/layout/activity_tracking.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/layout/fragment_login.xml b/starter/app/src/main/res/layout/fragment_login.xml
new file mode 100644
index 000000000..8e90a2da1
--- /dev/null
+++ b/starter/app/src/main/res/layout/fragment_login.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/layout/fragment_reminders.xml b/starter/app/src/main/res/layout/fragment_reminders.xml
index d7005268d..b4290a64b 100644
--- a/starter/app/src/main/res/layout/fragment_reminders.xml
+++ b/starter/app/src/main/res/layout/fragment_reminders.xml
@@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools">
-
@@ -17,9 +16,9 @@
tools:context=".locationreminders.reminderslist.ReminderListFragment">
+ android:layout_height="match_parent">
-
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/layout/fragment_save_reminder.xml b/starter/app/src/main/res/layout/fragment_save_reminder.xml
index 10323da92..2502840fb 100644
--- a/starter/app/src/main/res/layout/fragment_save_reminder.xml
+++ b/starter/app/src/main/res/layout/fragment_save_reminder.xml
@@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools">
-
@@ -12,8 +11,7 @@
+ android:layout_height="match_parent">
-
-
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/layout/fragment_select_location.xml b/starter/app/src/main/res/layout/fragment_select_location.xml
index c936ecc54..1aba0cb4c 100644
--- a/starter/app/src/main/res/layout/fragment_select_location.xml
+++ b/starter/app/src/main/res/layout/fragment_select_location.xml
@@ -11,10 +11,28 @@
-
+
+
diff --git a/starter/app/src/main/res/layout/it_reminder.xml b/starter/app/src/main/res/layout/it_reminder.xml
index 30b0d223c..5111bf390 100644
--- a/starter/app/src/main/res/layout/it_reminder.xml
+++ b/starter/app/src/main/res/layout/it_reminder.xml
@@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools">
-
@@ -27,7 +26,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_small"
- android:layout_marginLeft="@dimen/padding_small"
android:text="@{item.title}"
android:textColor="@color/black"
android:textSize="@dimen/text_size_normal"
@@ -56,7 +54,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/title"
tools:text="Location" />
-
\ No newline at end of file
diff --git a/starter/app/src/main/res/navigation/nav_graph.xml b/starter/app/src/main/res/navigation/nav_graph.xml
index 1cdabb3c4..c17cf8fbc 100644
--- a/starter/app/src/main/res/navigation/nav_graph.xml
+++ b/starter/app/src/main/res/navigation/nav_graph.xml
@@ -1,25 +1,25 @@
+ android:label="Reminder List Fragment"
+ tools:layout="@layout/fragment_reminders">
-
+
+ android:label="Add Reminder"
+ tools:layout="@layout/fragment_save_reminder">
@@ -27,9 +27,14 @@
android:id="@+id/action_saveReminderFragment_to_selectLocationFragment"
app:destination="@id/selectLocationFragment" />
+
-
+ android:label="Select Location"
+ tools:layout="@layout/fragment_select_location">
+
+
\ No newline at end of file
diff --git a/starter/app/src/main/res/raw/map_style.json b/starter/app/src/main/res/raw/map_style.json
new file mode 100644
index 000000000..8f89e90f4
--- /dev/null
+++ b/starter/app/src/main/res/raw/map_style.json
@@ -0,0 +1,224 @@
+[
+ {
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#ebe3cd"
+ }
+ ]
+ },
+ {
+ "elementType": "labels.text.fill",
+ "stylers": [
+ {
+ "color": "#523735"
+ }
+ ]
+ },
+ {
+ "elementType": "labels.text.stroke",
+ "stylers": [
+ {
+ "color": "#f5f1e6"
+ }
+ ]
+ },
+ {
+ "featureType": "administrative",
+ "elementType": "geometry.stroke",
+ "stylers": [
+ {
+ "color": "#c9b2a6"
+ }
+ ]
+ },
+ {
+ "featureType": "administrative.land_parcel",
+ "elementType": "geometry.stroke",
+ "stylers": [
+ {
+ "color": "#dcd2be"
+ }
+ ]
+ },
+ {
+ "featureType": "administrative.land_parcel",
+ "elementType": "labels.text.fill",
+ "stylers": [
+ {
+ "color": "#ae9e90"
+ }
+ ]
+ },
+ {
+ "featureType": "landscape.natural",
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#dfd2ae"
+ }
+ ]
+ },
+ {
+ "featureType": "poi",
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#dfd2ae"
+ }
+ ]
+ },
+ {
+ "featureType": "poi",
+ "elementType": "labels.text.fill",
+ "stylers": [
+ {
+ "color": "#93817c"
+ }
+ ]
+ },
+ {
+ "featureType": "poi.park",
+ "elementType": "geometry.fill",
+ "stylers": [
+ {
+ "color": "#a5b076"
+ }
+ ]
+ },
+ {
+ "featureType": "poi.park",
+ "elementType": "labels.text.fill",
+ "stylers": [
+ {
+ "color": "#447530"
+ }
+ ]
+ },
+ {
+ "featureType": "road",
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#f5f1e6"
+ }
+ ]
+ },
+ {
+ "featureType": "road",
+ "elementType": "geometry.fill",
+ "stylers": [
+ {
+ "color": "#813dff"
+ }
+ ]
+ },
+ {
+ "featureType": "road.arterial",
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#fdfcf8"
+ }
+ ]
+ },
+ {
+ "featureType": "road.highway",
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#f8c967"
+ }
+ ]
+ },
+ {
+ "featureType": "road.highway",
+ "elementType": "geometry.stroke",
+ "stylers": [
+ {
+ "color": "#e9bc62"
+ }
+ ]
+ },
+ {
+ "featureType": "road.highway.controlled_access",
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#e98d58"
+ }
+ ]
+ },
+ {
+ "featureType": "road.highway.controlled_access",
+ "elementType": "geometry.stroke",
+ "stylers": [
+ {
+ "color": "#db8555"
+ }
+ ]
+ },
+ {
+ "featureType": "road.local",
+ "elementType": "labels.text.fill",
+ "stylers": [
+ {
+ "color": "#806b63"
+ }
+ ]
+ },
+ {
+ "featureType": "transit.line",
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#dfd2ae"
+ }
+ ]
+ },
+ {
+ "featureType": "transit.line",
+ "elementType": "labels.text.fill",
+ "stylers": [
+ {
+ "color": "#8f7d77"
+ }
+ ]
+ },
+ {
+ "featureType": "transit.line",
+ "elementType": "labels.text.stroke",
+ "stylers": [
+ {
+ "color": "#ebe3cd"
+ }
+ ]
+ },
+ {
+ "featureType": "transit.station",
+ "elementType": "geometry",
+ "stylers": [
+ {
+ "color": "#dfd2ae"
+ }
+ ]
+ },
+ {
+ "featureType": "water",
+ "elementType": "geometry.fill",
+ "stylers": [
+ {
+ "color": "#b9d3c2"
+ }
+ ]
+ },
+ {
+ "featureType": "water",
+ "elementType": "labels.text.fill",
+ "stylers": [
+ {
+ "color": "#92998d"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/starter/app/src/main/res/values/colors.xml b/starter/app/src/main/res/values/colors.xml
index 38956ae7a..68c496cfe 100644
--- a/starter/app/src/main/res/values/colors.xml
+++ b/starter/app/src/main/res/values/colors.xml
@@ -5,4 +5,7 @@
#D81B60
#000000
#ffffff
+
+ #CCFFF2CC
+ #D6B656
diff --git a/starter/app/src/main/res/values/dimens.xml b/starter/app/src/main/res/values/dimens.xml
index 9054cf343..424d9b767 100644
--- a/starter/app/src/main/res/values/dimens.xml
+++ b/starter/app/src/main/res/values/dimens.xml
@@ -9,4 +9,8 @@
10dp
20dp
16dp
+
+ 15dp
+ 10dp
+ 16sp
\ No newline at end of file
diff --git a/starter/app/src/main/res/values/strings.xml b/starter/app/src/main/res/values/strings.xml
index 690a371cf..8d5e5c9ea 100644
--- a/starter/app/src/main/res/values/strings.xml
+++ b/starter/app/src/main/res/values/strings.xml
@@ -7,34 +7,63 @@
Description
Reminder Location
Save
- Reminder Saved !
+ Reminder Saved!
Logout
Welcome to the location reminder app
Login
+
+ Show Map
Map
+
Normal Map
Hybrid Map
Satellite Map
Terrain Map
Lat: %1$.5f, Long: %2$.5f
+
Dropped Pin
- poi
- Location services must be enabled to use the app
- You need to grant location permission in order to select the location.
- Settings
- Please select a point of interest
+ poi
+ Location services must be enabled to use the app
+ You need to grant location permission in order to select the location.
+ Settings
+ Please select a POI (Point Of Interest)
+
+ Google Maps loaded successfully!
+ Location Permission Needed
+ This app needs the Location permission, please accept to use location functionality
+ Your GPS seems to be disabled. Do you want to enable it?
+ Since you didn\'t accept the Location permission, this app won\'t be able to get your current location.
+ POI selected!
+
Geofence Entered
GeofenceStatus
Geofence status notification
Location Reminder
Please select title
Please select location
- Problem in adding geofences.
Welcome
+
Please enter title
Please select location
+ Please select latitude/longitude
+
+ Unknown error: the Geofence service is not available now
+ Clue Geofence added.
+ Problem in adding geofences.
+
+ Notification permission denied!
+ Notification permission granted!
+
+
+
+ Title:
+ Description:
+ Location:
+ Latitude:
+ Longitude:
+
Unknown error: the Geofence service is not available now.
Geofence service is not available now. Go to Settings>Location>Mode and choose High accuracy.
Your app has registered too many geofences.
diff --git a/starter/app/src/main/res/values/styles.xml b/starter/app/src/main/res/values/styles.xml
index 5885930df..dea2276a8 100644
--- a/starter/app/src/main/res/values/styles.xml
+++ b/starter/app/src/main/res/values/styles.xml
@@ -1,5 +1,4 @@
-
-
+
+
+
+
\ No newline at end of file
diff --git a/starter/app/src/test/java/com/udacity/project4/locationreminders/LiveDataTestUtil.kt b/starter/app/src/test/java/com/udacity/project4/locationreminders/LiveDataTestUtil.kt
new file mode 100644
index 000000000..8b7bd30b2
--- /dev/null
+++ b/starter/app/src/test/java/com/udacity/project4/locationreminders/LiveDataTestUtil.kt
@@ -0,0 +1,41 @@
+package com.udacity.project4.locationreminders
+
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+@VisibleForTesting(otherwise = VisibleForTesting.NONE)
+fun LiveData.getOrAwaitValue(
+ time: Long = 2,
+ timeUnit: TimeUnit = TimeUnit.SECONDS,
+ afterObserve: () -> Unit = {}
+): T {
+ var data: T? = null
+ val latch = CountDownLatch(1)
+ val observer = object : Observer {
+ override fun onChanged(o: T) {
+ data = o
+ latch.countDown()
+ this@getOrAwaitValue.removeObserver(this)
+ }
+ }
+ this.observeForever(observer)
+
+ try {
+ afterObserve.invoke()
+
+ // Don't wait indefinitely if the LiveData is not set.
+ if (!latch.await(time, timeUnit)) {
+ throw TimeoutException("LiveData value was never set.")
+ }
+
+ } finally {
+ this.removeObserver(observer)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return data as T
+}
\ No newline at end of file
diff --git a/starter/app/src/test/java/com/udacity/project4/locationreminders/MainCoroutineRule.kt b/starter/app/src/test/java/com/udacity/project4/locationreminders/MainCoroutineRule.kt
new file mode 100644
index 000000000..b2789e4fd
--- /dev/null
+++ b/starter/app/src/test/java/com/udacity/project4/locationreminders/MainCoroutineRule.kt
@@ -0,0 +1,27 @@
+package com.udacity.project4.locationreminders
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.*
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Source:
+ * https://stackoverflow.com/a/73300292
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class MainCoroutineRule(
+ val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
+) : TestWatcher() {
+
+ override fun starting(description: Description) {
+ super.starting(description)
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ override fun finished(description: Description) {
+ super.finished(description)
+ Dispatchers.resetMain()
+ }
+}
\ No newline at end of file
diff --git a/starter/app/src/test/java/com/udacity/project4/locationreminders/data/FakeDataSource.kt b/starter/app/src/test/java/com/udacity/project4/locationreminders/data/FakeDataSource.kt
index 65ff28e67..3fcaee1f6 100644
--- a/starter/app/src/test/java/com/udacity/project4/locationreminders/data/FakeDataSource.kt
+++ b/starter/app/src/test/java/com/udacity/project4/locationreminders/data/FakeDataSource.kt
@@ -2,27 +2,56 @@ package com.udacity.project4.locationreminders.data
import com.udacity.project4.locationreminders.data.dto.ReminderDTO
import com.udacity.project4.locationreminders.data.dto.Result
+import org.jetbrains.annotations.NotNull
-//Use FakeDataSource that acts as a test double to the LocalDataSource
-class FakeDataSource : ReminderDataSource {
+/**
+ * Use FakeDataSource that acts as a test double to the LocalDataSource
+ */
+class FakeDataSource(@NotNull private var reminderList: MutableList? = mutableListOf())
+ : ReminderDataSource {
-// TODO: Create a fake data source to act as a double to the real data source
+ private var shouldReturnError = false
+
+ /**
+ * Create a fake data source to act as a double to the real data source.
+ */
+ fun returnError(value: Boolean){
+ shouldReturnError = value
+ }
override suspend fun getReminders(): Result> {
- TODO("Return the reminders")
+ // Confirm the correct behavior when the reminders list for some reason can't be loaded
+ return if (shouldReturnError) {
+ Result.Error("There was an exception thrown while retrieving the data. " +
+ "This way, the reminders were unable to get retrieved.")
+ } else {
+ Result.Success(ArrayList(reminderList))
+ }
}
override suspend fun saveReminder(reminder: ReminderDTO) {
- TODO("save the reminder")
+ reminderList?.add(reminder)
}
override suspend fun getReminder(id: String): Result {
- TODO("return the reminder with the id")
+ // Confirm the correct behavior when the reminders list for some reason can't be loaded.
+ if (shouldReturnError) {
+ Result.Error("There was an exception thrown while retrieving the data. " +
+ "This way, the reminders were unable to get retrieved.")
+ }
+ // Return the reminder with the id
+ val reminder = reminderList?.find {
+ it.id == id
+ }
+ return if (reminder != null) {
+ Result.Success(reminder)
+ } else {
+ Result.Error("No Reminders found")
+ }
}
override suspend fun deleteAllReminders() {
- TODO("delete all the reminders")
+ // Delete all the reminders
+ reminderList?.clear()
}
-
-
}
\ No newline at end of file
diff --git a/starter/app/src/test/java/com/udacity/project4/locationreminders/reminderslist/RemindersListViewModelTest.kt b/starter/app/src/test/java/com/udacity/project4/locationreminders/reminderslist/RemindersListViewModelTest.kt
index 0bc84b5a9..bb7bc7ea9 100644
--- a/starter/app/src/test/java/com/udacity/project4/locationreminders/reminderslist/RemindersListViewModelTest.kt
+++ b/starter/app/src/test/java/com/udacity/project4/locationreminders/reminderslist/RemindersListViewModelTest.kt
@@ -1,13 +1,165 @@
package com.udacity.project4.locationreminders.reminderslist
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.udacity.project4.locationreminders.MainCoroutineRule
+import com.udacity.project4.locationreminders.data.FakeDataSource
+import com.udacity.project4.locationreminders.data.dto.ReminderDTO
+import com.udacity.project4.locationreminders.getOrAwaitValue
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.setMain
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.core.Is.`is`
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
import org.junit.runner.RunWith
+import org.koin.core.context.stopKoin
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class RemindersListViewModelTest {
- //TODO: provide testing to the RemindersListViewModel and its live data objects
+ // Executes each task synchronously using Architecture Components.
+ @get:Rule
+ var instantExecutorRule = InstantTaskExecutorRule()
+ // Set the main coroutines dispatcher for unit testing.
+ @ExperimentalCoroutinesApi
+ @get:Rule
+ var mainCoroutineRule = MainCoroutineRule()
+
+ // Subject under test
+ private lateinit var remindersListViewModel: RemindersListViewModel
+
+ // Use a fake repository to be injected into the view model.
+ private lateinit var fakeDataSource: FakeDataSource
+
+ private val item1 = ReminderDTO(
+ "Reminder1",
+ "Description1",
+ "Location1",
+ 1.0,
+ 1.0,
+ "1"
+ )
+ private val item2 = ReminderDTO(
+ "Reminder2",
+ "Description2",
+ "location2",
+ 2.0,
+ 2.0,
+ "2"
+ )
+ private val item3 = ReminderDTO(
+ "Reminder3",
+ "Description3",
+ "location3",
+ 3.0,
+ 3.0,
+ "3"
+ )
+
+ @Before
+ fun setupRemindersListViewModel() {
+ stopKoin()
+ fakeDataSource = FakeDataSource()
+ remindersListViewModel = RemindersListViewModel(
+ ApplicationProvider.getApplicationContext(),
+ fakeDataSource
+ )
+ }
+
+ @After
+ fun clearData() = runBlocking {
+ fakeDataSource.deleteAllReminders()
+ }
+
+ /**
+ * This function tries to load the Reminders from our ViewModel after removing all Reminders.
+ */
+ @Test
+ fun invalidateShowNoDataShowNoDataIsTrue() = runBlocking {
+ // Delete all current Reminders.
+ fakeDataSource.deleteAllReminders()
+
+ // Try to load them.
+ remindersListViewModel.loadReminders()
+
+ // Expect that our remindersList LiveData size is 0 and showNoData is true.
+ assertThat(remindersListViewModel.remindersList.getOrAwaitValue().size, `is` (0))
+ assertThat(remindersListViewModel.showNoData.getOrAwaitValue(), `is` (true))
+ }
+
+ /**
+ * Test retrieving the 3 reminders added to our Data Source.
+ */
+ @Test
+ fun loadRemindersLoadsThreeReminders()= runBlocking {
+ // Add 3 Reminders in our Data Source.
+ fakeDataSource.deleteAllReminders()
+ fakeDataSource.saveReminder(item1)
+ fakeDataSource.saveReminder(item2)
+ fakeDataSource.saveReminder(item3)
+
+ // Try to load the Reminders.
+ remindersListViewModel.loadReminders()
+
+ // Expect to have only 3 reminders in remindersList,
+ // and showNoData is false because we have data.
+ assertThat(remindersListViewModel.remindersList.getOrAwaitValue().size, `is` (3))
+ assertThat(remindersListViewModel.showNoData.getOrAwaitValue(), `is` (false))
+ }
+
+ /**
+ * Here, we are testing checkLoading in this test.
+ */
+ @Test
+ fun loadRemindersCheckLoading() = runBlocking {
+ // Set Main dispatcher to not run coroutines eagerly, for just this one test.
+ val dispatcher = StandardTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+
+ // Stop dispatcher so we may inspect initial values.
+// mainCoroutineRule.pauseDispatcher() // <--- deprecated
+
+ // Only 1 Reminder.
+ fakeDataSource.deleteAllReminders()
+ fakeDataSource.saveReminder(item1)
+
+ // Load Reminders.
+ remindersListViewModel.loadReminders()
+
+ // The loading indicator is displayed, then it is hidden after we are done.
+ assertThat(remindersListViewModel.showLoading.getOrAwaitValue(), `is`(true))
+
+ // Execute pending coroutines actions.
+// mainCoroutineRule.resumeDispatcher() // <--- deprecated
+ dispatcher.scheduler.advanceUntilIdle() // https://kt.academy/article/cc-testing
+
+ // Then, loading indicator is hidden.
+ assertThat(remindersListViewModel.showLoading.getOrAwaitValue(), `is`(false))
+ }
+
+ /**
+ * Testing showing an Error.
+ */
+ @Test
+ fun loadRemindersShouldReturnError() = runBlocking {
+ // GIVEN - Set returnError to true.
+ fakeDataSource.returnError(true)
+ // WHEN - We load the Reminders.
+ remindersListViewModel.loadReminders()
+ // THEN - We get showSnackBar MutableLiveData in the ViewModel returning us "not found".
+ assertThat(remindersListViewModel.showSnackBar.getOrAwaitValue(),
+ `is`(
+ "There was an exception thrown while retrieving the data. " +
+ "This way, the reminders were unable to get retrieved.")
+ )
+ }
}
\ No newline at end of file
diff --git a/starter/app/src/test/java/com/udacity/project4/locationreminders/savereminder/SaveReminderViewModelTest.kt b/starter/app/src/test/java/com/udacity/project4/locationreminders/savereminder/SaveReminderViewModelTest.kt
index 99e35d046..6b93abf3c 100644
--- a/starter/app/src/test/java/com/udacity/project4/locationreminders/savereminder/SaveReminderViewModelTest.kt
+++ b/starter/app/src/test/java/com/udacity/project4/locationreminders/savereminder/SaveReminderViewModelTest.kt
@@ -1,16 +1,133 @@
package com.udacity.project4.locationreminders.savereminder
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-
+import com.udacity.project4.R
+import com.udacity.project4.locationreminders.MainCoroutineRule
+import com.udacity.project4.locationreminders.data.FakeDataSource
+import com.udacity.project4.locationreminders.data.dto.Result
+import com.udacity.project4.locationreminders.getOrAwaitValue
+import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.setMain
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.core.Is.`is`
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
import org.junit.runner.RunWith
+import org.koin.core.context.GlobalContext.stopKoin
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class SaveReminderViewModelTest {
+ // Executes each task synchronously using Architecture Components.
+ @get:Rule
+ var instantExecutorRule = InstantTaskExecutorRule()
+
+ // Set the main coroutines dispatcher for unit testing.
+ @ExperimentalCoroutinesApi
+ @get:Rule
+ var mainCoroutineRule = MainCoroutineRule()
+
+ // Subject under test
+ private lateinit var saveReminderViewModel: SaveReminderViewModel
+
+ // Use a fake repository to be injected into the view model.
+ private lateinit var fakeDataSource: FakeDataSource
+
+ private val item1 = ReminderDataItem(
+ "Reminder1",
+ "Description1",
+ "Location1",
+ 1.0,
+ 1.0,
+ "1"
+ )
+ private val item2 = ReminderDataItem(
+ "",
+ "Description2",
+ "location2",
+ 2.0,
+ 2.0,
+ "2"
+ )
+ private val item3 = ReminderDataItem(
+ "Reminder3",
+ "Description3",
+ "",
+ 3.0,
+ 3.0,
+ "3"
+ )
+
+ @Before
+ fun setUpViewModel() {
+ stopKoin()
+ fakeDataSource = FakeDataSource()
+ saveReminderViewModel = SaveReminderViewModel(
+ ApplicationProvider.getApplicationContext(),
+ fakeDataSource
+ )
+ }
+
+ @Test
+ fun saveReminderAndCheckItOnDataSource() = runBlocking {
+ // GIVEN - saveReminder for item1.
+ saveReminderViewModel.saveReminder(item1)
+
+ // WHEN - getReminder for item with id 1.
+ val reminderFromDataSource = fakeDataSource.getReminder("1") as Result.Success
+
+ // THEN - Expect to get item1.
+ assertThat(reminderFromDataSource.data.title, `is` (item1.title))
+ assertThat(reminderFromDataSource.data.description, `is` (item1.description))
+ assertThat(reminderFromDataSource.data.location, `is` (item1.location))
+ assertThat(reminderFromDataSource.data.latitude, `is` (item1.latitude))
+ assertThat(reminderFromDataSource.data.longitude, `is` (item1.longitude))
+ assertThat(reminderFromDataSource.data.id, `is` (item1.id))
+ }
+
+ @Test
+ fun saveReminderAndCheckLoadingIndicator() = runBlocking {
+ // GIVEN - saveReminder for item1.
+ // Set Main dispatcher to not run coroutines eagerly, for just this one test.
+ val dispatcher = StandardTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+ saveReminderViewModel.saveReminder(item1)
+
+ // WHEN - Loading indicator is shown.
+ assertThat(saveReminderViewModel.showLoading.getOrAwaitValue(), `is`(true))
+ dispatcher.scheduler.advanceUntilIdle()
+
+ // THEN - Loading indicator is hidden.
+ assertThat(saveReminderViewModel.showLoading.getOrAwaitValue(), `is`(false))
+ }
+
+ @Test
+ fun validateEnteredData_missingTitle_showSnackAndReturnFalse() {
+ // GIVEN - validateEnteredData and WHEN - passing no title.
+ val validation = saveReminderViewModel.validateEnteredData(item2)
- //TODO: provide testing to the SaveReminderView and its live data objects
+ // THEN - Expect a SnackBar to display err_enter_title string.
+ // THEN - Expect validation to return false.
+ assertThat(saveReminderViewModel.showSnackBarInt.getOrAwaitValue(), `is` (R.string.err_enter_title))
+ assertThat(validation, `is` (false))
+ }
+ @Test
+ fun validateEnteredData_missingLocation_showSnackAndReturnFalse() {
+ // GIVEN - validateEnteredData and WHEN - passing no location.
+ val valid = saveReminderViewModel.validateEnteredData(item3)
+ // THEN - Expect a SnackBar to be shown displaying err_select_location string
+ // THEN - Expect validation return false.
+ assertThat(saveReminderViewModel.showSnackBarInt.getOrAwaitValue(), `is` (R.string.err_select_location))
+ assertThat(valid, `is` (false))
+ }
}
\ No newline at end of file
diff --git a/starter/build.gradle b/starter/build.gradle
index 8b9aab78a..36dd5e005 100644
--- a/starter/build.gradle
+++ b/starter/build.gradle
@@ -1,67 +1,66 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlinVersion = '1.3.72'
- ext.navigationVersion = "2.2.0"
+ ext {
+ gradleVersion = '7.4.2'
+ kotlinVersion = '1.8.0'
+ navigationVersion = '2.5.3'
+
+ // Sdk and tools
+ // Support library and architecture components support minSdk 14 and above.
+ minSdkVersion = 24
+ targetSdkVersion = 33
+
+ // App dependencies
+// androidXVersion = '1.0.0'
+// androidXAnnotations = '1.0.1'
+// androidXLegacySupport = '1.0.0'
+ appCompatVersion = '1.6.1'
+ archLifecycleVersion = '2.2.0'
+ archLifecycleKtxVersion = '2.6.1'
+// cardVersion = '1.0.0'
+ materialVersion = '1.8.0'
+// fragmentVersion = '1.1.0-alpha07'
+// recyclerViewVersion = '1.1.0'
+ mockitoVersion = '5.2.0'
+ constraintVersion = '2.1.4'
+ dexMakerVersion = '2.28.3'
+ coroutinesVersion = '1.6.4'
+ roomVersion = '2.5.1'
+ koinVersion = '3.4.0'
+ truthVersion = '1.1.3'
+ junitVersion = '4.13.2'
+ androidXTestCoreVersion = '1.5.0'
+ robolectricVersion = '4.10.3'
+ androidXTestExtKotlinRunnerVersion = '1.1.5'
+ archTestingVersion = '2.2.0'
+// playServicesVersion = '17.0.0'
+// hamcrestVersion = '1.3'
+ androidXTestRulesVersion = '1.5.0'
+ espressoVersion = '3.5.1'
+ swipeRefreshVersion = '1.1.0'
+ fragmentTestingVersion = '1.5.6'
+ playServicesLocationVersion = '21.0.1'
+ playServicesMapsVersion = '18.1.0'
+ firebaseUiAuthVersion = '8.0.2'
+ firebaseAuthKtxVersion = '21.2.0'
+ workManagerVersion = '2.8.1'
+ gsonVersion = '2.9.0'
+ }
repositories {
google()
- jcenter()
-
+ mavenCentral()
}
+
dependencies {
- classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath "com.android.tools.build:gradle:$gradleVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
- classpath 'com.google.gms:google-services:4.3.3'
+ classpath 'com.google.gms:google-services:4.3.15'
+ classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
-}
-
-allprojects {
- repositories {
- google()
- jcenter()
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
-ext {
- // Sdk and tools
- // Support library and architecture components support minSdk 14 and above.
- minSdkVersion = 16
- targetSdkVersion = 30
- compileSdkVersion = 30
-
- // App dependencies
- androidXVersion = '1.0.0'
- androidXAnnotations = '1.0.1'
- androidXLegacySupport = '1.0.0'
- appCompatVersion = '1.2.0'
- archLifecycleVersion = '2.2.0'
- cardVersion = '1.0.0'
- materialVersion = '1.1.0'
- fragmentVersion = '1.1.0-alpha07'
- recyclerViewVersion = '1.1.0'
- mockitoVersion = '2.8.9'
- constraintVersion = '2.0.0-rc1'
- dexMakerVersion = '2.12.1'
- coroutinesVersion = '1.2.1'
- roomVersion = '2.2.5'
- koinVersion = '2.0.1'
- truthVersion = '0.44'
- junitVersion = '4.12'
- androidXTestCoreVersion = '1.2.0-beta01'
- robolectricVersion = '4.3-beta-1'
- androidXTestExtKotlinRunnerVersion = '1.1.1'
- archTestingVersion = '2.0.0'
- playServicesVersion = '17.0.0'
- hamcrestVersion = '1.3'
- androidXTestRulesVersion = '1.2.0-beta01'
- espressoVersion = '3.2.0'
-
}
\ No newline at end of file
diff --git a/starter/gradle.properties b/starter/gradle.properties
index 23339e0df..1e6465cea 100644
--- a/starter/gradle.properties
+++ b/starter/gradle.properties
@@ -19,3 +19,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
+org.gradle.unsafe.configuration-cache=true
diff --git a/starter/gradle/wrapper/gradle-wrapper.properties b/starter/gradle/wrapper/gradle-wrapper.properties
index 49f80c0a9..0e9dc9772 100644
--- a/starter/gradle/wrapper/gradle-wrapper.properties
+++ b/starter/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
diff --git a/starter/settings.gradle b/starter/settings.gradle
index e4e15d848..9ba9469c0 100644
--- a/starter/settings.gradle
+++ b/starter/settings.gradle
@@ -1,2 +1,16 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "Project4"
include ':app'
-rootProject.name='Project4'