From 99b1b59a6e82d5e71cffd740b96ac5cdec17d793 Mon Sep 17 00:00:00 2001 From: Rodrigo Cericatto Konzen Date: Fri, 14 Apr 2023 17:22:28 -0400 Subject: [PATCH 01/36] Code updated to run on SDK 33 and Gradle 7.5 --- starter/.idea/compiler.xml | 6 ++ starter/.idea/gradle.xml | 19 ++++ starter/.idea/misc.xml | 7 ++ starter/app/build.gradle | 98 ++++++++--------- starter/app/src/main/AndroidManifest.xml | 34 +++--- .../main/java/com/udacity/project4/MyApp.kt | 58 +++++----- .../authentication/AuthenticationActivity.kt | 10 +- .../com/udacity/project4/base/BaseFragment.kt | 5 +- .../project4/base/BaseRecyclerViewAdapter.kt | 6 +- .../udacity/project4/base/BaseViewModel.kt | 2 - .../project4/base/DataBindingViewHolder.kt | 2 +- .../ReminderDescriptionActivity.kt | 15 ++- .../locationreminders/RemindersActivity.kt | 15 ++- .../locationreminders/data/dto/Result.kt | 1 - .../locationreminders/data/local/LocalDB.kt | 4 +- .../data/local/RemindersDao.kt | 4 +- .../data/local/RemindersDatabase.kt | 1 - .../data/local/RemindersLocalRepository.kt | 5 +- .../geofence/GeofenceBroadcastReceiver.kt | 5 +- .../GeofenceTransitionsJobIntentService.kt | 15 ++- .../reminderslist/ReminderListFragment.kt | 38 +++---- .../reminderslist/RemindersListAdapter.kt | 3 +- .../savereminder/SaveReminderFragment.kt | 31 +++--- .../savereminder/SaveReminderViewModel.kt | 4 +- .../SelectLocationFragment.kt | 50 +++------ .../udacity/project4/utils/BindingAdapters.kt | 1 - .../com/udacity/project4/utils/Extensions.kt | 21 ++-- .../udacity/project4/utils/SingleLiveEvent.kt | 2 - .../res/layout/activity_authentication.xml | 1 - .../layout/activity_reminder_description.xml | 6 +- .../main/res/layout/activity_reminders.xml | 18 ++-- .../main/res/layout/fragment_reminders.xml | 3 +- .../res/layout/fragment_save_reminder.xml | 4 +- .../res/layout/fragment_select_location.xml | 8 +- .../app/src/main/res/layout/it_reminder.xml | 2 - .../app/src/main/res/navigation/nav_graph.xml | 17 +-- starter/app/src/main/res/values/styles.xml | 4 +- starter/build.gradle | 101 +++++++++--------- .../gradle/wrapper/gradle-wrapper.properties | 2 +- starter/settings.gradle | 16 ++- 40 files changed, 314 insertions(+), 330 deletions(-) create mode 100644 starter/.idea/compiler.xml create mode 100644 starter/.idea/gradle.xml create mode 100644 starter/.idea/misc.xml diff --git a/starter/.idea/compiler.xml b/starter/.idea/compiler.xml new file mode 100644 index 000000000..fb7f4a8a4 --- /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..235dd5cef --- /dev/null +++ b/starter/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/starter/.idea/misc.xml b/starter/.idea/misc.xml new file mode 100644 index 000000000..cb1a63385 --- /dev/null +++ b/starter/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/starter/app/build.gradle b/starter/app/build.gradle index 73b7fbf4e..9d2a37bda 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 { @@ -31,10 +34,10 @@ android { returnDefaultValues = true } - //dataBinding { - // enabled = true - // enabledForTests = true - //} +// dataBinding { +// enabled = true +// enabledForTests = true +// } buildFeatures { dataBinding true } @@ -42,47 +45,43 @@ 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:2.8.5' - // 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" - //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" // 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" @@ -104,23 +103,26 @@ 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' } + // 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/src/main/AndroidManifest.xml b/starter/app/src/main/AndroidManifest.xml index db5d2ec2a..99662c945 100644 --- a/starter/app/src/main/AndroidManifest.xml +++ b/starter/app/src/main/AndroidManifest.xml @@ -1,19 +1,27 @@ - + + + - - + - + + + + + - + + + + - - - + + - - + - \ 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..f6378c09b 100644 --- a/starter/app/src/main/java/com/udacity/project4/MyApp.kt +++ b/starter/app/src/main/java/com/udacity/project4/MyApp.kt @@ -13,35 +13,35 @@ import org.koin.dsl.module class MyApp : Application() { - 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/authentication/AuthenticationActivity.kt b/starter/app/src/main/java/com/udacity/project4/authentication/AuthenticationActivity.kt index cdf405c6b..3d660c314 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 @@ -13,12 +13,12 @@ class AuthenticationActivity : AppCompatActivity() { 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 + // TODO: Implement the create account and sign in using FirebaseUI, + // use sign in using email and sign in using Google -// TODO: If the user was authenticated, send him to RemindersActivity + // TODO: If the user was authenticated, send him to RemindersActivity -// TODO: a bonus is to customize the sign in flow to look nice using : + // 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 - } -} +} \ 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..c6ea814ad 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 @@ -14,10 +14,12 @@ import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem */ 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 + // Receive the reminder object after the user clicks on the notification fun newIntent(context: Context, reminderDataItem: ReminderDataItem): Intent { val intent = Intent(context, ReminderDescriptionActivity::class.java) intent.putExtra(EXTRA_ReminderDataItem, reminderDataItem) @@ -25,13 +27,10 @@ class ReminderDescriptionActivity : AppCompatActivity() { } } - 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 + val layoutId = R.layout.activity_reminder_description + binding = DataBindingUtil.setContentView(this, layoutId) + // TODO: Add the implementation of the reminder details } -} +} \ 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..7e3647abc 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,31 +1,28 @@ 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 } } 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..1c68efda0 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,12 @@ 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 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 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..cf976c084 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 @@ -13,11 +13,8 @@ import android.content.Intent * 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 - + // TODO: implement the onReceive method to receive the geofencing events at the background } } \ 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 index 5932cd3ca..a1b006520 100644 --- 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 @@ -4,9 +4,9 @@ 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.ReminderDataSource 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.* @@ -22,7 +22,7 @@ class GeofenceTransitionsJobIntentService : JobIntentService(), CoroutineScope { companion object { private const val JOB_ID = 573 - // TODO: call this to start the JobIntentService to handle the geofencing transition events + // TODO: call this to start the JobIntentService to handle the geofencing transition events fun enqueueWork(context: Context, intent: Intent) { enqueueWork( context, @@ -33,12 +33,12 @@ class GeofenceTransitionsJobIntentService : JobIntentService(), CoroutineScope { } 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: 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 + // TODO: get the request id of the current geofence private fun sendNotification(triggeringGeofences: List) { val requestId = "" @@ -64,5 +64,4 @@ class GeofenceTransitionsJobIntentService : JobIntentService(), CoroutineScope { } } } - -} +} \ 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..1feffc61c 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 @@ -13,26 +13,24 @@ import com.udacity.project4.utils.setup import org.koin.androidx.viewmodel.ext.android.viewModel class ReminderListFragment : BaseFragment() { - //use Koin to retrieve the ViewModel instance + + // Use Koin to retrieve the ViewModel instance override val _viewModel: RemindersListViewModel by viewModel() private lateinit var binding: FragmentRemindersBinding + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - binding = - DataBindingUtil.inflate( - inflater, - R.layout.fragment_reminders, container, false - ) + ): View { + binding = DataBindingUtil.inflate(inflater, + R.layout.fragment_reminders, container, false + ) binding.viewModel = _viewModel setHasOptionsMenu(true) setDisplayHomeAsUpEnabled(false) setTitle(getString(R.string.app_name)) - binding.refreshLayout.setOnRefreshListener { _viewModel.loadReminders() } - return binding.root } @@ -47,41 +45,35 @@ class ReminderListFragment : BaseFragment() { override fun onResume() { super.onResume() - //load the reminders list on the ui + // Load the reminders list on the ui _viewModel.loadReminders() } private fun navigateToAddReminder() { - //use the navigationCommand live data to navigate between the fragments + // Use the navigationCommand live data to navigate between the fragments _viewModel.navigationCommand.postValue( - NavigationCommand.To( - ReminderListFragmentDirections.toSaveReminder() - ) + NavigationCommand.To(ReminderListFragmentDirections.toSaveReminder()) ) } private fun setupRecyclerView() { - val adapter = RemindersListAdapter { - } - -// setup the recycler view using the extension function + 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 + // TODO: add the logout implementation } } return super.onOptionsItemSelected(item) - } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) -// display logout as menu item + // Display logout as menu item inflater.inflate(R.menu.main_menu, menu) } - -} +} \ 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/savereminder/SaveReminderFragment.kt b/starter/app/src/main/java/com/udacity/project4/locationreminders/savereminder/SaveReminderFragment.kt index 04e7f182d..db1983590 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 @@ -13,21 +13,19 @@ import com.udacity.project4.utils.setDisplayHomeAsUpEnabled 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 + + // 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 override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = - DataBindingUtil.inflate(inflater, R.layout.fragment_save_reminder, container, false) + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + val layoutId = R.layout.fragment_save_reminder + binding = DataBindingUtil.inflate(inflater, layoutId, container, false) setDisplayHomeAsUpEnabled(true) - binding.viewModel = _viewModel - return binding.root } @@ -35,9 +33,10 @@ class SaveReminderFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = this binding.selectLocation.setOnClickListener { - // Navigate to another fragment to get the user location - _viewModel.navigationCommand.value = - NavigationCommand.To(SaveReminderFragmentDirections.actionSaveReminderFragmentToSelectLocationFragment()) + // Navigate to another fragment to get the user location + val directions = SaveReminderFragmentDirections + .actionSaveReminderFragmentToSelectLocationFragment() + _viewModel.navigationCommand.value = NavigationCommand.To(directions) } binding.saveReminder.setOnClickListener { @@ -47,15 +46,15 @@ class SaveReminderFragment : BaseFragment() { val latitude = _viewModel.latitude val longitude = _viewModel.longitude.value -// TODO: use the user entered reminder details to: -// 1) add a geofencing request -// 2) save the reminder to the local db + // TODO: use the user entered reminder details to: + // 1) add a geofencing request + // 2) save the reminder to the local db } } override fun onDestroy() { super.onDestroy() - //make sure to clear the view model after destroy, as it's a single view model. + // Make sure to clear the view model after destroy, as it's a single view model. _viewModel.onClear() } -} +} \ 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..188fb655c 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 @@ -45,7 +45,7 @@ class SaveReminderViewModel(val app: Application, val dataSource: ReminderDataSo /** * Save the reminder to the data source */ - fun saveReminder(reminderData: ReminderDataItem) { + private fun saveReminder(reminderData: ReminderDataItem) { showLoading.value = true viewModelScope.launch { dataSource.saveReminder( @@ -67,7 +67,7 @@ class SaveReminderViewModel(val app: Application, val dataSource: ReminderDataSo /** * Validate the entered data and show error to the user if there's any invalid data */ - fun validateEnteredData(reminderData: ReminderDataItem): Boolean { + private fun validateEnteredData(reminderData: ReminderDataItem): Boolean { if (reminderData.title.isNullOrEmpty()) { showSnackBarInt.value = R.string.err_enter_title return false 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..35b3e7f9f 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,29 +1,10 @@ package com.udacity.project4.locationreminders.savereminder.selectreminderlocation - -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.content.IntentSender -import android.content.pm.PackageManager -import android.content.res.Resources import android.os.Bundle -import android.util.Log import android.view.* -import androidx.core.content.ContextCompat 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 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.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.setDisplayHomeAsUpEnabled @@ -31,15 +12,15 @@ import org.koin.android.ext.android.inject class SelectLocationFragment : BaseFragment() { - //Use Koin to get the view model of the SaveReminder + // Use Koin to get the view model of the SaveReminder override val _viewModel: SaveReminderViewModel by inject() private lateinit var binding: FragmentSelectLocationBinding 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 @@ -47,25 +28,22 @@ class SelectLocationFragment : BaseFragment() { 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 - + // 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 -// TODO: call this function after the user confirms on the selected location + // TODO: call this function after the user confirms on the selected location onLocationSelected() - return binding.root } 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 + // 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 } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.map_options, menu) } @@ -86,6 +64,4 @@ class SelectLocationFragment : BaseFragment() { } else -> super.onOptionsItemSelected(item) } - - -} +} \ 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/Extensions.kt b/starter/app/src/main/java/com/udacity/project4/utils/Extensions.kt index 6e03d1f0a..dbb5348fe 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 @@ -2,26 +2,15 @@ package com.udacity.project4.utils import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.content.Context -import android.net.ConnectivityManager -import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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 - /** - * Extension function to setup the RecyclerView + * Extension function to setup the RecyclerView. */ fun RecyclerView.setup( adapter: BaseRecyclerViewAdapter @@ -46,7 +35,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 +48,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) { 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/layout/activity_authentication.xml b/starter/app/src/main/res/layout/activity_authentication.xml index 898573e26..0e3948ee0 100644 --- a/starter/app/src/main/res/layout/activity_authentication.xml +++ b/starter/app/src/main/res/layout/activity_authentication.xml @@ -16,5 +16,4 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - \ 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..bb0e6fa18 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"> - + @@ -16,7 +16,8 @@ android:gravity="center" android:orientation="vertical" tools:context=".locationreminders.ReminderDescriptionActivity"> - + + - \ 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/fragment_reminders.xml b/starter/app/src/main/res/layout/fragment_reminders.xml index d7005268d..75f545efd 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"> - @@ -69,4 +68,4 @@ app:layout_constraintEnd_toEndOf="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..18b3d8c61 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"> - @@ -86,6 +85,5 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="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..54b4dba08 100644 --- a/starter/app/src/main/res/layout/fragment_select_location.xml +++ b/starter/app/src/main/res/layout/fragment_select_location.xml @@ -1,10 +1,7 @@ - + - @@ -13,8 +10,7 @@ - - + diff --git a/starter/app/src/main/res/layout/it_reminder.xml b/starter/app/src/main/res/layout/it_reminder.xml index 30b0d223c..72a0e97b6 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"> - @@ -56,7 +55,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..621cf78c3 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="ReminderListFragment" + tools:layout="@layout/fragment_reminders"> - + + android:label="Add Reminder" + tools:layout="@layout/fragment_save_reminder"> @@ -27,9 +27,10 @@ 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/values/styles.xml b/starter/app/src/main/res/values/styles.xml index 5885930df..7fb1e2db4 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/build.gradle b/starter/build.gradle index 8b9aab78a..08bb038d9 100644 --- a/starter/build.gradle +++ b/starter/build.gradle @@ -1,67 +1,64 @@ // 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.3.1' + 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' + } 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/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' From d8ffa9da03163d571a4f5f3d2244ea67355551f2 Mon Sep 17 00:00:00 2001 From: Rodrigo Cericatto Konzen Date: Sat, 15 Apr 2023 21:14:21 -0400 Subject: [PATCH 02/36] Authentication flow implemented --- starter/.idea/gradle.xml | 1 + starter/app/google-services.json | 47 ++++++ .../authentication/FirebaseUserLiveData.kt | 56 +++++++ .../project4/authentication/LoginFragment.kt | 138 ++++++++++++++++++ .../project4/authentication/LoginViewModel.kt | 22 +++ .../reminderslist/ReminderListFragment.kt | 5 +- .../src/main/res/layout/fragment_login.xml | 23 +++ .../app/src/main/res/navigation/nav_graph.xml | 14 +- starter/app/src/main/res/values/dimens.xml | 4 + starter/app/src/main/res/values/strings.xml | 6 + 10 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 starter/app/google-services.json create mode 100644 starter/app/src/main/java/com/udacity/project4/authentication/FirebaseUserLiveData.kt create mode 100644 starter/app/src/main/java/com/udacity/project4/authentication/LoginFragment.kt create mode 100644 starter/app/src/main/java/com/udacity/project4/authentication/LoginViewModel.kt create mode 100644 starter/app/src/main/res/layout/fragment_login.xml diff --git a/starter/.idea/gradle.xml b/starter/.idea/gradle.xml index 235dd5cef..a0de2a152 100644 --- a/starter/.idea/gradle.xml +++ b/starter/.idea/gradle.xml @@ -1,5 +1,6 @@ + \ No newline at end of file diff --git a/starter/app/src/main/AndroidManifest.xml b/starter/app/src/main/AndroidManifest.xml index bceec7e88..a06a20673 100644 --- a/starter/app/src/main/AndroidManifest.xml +++ b/starter/app/src/main/AndroidManifest.xml @@ -42,7 +42,6 @@ - + + + + diff --git a/starter/app/src/main/res/menu/main_menu.xml b/starter/app/src/main/res/menu/main_menu.xml index 4e4f74446..bf724c22a 100644 --- a/starter/app/src/main/res/menu/main_menu.xml +++ b/starter/app/src/main/res/menu/main_menu.xml @@ -5,8 +5,4 @@ android:id="@+id/logout" android:title="@string/logout" app:showAsAction="always" /> - \ No newline at end of file From 2457bc3199b23de1b89950594e90946233e464e1 Mon Sep 17 00:00:00 2001 From: Rodrigo Cericatto Konzen Date: Mon, 17 Apr 2023 16:53:18 -0400 Subject: [PATCH 07/36] Navigation bugs fixed - Back Pressed callback added to Fragments - Old Menu implementations updated to menuHost.addMenuProvider --- .../main/java/com/udacity/project4/MyApp.kt | 4 +- .../locationreminders/RemindersActivity.kt | 5 +- .../reminderslist/ReminderListFragment.kt | 83 ++++++++++++++++--- .../savereminder/SaveReminderFragment.kt | 55 +++++++++++- .../SelectLocationFragment.kt | 72 ++++++++++++++-- .../app/src/main/res/navigation/nav_graph.xml | 8 +- starter/build.gradle | 2 +- 7 files changed, 203 insertions(+), 26 deletions(-) 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 f6378c09b..a4e8e5bfb 100644 --- a/starter/app/src/main/java/com/udacity/project4/MyApp.kt +++ b/starter/app/src/main/java/com/udacity/project4/MyApp.kt @@ -20,14 +20,14 @@ class MyApp : Application() { * use Koin Library as a service locator */ val myModule = module { - //Declare a ViewModel - be later inject into Fragment with dedicated injector using by viewModel() + // 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() + // Declare singleton definitions to be later injected using by inject() single { //This view model is declared singleton to be used across multiple fragments SaveReminderViewModel( 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 7e3647abc..5eeceb2a5 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 @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.navigation.fragment.NavHostFragment +import com.firebase.ui.auth.AuthUI import com.udacity.project4.databinding.ActivityRemindersBinding /** @@ -19,6 +20,7 @@ class RemindersActivity : AppCompatActivity() { setContentView(binding.root) } + /* override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { @@ -28,4 +30,5 @@ class RemindersActivity : AppCompatActivity() { } return super.onOptionsItemSelected(item) } -} + */ +} \ 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 e20156d51..fee6971e1 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,8 +1,17 @@ package com.udacity.project4.locationreminders.reminderslist import android.os.Bundle -import android.view.* +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.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle import com.firebase.ui.auth.AuthUI import com.udacity.project4.R import com.udacity.project4.base.BaseFragment @@ -19,6 +28,8 @@ class ReminderListFragment : BaseFragment() { override val _viewModel: RemindersListViewModel by viewModel() private lateinit var binding: FragmentRemindersBinding + private var logoutClicked = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -27,10 +38,15 @@ class ReminderListFragment : BaseFragment() { R.layout.fragment_reminders, container, false ) binding.viewModel = _viewModel + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) + +// setHasOptionsMenu(true) + val menuHost: MenuHost = requireActivity() + addMenu(menuHost) - setHasOptionsMenu(true) setDisplayHomeAsUpEnabled(false) setTitle(getString(R.string.app_name)) + binding.refreshLayout.setOnRefreshListener { _viewModel.loadReminders() } return binding.root } @@ -44,8 +60,23 @@ class ReminderListFragment : BaseFragment() { } } + /** + * 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() + } + } + override fun onResume() { super.onResume() + + logoutClicked = false // Load the reminders list on the ui _viewModel.loadReminders() } @@ -53,7 +84,9 @@ class ReminderListFragment : BaseFragment() { private fun navigateToAddReminder() { // Use the navigationCommand live data to navigate between the fragments _viewModel.navigationCommand.postValue( - NavigationCommand.To(ReminderListFragmentDirections.toSaveReminder()) + NavigationCommand.To( + ReminderListFragmentDirections.actionReminderListFragmentToSaveReminderFragment() + ) ) } @@ -63,20 +96,48 @@ class ReminderListFragment : BaseFragment() { binding.reminderssRecyclerView.setup(adapter) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.logout -> { - // Logout implementation - AuthUI.getInstance().signOut(requireContext()) - requireActivity().onBackPressedDispatcher.onBackPressed() + 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.main_menu, menu) } - } - return super.onOptionsItemSelected(item) + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.logout -> { + logoutClicked = true + requireActivity().onBackPressedDispatcher.onBackPressed() + } + android.R.id.home -> { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + } + return true + } + }, viewLifecycleOwner, Lifecycle.State.RESUMED) } + /* override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) // Display logout as menu item inflater.inflate(R.menu.main_menu, menu) } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.logout -> { + logoutClicked = true + requireActivity().onBackPressedDispatcher.onBackPressed() + } + android.R.id.home -> { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + } + return super.onOptionsItemSelected(item) + } + */ } \ 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 4c23a6be8..1591f9c1e 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 @@ -2,9 +2,16 @@ package com.udacity.project4.locationreminders.savereminder import android.os.Bundle 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.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle import com.udacity.project4.R import com.udacity.project4.base.BaseFragment import com.udacity.project4.base.NavigationCommand @@ -23,9 +30,14 @@ class SaveReminderFragment : BaseFragment() { ): View { val layoutId = R.layout.fragment_save_reminder binding = DataBindingUtil.inflate(inflater, layoutId, container, false) - setDisplayHomeAsUpEnabled(true) binding.viewModel = _viewModel + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) + +// setHasOptionsMenu(true) + val menuHost: MenuHost = requireActivity() + addMenu(menuHost) + return binding.root } @@ -47,8 +59,34 @@ class SaveReminderFragment : BaseFragment() { val longitude = _viewModel.longitude.value // TODO: Use the user entered reminder details to: - // 1) add a geofencing request - // 2) save the reminder to the local db + // 1) Add a geofencing request + // 2) Save the reminder to the local db + } + } + + 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) {} + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + android.R.id.home -> { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + } + return true + } + }, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + + private val backPressHandler = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + _viewModel.navigationCommand.postValue( + NavigationCommand.Back + ) } } @@ -57,4 +95,15 @@ class SaveReminderFragment : BaseFragment() { // Make sure to clear the view model after destroy, as it's a single view model. _viewModel.onClear() } + + /* + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + } + return super.onOptionsItemSelected(item) + } + */ } \ 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 4cdd32df4..3ae1ea69a 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 @@ -7,8 +7,12 @@ import android.content.res.Resources import android.os.Bundle import android.util.Log import android.view.* +import androidx.activity.OnBackPressedCallback import androidx.core.app.ActivityCompat +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.Granularity import com.google.android.gms.location.LocationRequest @@ -23,6 +27,7 @@ import com.google.android.gms.maps.SupportMapFragment import com.google.android.gms.maps.model.* 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.setDisplayHomeAsUpEnabled @@ -42,7 +47,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { override val _viewModel: SaveReminderViewModel by inject() private lateinit var map: GoogleMap - private lateinit var locationClient: FusedLocationProviderClient + private var locationClient: FusedLocationProviderClient? = null private lateinit var locationRequest: LocationRequest private lateinit var binding: FragmentSelectLocationBinding @@ -59,8 +64,12 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { binding.viewModel = _viewModel binding.lifecycleOwner = this + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) + +// setHasOptionsMenu(true) + val menuHost: MenuHost = requireActivity() + addMenu(menuHost) - setHasOptionsMenu(true) setDisplayHomeAsUpEnabled(true) // Add the map setup implementation. @@ -76,9 +85,54 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { return binding.root } + 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 + ) + } + } + override fun onDestroy() { super.onDestroy() - locationClient.flushLocations() + locationClient?.flushLocations() // locationClient.removeLocationUpdates(this) } @@ -86,6 +140,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { // Menu Methods //-------------------------------------------------- + /* override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.map_options, menu) } @@ -108,8 +163,13 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { map.mapType = GoogleMap.MAP_TYPE_TERRAIN true } + android.R.id.home -> { + requireActivity().onBackPressedDispatcher.onBackPressed() + true + } else -> super.onOptionsItemSelected(item) } + */ //-------------------------------------------------- // Location Methods @@ -165,11 +225,11 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { */ locationClient = getFusedLocationProviderClient(requireActivity()) - locationClient.lastLocation - .addOnSuccessListener { location -> // GPS location can be null if GPS is switched off + locationClient?.lastLocation + ?.addOnSuccessListener { location -> // GPS location can be null if GPS is switched off location?.let { goToLocation(location.latitude, location.longitude) } } - .addOnFailureListener { e -> + ?.addOnFailureListener { e -> Log.d("MapDemoActivity", "Error trying to get last GPS location") e.printStackTrace() } diff --git a/starter/app/src/main/res/navigation/nav_graph.xml b/starter/app/src/main/res/navigation/nav_graph.xml index a3dbf35a8..c17cf8fbc 100644 --- a/starter/app/src/main/res/navigation/nav_graph.xml +++ b/starter/app/src/main/res/navigation/nav_graph.xml @@ -11,7 +11,7 @@ android:label="Reminder List Fragment" tools:layout="@layout/fragment_reminders"> @@ -32,5 +32,9 @@ android:id="@+id/selectLocationFragment" android:name="com.udacity.project4.locationreminders.savereminder.selectreminderlocation.SelectLocationFragment" android:label="Select Location" - tools:layout="@layout/fragment_select_location" /> + tools:layout="@layout/fragment_select_location"> + + \ No newline at end of file diff --git a/starter/build.gradle b/starter/build.gradle index 08bb038d9..8f7d99468 100644 --- a/starter/build.gradle +++ b/starter/build.gradle @@ -4,7 +4,7 @@ buildscript { ext { gradleVersion = '7.4.2' kotlinVersion = '1.8.0' - navigationVersion = '2.5.3' + navigationVersion = '2.5.3' // Sdk and tools // Support library and architecture components support minSdk 14 and above. From 78d497653a2cca680854e9b7ac56d861e21e99e5 Mon Sep 17 00:00:00 2001 From: Rodrigo Cericatto Konzen Date: Wed, 26 Apr 2023 16:26:42 -0400 Subject: [PATCH 08/36] SelectLocationFragment refactored. - Updating Permission's checking. - Location tracking fixed. - registerForActivityResult added. - onActivityResult and onRequestPermissionsResult removed. - Permission's Extension methods created. --- starter/.idea/misc.xml | 2 +- starter/app/src/main/AndroidManifest.xml | 1 + .../com/udacity/project4/TrackingActivity.kt | 233 ++++++ .../authentication/AuthenticationActivity.kt | 17 +- .../SelectLocationFragment.kt | 735 +++++++++++++----- .../com/udacity/project4/utils/Extensions.kt | 38 + .../src/main/res/layout/activity_tracking.xml | 5 + .../res/layout/fragment_select_location.xml | 3 +- starter/app/src/main/res/values/strings.xml | 9 +- 9 files changed, 843 insertions(+), 200 deletions(-) create mode 100644 starter/app/src/main/java/com/udacity/project4/TrackingActivity.kt create mode 100644 starter/app/src/main/res/layout/activity_tracking.xml diff --git a/starter/.idea/misc.xml b/starter/.idea/misc.xml index 481db9dd1..8d25e2666 100644 --- a/starter/.idea/misc.xml +++ b/starter/.idea/misc.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/starter/app/src/main/AndroidManifest.xml b/starter/app/src/main/AndroidManifest.xml index a06a20673..a6f15d085 100644 --- a/starter/app/src/main/AndroidManifest.xml +++ b/starter/app/src/main/AndroidManifest.xml @@ -31,6 +31,7 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> + 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..b10114062 --- /dev/null +++ b/starter/app/src/main/java/com/udacity/project4/TrackingActivity.kt @@ -0,0 +1,233 @@ +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 + */ +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 78431071e..e963523fb 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 @@ -35,16 +35,23 @@ class AuthenticationActivity : AppCompatActivity() { binding = DataBindingUtil.setContentView(this, layoutId) binding.authButton.text = getString(R.string.login) - AuthUI.getInstance().signOut(this) + // TODO: Line below was commented to avoid losing time Login In on Firebase + // every time I test the app. +// AuthUI.getInstance().signOut(this) } override fun onResume() { super.onResume() - observeAuthenticationState() + // TODO: Line below was commented to avoid losing time Login In on Firebase + // every time I test the app. +// observeAuthenticationState() - binding.authButton.setOnClickListener { - launchSignInFlow() - } + // TODO: Lines below were commented to avoid losing time Login In on Firebase + // every time I test the app. +// binding.authButton.setOnClickListener { +// launchSignInFlow() +// } + startActivity(Intent(this, RemindersActivity::class.java)) } /** 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 3ae1ea69a..16d667955 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 @@ -2,23 +2,31 @@ package com.udacity.project4.locationreminders.savereminder.selectreminderlocati import android.Manifest import android.annotation.SuppressLint -import android.content.pm.PackageManager +import android.content.Context +import android.content.Intent import android.content.res.Resources +import android.location.LocationManager +import android.os.Build import android.os.Bundle +import android.os.Looper +import android.provider.Settings import android.util.Log import android.view.* import androidx.activity.OnBackPressedCallback -import androidx.core.app.ActivityCompat +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 androidx.lifecycle.lifecycleScope 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.LocationServices +import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices.getFusedLocationProviderClient -import com.google.android.gms.location.LocationSettingsRequest import com.google.android.gms.location.Priority import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.GoogleMap @@ -30,27 +38,131 @@ 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.permissionGranted import com.udacity.project4.utils.setDisplayHomeAsUpEnabled +import com.udacity.project4.utils.showRequestPermissionRationale +import com.udacity.project4.utils.toast +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import java.util.* + class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { + //-------------------------------------------------- + // Attributes + //-------------------------------------------------- + companion object { private val TAG = SelectLocationFragment::class.java.simpleName - private const val REQUEST_LOCATION_PERMISSION = 1 + 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 const val GO_TO_SETTINGS = 33 + private const val FINE = Manifest.permission.ACCESS_FINE_LOCATION + private const val BACK = Manifest.permission.ACCESS_BACKGROUND_LOCATION } // Use Koin to get the view model of the SaveReminder override val _viewModel: SaveReminderViewModel by inject() + private var map: GoogleMap? = null + private var checkedGpsBefore = false + private var permissionAskedForUser = false - private lateinit var map: GoogleMap - private var locationClient: FusedLocationProviderClient? = null - private lateinit var locationRequest: LocationRequest + private var alertPleaseAcceptAllowAllTime: AlertDialog? = null + private var alertShouldEnableGps: AlertDialog? = null private lateinit var binding: FragmentSelectLocationBinding + private lateinit var parent: FragmentActivity + + //-------------------------------------------------- + // GPS Location Attributes + //-------------------------------------------------- + + 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 var locationCallback: LocationCallback = object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + Log.d(TAG, "onLocationResult().") + val locationList = locationResult.locations + if (locationList.isNotEmpty()) { + // The last location in the list is the newest. + val location = locationList.last() + goToLocation(location.latitude, location.longitude) +// parent.toast("Got Location: $location") + // TODO: Here I need to call method onLocationSelected() + onLocationSelected() + } + } + } + + //-------------------------------------------------- + // Activity Result Callbacks + //-------------------------------------------------- + + /** + * Source: + * https://developer.android.com/training/permissions/requesting#allow-system-manage-request-code + */ + + // Register the permissions callback, which handles the user's response to the system + // permissions dialog. Save the return value, an instance of ActivityResultLauncher. You can use + // either a val, as shown in this snippet, or a lateinit var in your onAttach() or onCreate() + // method. + val requestFinePermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + Log.d(TAG, "requestFinePermissionLauncher() -> isGranted: $isGranted") + if (isGranted) { + // Permission is granted. Continue the action or workflow in your app. + checkBackgroundLocationPermission() + } else { + // Explain to the user that the feature is unavailable because the feature requires a + // permission that the user has denied. At the same time, respect the user's decision. + // Don't link to system settings in an effort to convince the user to change their + // decision. + permissionDeniedFeedback() + } + } + + val requestBackPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + Log.d(TAG, "requestBackPermissionLauncher() -> isGranted: $isGranted") + if (isGranted) { + lifecycleScope.launch { + delay(3000) + checkGpsEnabled() + } + } else { + permissionDeniedFeedback() + } + } + + /** + * Source: + * https://www.tothenew.com/blog/android-katha-onactivityresult-is-deprecated-now-what/ + */ + var activityResultLauncher = registerForActivityResult(ActivityResultContracts + .StartActivityForResult()) { + Log.d(TAG, "activityResultLauncher().") + if (isGpsEnabled()) { + parent.toast(R.string.gps_enabled) + requestTracking() + } else { + permissionDeniedFeedback() + } + } //-------------------------------------------------- // Lifecycle Methods @@ -64,32 +176,134 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { binding.viewModel = _viewModel binding.lifecycleOwner = this + parent = requireActivity() requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) -// setHasOptionsMenu(true) - val menuHost: MenuHost = requireActivity() - addMenu(menuHost) - setDisplayHomeAsUpEnabled(true) - // Add the map setup implementation. - // Obtain the SupportMapFragment and get notified when the map is ready to be used. - initMap() + fusedLocationProvider = getFusedLocationProviderClient(parent) + startMapFeature() + initMenus() + checkLocationPermission() - // TODO: Call this function after the user confirms on the selected location + binding.fab.visibility = View.GONE + /* binding.fab.setOnClickListener { -// onLocationSelected() - connectToGetLocation() + if (!isGpsEnabled()) { + startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) + } } + */ return binding.root } + // + override fun onResume() { + super.onResume() + Log.d(TAG, "onResume().") + /* + checkUserDecision() + Log.d(TAG, "onResume() -> [1]") + if (parent.permissionGranted(FINE)) { + Log.d(TAG, "onResume() -> [2]") + if (parent.permissionGranted(BACK)) { + Log.d(TAG, "onResume() -> [3]") + lifecycleScope.launch { + delay(3000) + checkGpsEnabled() + } + } else { + Log.d(TAG, "onResume() -> [4]") + checkGpsEnabled() + } + } else { + Log.d(TAG, "onResume() -> [5]") + if (permissionAskedForUser) { + Log.d(TAG, "onResume() -> [6]") + permissionDeniedFeedback() + } + } + */ + } + + private fun checkUserDecision() { + val fine = parent.permissionGranted(FINE) + val back = parent.permissionGranted(BACK) + /* + val userDenies = grantResults[0] == PermissionChecker.PERMISSION_DENIED && + grantResults[1] == PermissionChecker.PERMISSION_DENIED + val userAllowWhileUsing = grantResults[0] == PermissionChecker.PERMISSION_DENIED && + grantResults[1] == PermissionChecker.PERMISSION_GRANTED + val userAllowAllTheTime = grantResults[0] == PermissionChecker.PERMISSION_GRANTED && + grantResults[1] == PermissionChecker.PERMISSION_GRANTED + */ + Log.d(TAG, "checkUserDecision() -> fine? $fine") + Log.d(TAG, "checkUserDecision() -> back? $back") + } + + override fun onPause() { + super.onPause() + Log.d(TAG, "onPause().") + if (parent.permissionGranted(FINE)) { + fusedLocationProvider?.removeLocationUpdates(locationCallback) + } + } + + /* + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + } + */ + + /* + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + Log.d(TAG, "onActivityResult().") + if (requestCode == GO_TO_SETTINGS) { + if (isGpsEnabled()) { + parent.toast(R.string.gps_enabled) + requestTracking() + } else { + permissionDeniedFeedback() + } + } + } + */ + + override fun onDestroy() { + super.onDestroy() + Log.d(TAG, "onDestroy().") + disableDialogs() + } + + private fun disableDialogs() { + alertPleaseAcceptAllowAllTime?.let { + if (it.isShowing) { + it.cancel() + } + } + alertShouldEnableGps?.let { + if (it.isShowing) { + it.cancel() + } + } + } + + //-------------------------------------------------- + // Menu Methods + //-------------------------------------------------- + + private fun initMenus() { + val menuHost: MenuHost = requireActivity() + addMenu(menuHost) + } + 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 + // 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) @@ -97,19 +311,19 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { override fun onMenuItemSelected(menuItem: MenuItem): Boolean { when (menuItem.itemId) { R.id.normal_map -> { - map.mapType = GoogleMap.MAP_TYPE_NORMAL + map?.mapType = GoogleMap.MAP_TYPE_NORMAL true } R.id.hybrid_map -> { - map.mapType = GoogleMap.MAP_TYPE_HYBRID + map?.mapType = GoogleMap.MAP_TYPE_HYBRID true } R.id.satellite_map -> { - map.mapType = GoogleMap.MAP_TYPE_SATELLITE + map?.mapType = GoogleMap.MAP_TYPE_SATELLITE true } R.id.terrain_map -> { - map.mapType = GoogleMap.MAP_TYPE_TERRAIN + map?.mapType = GoogleMap.MAP_TYPE_TERRAIN true } android.R.id.home -> { @@ -130,178 +344,281 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } } - override fun onDestroy() { - super.onDestroy() - locationClient?.flushLocations() -// locationClient.removeLocationUpdates(this) - } - //-------------------------------------------------- - // Menu Methods + // Permission Methods //-------------------------------------------------- - /* - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.map_options, menu) + private fun checkLocationPermission() { + Log.d(TAG, "checkLocationPermission().") + when { + parent.permissionGranted(FINE) -> { + // You can use the API that requires the permission. + checkBackgroundLocationPermission() + } + parent.showRequestPermissionRationale(FINE) -> { + // In an educational UI, explain to the user why your app requires this permission + // for a specific feature to behave as expected, and what features are disabled if + // it's declined. In this UI, include a "cancel" or "no thanks" button that lets the + // user continue using your app without granting the permission. + // -------------------------------------------------- + // 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. + Log.d(TAG, "checkLocationPermission() -> [2]") + 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 -> { + // You can directly ask for the permission. + // The registered ActivityResultCallback gets the result of this request. + requestFinePermissionLauncher.launch(FINE) + } + } } - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - // Change the map type based on the user's selection. - 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 + /* + private fun checkLocationPermission() { + Log.d(TAG, "checkLocationPermission().") + if (!parent.permissionGranted(FINE)) { + Log.d(TAG, "checkLocationPermission() -> [1]") + // Should we show an explanation? + if (parent.showRequestPermissionRationale(FINE)) { + // 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. + Log.d(TAG, "checkLocationPermission() -> [2]") + 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. + Log.d(TAG, "checkLocationPermission() -> [3]") + requestLocationPermission() + } + } else { + Log.d(TAG, "checkLocationPermission() -> [4]") + checkBackgroundLocationPermission() } - else -> super.onOptionsItemSelected(item) } */ - //-------------------------------------------------- - // Location Methods - //-------------------------------------------------- - - @SuppressLint("MissingPermission") - private fun startLocationUpdates() { - // Create the location request to start receiving updates - // https://tomas-repcik.medium.com/locationrequest-create-got-deprecated-how-to-fix-it-e4f814138764 - locationRequest = LocationRequest.Builder( - Priority.PRIORITY_HIGH_ACCURACY, UPDATE_INTERVAL - ).apply { - setMinUpdateDistanceMeters(5F) - setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL) - setWaitForAccurateLocation(true) - setMinUpdateIntervalMillis(FASTEST_INTERVAL) - }.build() - - // Create LocationSettingsRequest object using location request - val builder = LocationSettingsRequest.Builder() - builder.addLocationRequest(locationRequest) - val locationSettingsRequest = builder.build() - - // Check whether location settings are satisfied - // https://developers.google.com/android/reference/com/google/android/gms/location/SettingsClient - val settingsClient = LocationServices.getSettingsClient(requireActivity()) - settingsClient.checkLocationSettings(locationSettingsRequest) + private fun checkBackgroundLocationPermission() { + Log.d(TAG, "checkBackgroundLocation().") + when { + parent.permissionGranted(BACK) -> { + // You can use the API that requires the permission. + lifecycleScope.launch { + delay(3000) + checkGpsEnabled() + } + } + else -> { + // You can directly ask for the permission. + // The registered ActivityResultCallback gets the result of this request. + requestBackgroundLocationPermission() + } + } } /* - private fun onLocationChanged(location: Location) { - // New location has now been determined - val msg = "Updated Location: " + location.latitude.toString() + "," + location.longitude.toString() - Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() - // You can now create a LatLng Object for use with maps - val latLng = LatLng(location.latitude, location.longitude) + private fun checkBackgroundLocationPermission() { + Log.d(TAG, "checkBackgroundLocation().") + if (!parent.permissionGranted(BACK)) { + Log.d(TAG, "checkBackgroundLocation() -> [1]") + requestBackgroundLocationPermission() + } else { + Log.d(TAG, "checkBackgroundLocation() -> [2]") + } } */ - @SuppressLint("MissingPermission") - private fun connectToGetLocation() { - /* - locationClient = getFusedLocationProviderClient(requireActivity()) - locationClient.requestLocationUpdates( - locationRequest, object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - // do work here - locationResult.lastLocation?.let { onLocationChanged(it) } + private fun requestLocationPermission() { + Log.d(TAG, "+++++++++++++++ requestLocationPermission().") + permissionAskedForUser = true + requestFinePermissionLauncher.launch(FINE) +// parent.requestTrackingPermissions( +// arrayOf(FINE), +// MY_PERMISSIONS_REQUEST_LOCATION +// ) + } + + /** + * This is the only method that will call the "Location permission" settings screen + * (https://bit.ly/41VLahm) of your app, showing the user 4 possible options: + * - "Allow all the time" + * - "Allow only while using the app" + * - "Ask every time" + * - "Don't allow" + * This settings screen will only be called if the app has Manifest.permission.ACCESS_FINE_LOCATION. + * Once the app have it, then, we need the Manifest.permission.ACCESS_BACKGROUND_LOCATION + * (in order to track the user GPS's location). + */ + private fun requestBackgroundLocationPermission() { + Log.d(TAG, "+++++++++++++++ requestBackgroundLocationPermission().") + permissionAskedForUser = true + val builder = AlertDialog.Builder(parent) + builder.setMessage(R.string.please_accept_allow_all_time) + .setCancelable(false) + .setPositiveButton("Yes") { _, _ -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { +// parent.requestTrackingPermissions( +// arrayOf(BACK), +// MY_PERMISSIONS_REQUEST_BACKGROUND_LOCATION +// ) + requestBackPermissionLauncher.launch(BACK) + } else { +// parent.requestTrackingPermissions( +// arrayOf(FINE), +// MY_PERMISSIONS_REQUEST_LOCATION +// ) + requestFinePermissionLauncher.launch(FINE) } - }, - Looper.myLooper() - ) - */ + } + .setNegativeButton("No") { dialog, _ -> + dialog.cancel() + permissionDeniedFeedback() + } + alertPleaseAcceptAllowAllTime = builder.create() + alertPleaseAcceptAllowAllTime?.show() + } - locationClient = getFusedLocationProviderClient(requireActivity()) - locationClient?.lastLocation - ?.addOnSuccessListener { location -> // GPS location can be null if GPS is switched off - location?.let { goToLocation(location.latitude, location.longitude) } + /* + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + */ + /* + Log.d(TAG, "----- onRequestPermissionsResult().") + // https://stackoverflow.com/a/69135604/1354788 + when (requestCode) { + MY_PERMISSIONS_REQUEST_LOCATION -> { + onRequestLocationPermissionsResult(grantResults) + return } - ?.addOnFailureListener { e -> - Log.d("MapDemoActivity", "Error trying to get last GPS location") - e.printStackTrace() + MY_PERMISSIONS_REQUEST_BACKGROUND_LOCATION -> { + onRequestBackgroundLocationPermissionsResult(grantResults) + return } + } } + */ - private fun goToLocation(lat: Double, lng: Double) { - val latLng = LatLng(lat, lng) - val cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 18F) - map.moveCamera(cameraUpdate) - map.mapType = GoogleMap.MAP_TYPE_NORMAL + /* + private fun onRequestLocationPermissionsResult(grantResults: IntArray) { + // If request is cancelled, the result arrays are empty. + Log.d(TAG, "----- onRequestLocationPermissionsResult().") + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission was granted, yay! Do the location-related task you need to do. + if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { + Log.d(TAG, "onRequestLocationPermissionsResult() -> " + + "if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION))") + requestTrackingAndStartMap() + // Now check background location. + checkBackgroundLocationPermission() + } + } else { + Log.d(TAG, "onRequestLocationPermissionsResult() -> " + + "else OF if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION))") + // Permission denied, boo! Disable the functionality that depends on this permission. + parent.toast("Permission denied") + + // Check if we are in a state where the user has denied the permission and + // selected Don't ask again. + if (!parent.showRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + parent.toast("blaaaaaa") + /* + startActivity( + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", parent.packageName, null), + ), + ) + */ + } + } } + */ - 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 + /* + private fun onRequestBackgroundLocationPermissionsResult(grantResults: IntArray) { + Log.d(TAG, "----- onRequestBackgroundLocationPermissionsResult().") + // If request is cancelled, the result arrays are empty. + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "onRequestBackgroundLocationPermissionsResult() -> " + + "if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)") + // Permission was granted, yay! Do the location-related task you need to do. + if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { + Log.d(TAG, "onRequestBackgroundLocationPermissionsResult() -> " + + "if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION))") + requestTrackingAndStartMap() + parent.toast("Granted Background Location Permission") + } + } else { + Log.d(TAG, "onRequestBackgroundLocationPermissionsResult() -> " + + "else OF if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)" + + "... Permission denied") + // Permission denied, boo! Disable the functionality that depends on this permission. + parent.toast("Permission denied") + } } + */ //-------------------------------------------------- // Maps Methods //-------------------------------------------------- + //#region MapsMethods - private fun initMap() { - if (isPermissionGranted()) { - val mapFragment = childFragmentManager - .findFragmentById(R.id.map) as SupportMapFragment - mapFragment.getMapAsync(this) - startLocationUpdates() - } + private fun startMapFeature() { + Log.d(TAG, "startMapFeature().") + val mapFragment = childFragmentManager + .findFragmentById(R.id.map) 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. + * 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) { + Log.d(TAG, "onMapReady().") map = googleMap +// requireActivity().toast(R.string.maps_success) // Add a marker in Sydney and move the camera - val sydney = LatLng(-34.0, 151.0) - map.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney")) - map.moveCamera(CameraUpdateFactory.newLatLng(sydney)) - -// val latitude = 40.417297 -// val longitude = -86.892163 -// val zoomLevel = 18f -// val homeLatLng = LatLng(latitude, longitude) -// map.addMarker(MarkerOptions().position(homeLatLng)) -// map.moveCamera(CameraUpdateFactory.newLatLngZoom(homeLatLng, zoomLevel)) - -// val overlaySize = 100f -// val androidOverlay = GroundOverlayOptions() -// .image(BitmapDescriptorFactory.fromResource(R.drawable.android)) -// .position(homeLatLng, overlaySize) -// map.addGroundOverlay(androidOverlay) - - setMapLongClick(map) - - // Put a marker to location that the user selected - setPoiClick(map) - - // Add style to the map - setMapStyle(map) - - enableMyLocation() - - // TODO: Zoom to the user location after taking his permission + val sydney = LatLng(-34.0, 151.0) + map?.let { + it.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney")) + it.moveCamera(CameraUpdateFactory.newLatLng(sydney)) + + setMapLongClick(it) + // Put a marker to location that the user selected + setPoiClick(it) + // Add style to the map + setMapStyle(it) + } } private fun setMapLongClick(map: GoogleMap) { @@ -337,8 +654,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { private fun setMapStyle(map: GoogleMap) { try { - // Customize the styling of the base map using a JSON object defined - // in a raw resource file. + // Customize the styling of the base map using a JSON object defined in a raw res file. val success = map.setMapStyle( MapStyleOptions.loadRawResourceStyle( requireActivity(), @@ -352,44 +668,81 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { Log.e(TAG, "Can't find style. Error: ", e) } } + //#endregion - private fun isPermissionGranted(): Boolean { - val fine = ActivityCompat.checkSelfPermission( - requireActivity(), - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED + //-------------------------------------------------- + // Location Methods + //-------------------------------------------------- - val coarse = ActivityCompat.checkSelfPermission( - requireActivity(), - Manifest.permission.ACCESS_COARSE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - return fine && coarse + /** + * Source: + * https://stackoverflow.com/a/25175756/1354788 + */ + private fun checkGpsEnabled() { + Log.d(TAG, "checkGpsEnabled() -> checkedGpsBefore: $checkedGpsBefore") +// if (!checkedGpsBefore) { + if (!isGpsEnabled()) { + Log.d(TAG, "checkGpsEnabled() -> [1]") + buildAlertMessageNoGps() + } else { + Log.d(TAG, "checkGpsEnabled() -> [2]") + requestTracking() + } +// } + checkedGpsBefore = true } - @SuppressLint("MissingPermission") - private fun enableMyLocation() { - if (isPermissionGranted()) { - map.isMyLocationEnabled = true - } else { - ActivityCompat.requestPermissions( - requireActivity(), - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - REQUEST_LOCATION_PERMISSION - ) - } + private fun isGpsEnabled(): Boolean { + Log.d(TAG, "isGpsEnabled().") + val manager = parent.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return manager.isProviderEnabled(LocationManager.GPS_PROVIDER) } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - // Check if location permissions are granted and if so enable the location data layer. - if (requestCode == REQUEST_LOCATION_PERMISSION) { - if (grantResults.isNotEmpty() && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { - enableMyLocation() + private fun buildAlertMessageNoGps() { + Log.d(TAG, "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() + permissionDeniedFeedback() + } + alertShouldEnableGps = builder.create() + alertShouldEnableGps?.show() + } + + private fun goToLocation(lat: Double, lng: Double) { + Log.d(TAG, "goToLocation().") + map?.let { + val latLng = LatLng(lat, lng) + // Zoom to the user location after taking his permission. + val cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 18F) + it.moveCamera(cameraUpdate) + it.mapType = GoogleMap.MAP_TYPE_NORMAL } } + + private fun permissionDeniedFeedback() { + parent.toast(R.string.allow_all_time_did_not_accepted) + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + @SuppressLint("MissingPermission") + private fun requestTracking() { + Log.d(TAG, "requestTracking().") + fusedLocationProvider?.requestLocationUpdates( + locationRequest, + locationCallback, + Looper.getMainLooper() + ) + } + + 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. + } } \ 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 dbb5348fe..c3b0cfcfb 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 @@ -1,13 +1,23 @@ package com.udacity.project4.utils +import android.Manifest import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.app.AlertDialog +import android.content.Context +import android.content.pm.PackageManager +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.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.udacity.project4.base.BaseRecyclerViewAdapter +import com.udacity.project4.locationreminders.savereminder.selectreminderlocation.SelectLocationFragment /** * Extension function to setup the RecyclerView. @@ -59,3 +69,31 @@ fun View.fadeOut() { } }) } + +fun Context.toast(textId: Int) { + Toast.makeText(this, textId, Toast.LENGTH_SHORT).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 + ) 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_select_location.xml b/starter/app/src/main/res/layout/fragment_select_location.xml index 62ffbfd4c..837925465 100644 --- a/starter/app/src/main/res/layout/fragment_select_location.xml +++ b/starter/app/src/main/res/layout/fragment_select_location.xml @@ -10,11 +10,10 @@ - - - Location Reminders + AAA Location Reminders Error happened Retry No Data @@ -25,6 +25,13 @@ You need to grant location permission in order to select the location. Settings Please select a 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? + GPS enabled! Tracking now the user location… + Please click into \"Allow all the time\" to accept the GPS location permission! + Since you didn\'t accept the Tracking permission, we\'re closing the Maps screen. Geofence Entered GeofenceStatus From c76efc3ca67def0029fdd2cdc2c3645b24d80c91 Mon Sep 17 00:00:00 2001 From: Rodrigo Cericatto Konzen Date: Wed, 26 Apr 2023 16:50:05 -0400 Subject: [PATCH 09/36] Removed unused methods on SelectLocationFragment --- .../src/debug/res/values/google_maps_api.xml | 2 +- .../SelectLocationFragment.kt | 263 ++---------------- .../res/layout/fragment_select_location.xml | 12 - 3 files changed, 17 insertions(+), 260 deletions(-) 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 141af24c0..b907946b1 100644 --- a/starter/app/src/debug/res/values/google_maps_api.xml +++ b/starter/app/src/debug/res/values/google_maps_api.xml @@ -3,5 +3,5 @@ TODO: Before you run your application, you need a Google Maps API key. --> AIzaSyC6hBgGRJHLlKoosvbR-nPUg0w5Lg3cbWs + translatable="false"> \ 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 16d667955..fe282f2f6 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 @@ -47,7 +47,6 @@ import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import java.util.* - class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { //-------------------------------------------------- @@ -56,11 +55,8 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { companion object { private val TAG = SelectLocationFragment::class.java.simpleName - 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 const val GO_TO_SETTINGS = 33 private const val FINE = Manifest.permission.ACCESS_FINE_LOCATION private const val BACK = Manifest.permission.ACCESS_BACKGROUND_LOCATION } @@ -68,8 +64,6 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { // Use Koin to get the view model of the SaveReminder override val _viewModel: SaveReminderViewModel by inject() private var map: GoogleMap? = null - private var checkedGpsBefore = false - private var permissionAskedForUser = false private var alertPleaseAcceptAllowAllTime: AlertDialog? = null private var alertShouldEnableGps: AlertDialog? = null @@ -93,13 +87,12 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { private var locationCallback: LocationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { - Log.d(TAG, "onLocationResult().") + Log.d(TAG, "locationCallback.onLocationResult().") val locationList = locationResult.locations if (locationList.isNotEmpty()) { // The last location in the list is the newest. val location = locationList.last() goToLocation(location.latitude, location.longitude) -// parent.toast("Got Location: $location") // TODO: Here I need to call method onLocationSelected() onLocationSelected() } @@ -186,62 +179,9 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { initMenus() checkLocationPermission() - binding.fab.visibility = View.GONE - /* - binding.fab.setOnClickListener { - if (!isGpsEnabled()) { - startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) - } - } - */ - return binding.root } - // - override fun onResume() { - super.onResume() - Log.d(TAG, "onResume().") - /* - checkUserDecision() - Log.d(TAG, "onResume() -> [1]") - if (parent.permissionGranted(FINE)) { - Log.d(TAG, "onResume() -> [2]") - if (parent.permissionGranted(BACK)) { - Log.d(TAG, "onResume() -> [3]") - lifecycleScope.launch { - delay(3000) - checkGpsEnabled() - } - } else { - Log.d(TAG, "onResume() -> [4]") - checkGpsEnabled() - } - } else { - Log.d(TAG, "onResume() -> [5]") - if (permissionAskedForUser) { - Log.d(TAG, "onResume() -> [6]") - permissionDeniedFeedback() - } - } - */ - } - - private fun checkUserDecision() { - val fine = parent.permissionGranted(FINE) - val back = parent.permissionGranted(BACK) - /* - val userDenies = grantResults[0] == PermissionChecker.PERMISSION_DENIED && - grantResults[1] == PermissionChecker.PERMISSION_DENIED - val userAllowWhileUsing = grantResults[0] == PermissionChecker.PERMISSION_DENIED && - grantResults[1] == PermissionChecker.PERMISSION_GRANTED - val userAllowAllTheTime = grantResults[0] == PermissionChecker.PERMISSION_GRANTED && - grantResults[1] == PermissionChecker.PERMISSION_GRANTED - */ - Log.d(TAG, "checkUserDecision() -> fine? $fine") - Log.d(TAG, "checkUserDecision() -> back? $back") - } - override fun onPause() { super.onPause() Log.d(TAG, "onPause().") @@ -250,28 +190,6 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } } - /* - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - } - */ - - /* - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - Log.d(TAG, "onActivityResult().") - if (requestCode == GO_TO_SETTINGS) { - if (isGpsEnabled()) { - parent.toast(R.string.gps_enabled) - requestTracking() - } else { - permissionDeniedFeedback() - } - } - } - */ - override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy().") @@ -353,6 +271,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { when { parent.permissionGranted(FINE) -> { // You can use the API that requires the permission. + Log.d(TAG, "checkLocationPermission() -> [1]") checkBackgroundLocationPermission() } parent.showRequestPermissionRationale(FINE) -> { @@ -372,7 +291,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { .setMessage(message) .setPositiveButton("OK") { _, _ -> // Prompt the user once explanation has been shown. - requestLocationPermission() + requestFinePermissionLauncher.launch(FINE) } .create() .show() @@ -380,52 +299,19 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { else -> { // You can directly ask for the permission. // The registered ActivityResultCallback gets the result of this request. - requestFinePermissionLauncher.launch(FINE) - } - } - } - - /* - private fun checkLocationPermission() { - Log.d(TAG, "checkLocationPermission().") - if (!parent.permissionGranted(FINE)) { - Log.d(TAG, "checkLocationPermission() -> [1]") - // Should we show an explanation? - if (parent.showRequestPermissionRationale(FINE)) { - // 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. - Log.d(TAG, "checkLocationPermission() -> [2]") - 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. Log.d(TAG, "checkLocationPermission() -> [3]") - requestLocationPermission() + requestFinePermissionLauncher.launch(FINE) } - } else { - Log.d(TAG, "checkLocationPermission() -> [4]") - checkBackgroundLocationPermission() } } - */ private fun checkBackgroundLocationPermission() { - Log.d(TAG, "checkBackgroundLocation().") + Log.d(TAG, "checkBackgroundLocationPermission().") when { parent.permissionGranted(BACK) -> { // You can use the API that requires the permission. lifecycleScope.launch { - delay(3000) + delay(2000) checkGpsEnabled() } } @@ -437,28 +323,6 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } } - /* - private fun checkBackgroundLocationPermission() { - Log.d(TAG, "checkBackgroundLocation().") - if (!parent.permissionGranted(BACK)) { - Log.d(TAG, "checkBackgroundLocation() -> [1]") - requestBackgroundLocationPermission() - } else { - Log.d(TAG, "checkBackgroundLocation() -> [2]") - } - } - */ - - private fun requestLocationPermission() { - Log.d(TAG, "+++++++++++++++ requestLocationPermission().") - permissionAskedForUser = true - requestFinePermissionLauncher.launch(FINE) -// parent.requestTrackingPermissions( -// arrayOf(FINE), -// MY_PERMISSIONS_REQUEST_LOCATION -// ) - } - /** * This is the only method that will call the "Location permission" settings screen * (https://bit.ly/41VLahm) of your app, showing the user 4 possible options: @@ -471,23 +335,14 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { * (in order to track the user GPS's location). */ private fun requestBackgroundLocationPermission() { - Log.d(TAG, "+++++++++++++++ requestBackgroundLocationPermission().") - permissionAskedForUser = true + Log.d(TAG, "requestBackgroundLocationPermission().") val builder = AlertDialog.Builder(parent) builder.setMessage(R.string.please_accept_allow_all_time) .setCancelable(false) .setPositiveButton("Yes") { _, _ -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -// parent.requestTrackingPermissions( -// arrayOf(BACK), -// MY_PERMISSIONS_REQUEST_BACKGROUND_LOCATION -// ) requestBackPermissionLauncher.launch(BACK) } else { -// parent.requestTrackingPermissions( -// arrayOf(FINE), -// MY_PERMISSIONS_REQUEST_LOCATION -// ) requestFinePermissionLauncher.launch(FINE) } } @@ -499,89 +354,6 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { alertPleaseAcceptAllowAllTime?.show() } - /* - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - */ - /* - Log.d(TAG, "----- onRequestPermissionsResult().") - // https://stackoverflow.com/a/69135604/1354788 - when (requestCode) { - MY_PERMISSIONS_REQUEST_LOCATION -> { - onRequestLocationPermissionsResult(grantResults) - return - } - MY_PERMISSIONS_REQUEST_BACKGROUND_LOCATION -> { - onRequestBackgroundLocationPermissionsResult(grantResults) - return - } - } - } - */ - - /* - private fun onRequestLocationPermissionsResult(grantResults: IntArray) { - // If request is cancelled, the result arrays are empty. - Log.d(TAG, "----- onRequestLocationPermissionsResult().") - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Permission was granted, yay! Do the location-related task you need to do. - if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { - Log.d(TAG, "onRequestLocationPermissionsResult() -> " + - "if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION))") - requestTrackingAndStartMap() - // Now check background location. - checkBackgroundLocationPermission() - } - } else { - Log.d(TAG, "onRequestLocationPermissionsResult() -> " + - "else OF if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION))") - // Permission denied, boo! Disable the functionality that depends on this permission. - parent.toast("Permission denied") - - // Check if we are in a state where the user has denied the permission and - // selected Don't ask again. - if (!parent.showRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { - parent.toast("blaaaaaa") - /* - startActivity( - Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", parent.packageName, null), - ), - ) - */ - } - } - } - */ - - /* - private fun onRequestBackgroundLocationPermissionsResult(grantResults: IntArray) { - Log.d(TAG, "----- onRequestBackgroundLocationPermissionsResult().") - // If request is cancelled, the result arrays are empty. - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "onRequestBackgroundLocationPermissionsResult() -> " + - "if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)") - // Permission was granted, yay! Do the location-related task you need to do. - if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { - Log.d(TAG, "onRequestBackgroundLocationPermissionsResult() -> " + - "if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION))") - requestTrackingAndStartMap() - parent.toast("Granted Background Location Permission") - } - } else { - Log.d(TAG, "onRequestBackgroundLocationPermissionsResult() -> " + - "else OF if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)" + - "... Permission denied") - // Permission denied, boo! Disable the functionality that depends on this permission. - parent.toast("Permission denied") - } - } - */ - //-------------------------------------------------- // Maps Methods //-------------------------------------------------- @@ -605,7 +377,6 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { override fun onMapReady(googleMap: GoogleMap) { Log.d(TAG, "onMapReady().") map = googleMap -// requireActivity().toast(R.string.maps_success) // Add a marker in Sydney and move the camera val sydney = LatLng(-34.0, 151.0) @@ -679,17 +450,14 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { * https://stackoverflow.com/a/25175756/1354788 */ private fun checkGpsEnabled() { - Log.d(TAG, "checkGpsEnabled() -> checkedGpsBefore: $checkedGpsBefore") -// if (!checkedGpsBefore) { - if (!isGpsEnabled()) { - Log.d(TAG, "checkGpsEnabled() -> [1]") - buildAlertMessageNoGps() - } else { - Log.d(TAG, "checkGpsEnabled() -> [2]") - requestTracking() - } -// } - checkedGpsBefore = true + Log.d(TAG, "checkGpsEnabled().") + if (!isGpsEnabled()) { + Log.d(TAG, "checkGpsEnabled() -> [1]") + buildAlertMessageNoGps() + } else { + Log.d(TAG, "checkGpsEnabled() -> [2]") + requestTracking() + } } private fun isGpsEnabled(): Boolean { @@ -726,6 +494,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } private fun permissionDeniedFeedback() { + Log.d(TAG, "permissionDeniedFeedback().") parent.toast(R.string.allow_all_time_did_not_accepted) requireActivity().onBackPressedDispatcher.onBackPressed() } 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 837925465..c7e017c48 100644 --- a/starter/app/src/main/res/layout/fragment_select_location.xml +++ b/starter/app/src/main/res/layout/fragment_select_location.xml @@ -1,6 +1,5 @@ @@ -21,16 +20,5 @@ android:layout_height="match_parent" tools:layout_editor_absoluteX="64dp" tools:layout_editor_absoluteY="16dp" /> - - From 91f6ab012efc4da0a2e9ddefb28dd91375387d1c Mon Sep 17 00:00:00 2001 From: Rodrigo Cericatto Konzen Date: Fri, 28 Apr 2023 00:04:26 -0400 Subject: [PATCH 10/36] Method onLocationSelected() added to setPoiClick() inside SelectLocationFragment --- .../geofence/GeofenceBroadcastReceiver.kt | 57 ++++++++++++- .../GeofenceTransitionsJobIntentService.kt | 10 +-- .../savereminder/SaveReminderFragment.kt | 53 +++++++------ .../savereminder/SaveReminderViewModel.kt | 16 ++-- .../SelectLocationFragment.kt | 79 ++++++++++++++++--- .../com/udacity/project4/utils/Extensions.kt | 39 +++++++-- .../project4/utils/NotificationUtils.kt | 3 +- starter/app/src/main/res/values/strings.xml | 16 ++-- starter/gradle.properties | 1 + 9 files changed, 215 insertions(+), 59 deletions(-) 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 cf976c084..107783cd1 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 @@ -1,8 +1,15 @@ package com.udacity.project4.locationreminders.geofence +import android.app.NotificationManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.util.Log +import androidx.core.content.ContextCompat +import com.google.android.gms.location.Geofence +import com.google.android.gms.location.GeofencingEvent +import com.udacity.project4.R +import com.udacity.project4.utils.errorMessage /** * Triggered by the Geofence. Since we can have many Geofences at once, we pull the request @@ -14,7 +21,55 @@ import android.content.Intent * */ class GeofenceBroadcastReceiver : BroadcastReceiver() { + private val TAG = "GeofenceReceiver" + override fun onReceive(context: Context, intent: Intent) { - // TODO: implement the onReceive method to receive the geofencing events at the background + // TODO: Implement the onReceive method to receive the geofencing events at the background. + val geofencingEvent = GeofencingEvent.fromIntent(intent) + + if (geofencingEvent != null) { + if (geofencingEvent.hasError()) { + val errorMessage = errorMessage(context, geofencingEvent.errorCode) + Log.e(TAG, errorMessage) + return + } + } + + /* + if (geofencingEvent != null) { + if (geofencingEvent.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) { + Log.v(TAG, context.getString(R.string.geofence_entered)) + + val fenceId = when { + geofencingEvent.triggeringGeofences!!.isNotEmpty() -> + geofencingEvent.triggeringGeofences!![0].requestId + else -> { + Log.e(TAG, "No Geofence Trigger Found! Abort mission!") + return + } + } + // Check geofence against the constants listed in GeofenceUtil.kt to see if the + // user has entered any of the locations we track for geofences. + val foundIndex = GeofencingConstants.LANDMARK_DATA.indexOfFirst { + it.id == fenceId + } + + // Unknown Geofences aren't helpful to us + if ( -1 == foundIndex ) { + Log.e(TAG, "Unknown Geofence: Abort Mission") + return + } + + val notificationManager = ContextCompat.getSystemService( + context, + NotificationManager::class.java + ) as NotificationManager + + notificationManager.sendGeofenceEnteredNotification( + context, foundIndex + ) + } + } + */ } } \ 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 index a1b006520..9a958b4f3 100644 --- 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 @@ -22,7 +22,7 @@ class GeofenceTransitionsJobIntentService : JobIntentService(), CoroutineScope { companion object { private const val JOB_ID = 573 - // TODO: call this to start the JobIntentService to handle the geofencing transition events + // TODO: Call this to start the JobIntentService to handle the geofencing transition events. fun enqueueWork(context: Context, intent: Intent) { enqueueWork( context, @@ -33,12 +33,12 @@ class GeofenceTransitionsJobIntentService : JobIntentService(), CoroutineScope { } 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: 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 + // TODO: Get the request id of the current geofence. private fun sendNotification(triggeringGeofences: List) { val requestId = "" 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 1591f9c1e..7bc302e81 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,6 +1,7 @@ package com.udacity.project4.locationreminders.savereminder import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -16,6 +17,10 @@ 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.reminderslist.ReminderDataItem +import com.udacity.project4.locationreminders.savereminder.selectreminderlocation.SelectLocationFragment +import com.udacity.project4.locationreminders.savereminder.selectreminderlocation.SelectLocationFragment.Companion.ARGUMENTS +import com.udacity.project4.utils.getNavigationResult import com.udacity.project4.utils.setDisplayHomeAsUpEnabled import org.koin.android.ext.android.inject @@ -28,13 +33,21 @@ class SaveReminderFragment : BaseFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + Log.d(SelectLocationFragment.TAG, "SaveReminderFragment.onCreateView().") + val layoutId = R.layout.fragment_save_reminder binding = DataBindingUtil.inflate(inflater, layoutId, container, false) setDisplayHomeAsUpEnabled(true) binding.viewModel = _viewModel requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) -// setHasOptionsMenu(true) + getNavigationResult(ARGUMENTS)?.observe(viewLifecycleOwner) { result -> + val triple = result as Triple + _viewModel.reminderSelectedLocationStr.value = triple.component1() + _viewModel.latitude.value = triple.component2() + _viewModel.longitude.value = triple.component3() + } + val menuHost: MenuHost = requireActivity() addMenu(menuHost) @@ -44,6 +57,7 @@ class SaveReminderFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = this + binding.selectLocation.setOnClickListener { // Navigate to another fragment to get the user location val directions = SaveReminderFragmentDirections @@ -53,22 +67,28 @@ class SaveReminderFragment : BaseFragment() { binding.saveReminder.setOnClickListener { val title = _viewModel.reminderTitle.value - val description = _viewModel.reminderDescription + val description = _viewModel.reminderDescription.value val location = _viewModel.reminderSelectedLocationStr.value - val latitude = _viewModel.latitude + val latitude = _viewModel.latitude.value val longitude = _viewModel.longitude.value - - // TODO: Use the user entered reminder details to: - // 1) Add a geofencing request - // 2) Save the reminder to the local db + // TODO: Use the user entered reminder details to: + // [DOING] 1) Add a geofencing request + // [DONE] 2) Save the reminder to the local db + val dataItem = ReminderDataItem( + title = title, + description = description, + location = location, + latitude = latitude, + longitude = longitude + ) + _viewModel.validateAndSaveReminder(dataItem) } } 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 + // 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 { @@ -95,15 +115,4 @@ class SaveReminderFragment : BaseFragment() { // Make sure to clear the view model after destroy, as it's a single view model. _viewModel.onClear() } - - /* - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - requireActivity().onBackPressedDispatcher.onBackPressed() - } - } - return super.onOptionsItemSelected(item) - } - */ } \ 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 c3cd9f769..126dce151 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 @@ -15,12 +15,16 @@ 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() + val reminderTitle = MutableLiveData() + val reminderDescription = MutableLiveData() + val reminderSelectedLocationStr = MutableLiveData() + val selectedPOI = MutableLiveData() + val latitude = MutableLiveData() + val longitude = MutableLiveData() + + init { + reminderDescription.value = "" + } /** * Clear the live data objects to start fresh next time the view model gets called 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 fe282f2f6..cc923121f 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 @@ -21,6 +21,7 @@ import androidx.databinding.DataBindingUtil import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.Granularity import com.google.android.gms.location.LocationCallback @@ -40,6 +41,7 @@ import com.udacity.project4.databinding.FragmentSelectLocationBinding import com.udacity.project4.locationreminders.savereminder.SaveReminderViewModel import com.udacity.project4.utils.permissionGranted import com.udacity.project4.utils.setDisplayHomeAsUpEnabled +import com.udacity.project4.utils.setNavigationResult import com.udacity.project4.utils.showRequestPermissionRationale import com.udacity.project4.utils.toast import kotlinx.coroutines.delay @@ -54,11 +56,12 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { //-------------------------------------------------- companion object { - private val TAG = SelectLocationFragment::class.java.simpleName + val TAG = SelectLocationFragment::class.java.simpleName private const val UPDATE_INTERVAL = (10 * 1000).toLong() // 10 secs private const val FASTEST_INTERVAL: Long = 2000 // 2 secs private const val FINE = Manifest.permission.ACCESS_FINE_LOCATION private const val BACK = Manifest.permission.ACCESS_BACKGROUND_LOCATION + const val ARGUMENTS = "args" } // Use Koin to get the view model of the SaveReminder @@ -93,8 +96,9 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { // The last location in the list is the newest. val location = locationList.last() goToLocation(location.latitude, location.longitude) - // TODO: Here I need to call method onLocationSelected() - onLocationSelected() + + // Inform user to select a POI (Point of Interest) + parent.toast(R.string.select_poi) } } } @@ -112,7 +116,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { // permissions dialog. Save the return value, an instance of ActivityResultLauncher. You can use // either a val, as shown in this snippet, or a lateinit var in your onAttach() or onCreate() // method. - val requestFinePermissionLauncher = registerForActivityResult( + private val requestFinePermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> Log.d(TAG, "requestFinePermissionLauncher() -> isGranted: $isGranted") @@ -128,13 +132,13 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } } - val requestBackPermissionLauncher = registerForActivityResult( + private val requestBackPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> Log.d(TAG, "requestBackPermissionLauncher() -> isGranted: $isGranted") if (isGranted) { lifecycleScope.launch { - delay(3000) + delay(2000) checkGpsEnabled() } } else { @@ -146,7 +150,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { * Source: * https://www.tothenew.com/blog/android-katha-onactivityresult-is-deprecated-now-what/ */ - var activityResultLauncher = registerForActivityResult(ActivityResultContracts + private var activityResultLauncher = registerForActivityResult(ActivityResultContracts .StartActivityForResult()) { Log.d(TAG, "activityResultLauncher().") if (isGpsEnabled()) { @@ -357,7 +361,6 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { //-------------------------------------------------- // Maps Methods //-------------------------------------------------- - //#region MapsMethods private fun startMapFeature() { Log.d(TAG, "startMapFeature().") @@ -383,8 +386,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { map?.let { it.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney")) it.moveCamera(CameraUpdateFactory.newLatLng(sydney)) - - setMapLongClick(it) +// setMapLongClick(it) // Put a marker to location that the user selected setPoiClick(it) // Add style to the map @@ -392,8 +394,11 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } } + /* private fun setMapLongClick(map: GoogleMap) { + Log.d(TAG, "setMapLongClick().") map.setOnMapLongClickListener { latLng -> + Log.d(TAG, "setMapLongClick() -> map.setOnMapLongClickListener.") // A Snippet is Additional text that's displayed below the title. val snippet = String.format( Locale.getDefault(), @@ -409,8 +414,15 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { .snippet(snippet) .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)) ) + + onLocationSelected( + location = "", + latitude = latLng.latitude, + longitude = latLng.longitude + ) } } + */ private fun setPoiClick(map: GoogleMap) { map.setOnPoiClickListener { poi -> @@ -419,7 +431,31 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { .position(poi.latLng) .title(poi.name) ) - poiMarker?.showInfoWindow() +// poiMarker?.showInfoWindow() + + val latLng = poi.latLng + val poiLocation = poi.name + + val snippet = String.format( + Locale.getDefault(), + "Lat: %1$.5f, Long: %2$.5f", + latLng.latitude, latLng.longitude + ) + + map.addMarker( + MarkerOptions() + .position(latLng) +// .title(getString(R.string.dropped_pin)) + .title(poiLocation) + .snippet(snippet) + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)) + ) + + onLocationSelected( + location = poiLocation, + latitude = latLng.latitude, + longitude = latLng.longitude + ) } } @@ -439,7 +475,6 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { Log.e(TAG, "Can't find style. Error: ", e) } } - //#endregion //-------------------------------------------------- // Location Methods @@ -509,9 +544,27 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { ) } - private fun onLocationSelected() { + private fun onLocationSelected(location: String, latitude: Double, longitude: Double) { + Log.d(TAG, "onLocationSelected() -> location: $location, latitude: $latitude, longitude: $longitude") // 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. + parent.toast(R.string.poi_selected) + lifecycleScope.launch { + delay(2000) + 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/utils/Extensions.kt b/starter/app/src/main/java/com/udacity/project4/utils/Extensions.kt index c3b0cfcfb..be66c0b6a 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 @@ -1,12 +1,9 @@ package com.udacity.project4.utils -import android.Manifest import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.app.AlertDialog import android.content.Context import android.content.pm.PackageManager -import android.util.Log import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity @@ -14,10 +11,12 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment 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.location.GeofenceStatusCodes +import com.udacity.project4.R import com.udacity.project4.base.BaseRecyclerViewAdapter -import com.udacity.project4.locationreminders.savereminder.selectreminderlocation.SelectLocationFragment /** * Extension function to setup the RecyclerView. @@ -71,7 +70,7 @@ fun View.fadeOut() { } fun Context.toast(textId: Int) { - Toast.makeText(this, textId, Toast.LENGTH_SHORT).show() + Toast.makeText(this, textId, Toast.LENGTH_LONG).show() } fun Context.toast(text: String) { @@ -97,3 +96,33 @@ fun FragmentActivity.showRequestPermissionRationale(permission: String): Boolean 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) + +/** + * 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) + } +} \ 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..c504a48fe 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 @@ -15,8 +15,7 @@ import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem private const val NOTIFICATION_CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel" fun sendNotification(context: Context, reminderDataItem: ReminderDataItem) { - val notificationManager = context - .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + 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 diff --git a/starter/app/src/main/res/values/strings.xml b/starter/app/src/main/res/values/strings.xml index dc2cb13bb..e4c3a7c07 100644 --- a/starter/app/src/main/res/values/strings.xml +++ b/starter/app/src/main/res/values/strings.xml @@ -14,17 +14,20 @@ 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 @@ -32,6 +35,7 @@ GPS enabled! Tracking now the user location… Please click into \"Allow all the time\" to accept the GPS location permission! Since you didn\'t accept the Tracking permission, we\'re closing the Maps screen. + POI selected! Geofence Entered GeofenceStatus @@ -43,8 +47,10 @@ Welcome Please enter title Please select location + Unknown error: the Geofence service is not available now + 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/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 From b088df2241b501f9c4c24cdf39e26c8d32e4ac4e Mon Sep 17 00:00:00 2001 From: Rodrigo Cericatto Konzen Date: Sat, 29 Apr 2023 00:13:54 -0400 Subject: [PATCH 11/36] Geofences added and Notification implemented --- .../src/debug/res/values/google_maps_api.xml | 2 +- .../main/java/com/udacity/project4/MyApp.kt | 2 +- .../authentication/AuthenticationActivity.kt | 6 +- .../ReminderDescriptionActivity.kt | 6 +- .../locationreminders/RemindersActivity.kt | 3 - .../geofence/GeofenceBroadcastReceiver.kt | 107 ++++----- .../reminderslist/ReminderListFragment.kt | 153 +++++++++---- .../reminderslist/RemindersListViewModel.kt | 21 +- .../savereminder/SaveReminderFragment.kt | 214 +++++++++++++++--- .../savereminder/SaveReminderViewModel.kt | 4 +- .../SelectLocationFragment.kt | 49 ++-- .../com/udacity/project4/utils/Extensions.kt | 18 +- .../project4/utils/NotificationUtils.kt | 74 ++++-- .../app/src/main/res/drawable/map_small.xml | 10 + .../main/res/layout/fragment_reminders.xml | 1 + .../app/src/main/res/layout/it_reminder.xml | 1 - starter/app/src/main/res/values/strings.xml | 6 +- 17 files changed, 476 insertions(+), 201 deletions(-) create mode 100644 starter/app/src/main/res/drawable/map_small.xml 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 b907946b1..141af24c0 100644 --- a/starter/app/src/debug/res/values/google_maps_api.xml +++ b/starter/app/src/debug/res/values/google_maps_api.xml @@ -3,5 +3,5 @@ TODO: Before you run your application, you need a Google Maps API key. --> + translatable="false">AIzaSyC6hBgGRJHLlKoosvbR-nPUg0w5Lg3cbWs \ 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 a4e8e5bfb..a89af6429 100644 --- a/starter/app/src/main/java/com/udacity/project4/MyApp.kt +++ b/starter/app/src/main/java/com/udacity/project4/MyApp.kt @@ -12,7 +12,7 @@ import org.koin.core.context.startKoin import org.koin.dsl.module class MyApp : Application() { - + var hasNotificationPermission = true override fun onCreate() { super.onCreate() 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 e963523fb..10c421aa8 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 @@ -28,6 +28,7 @@ class AuthenticationActivity : AppCompatActivity() { // Get a reference to the ViewModel scoped to this Fragment private val viewModel by viewModels() private lateinit var binding: ActivityAuthenticationBinding + private var fakeLogin = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -51,7 +52,10 @@ class AuthenticationActivity : AppCompatActivity() { // binding.authButton.setOnClickListener { // launchSignInFlow() // } - startActivity(Intent(this, RemindersActivity::class.java)) + if (fakeLogin) { + fakeLogin = false + startActivity(Intent(this, RemindersActivity::class.java)) + } } /** 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 c6ea814ad..074d872a7 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,11 +3,13 @@ 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 @@ -21,6 +23,7 @@ class ReminderDescriptionActivity : AppCompatActivity() { // 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) return intent @@ -31,6 +34,7 @@ class ReminderDescriptionActivity : AppCompatActivity() { super.onCreate(savedInstanceState) val layoutId = R.layout.activity_reminder_description binding = DataBindingUtil.setContentView(this, layoutId) - // TODO: Add the implementation of the reminder details + // TODO: Add the implementation of the reminder details. + Log.d(TAG, "ReminderDescriptionActivity.onCreate().") } } \ 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 5eeceb2a5..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,10 +1,7 @@ package com.udacity.project4.locationreminders import android.os.Bundle -import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.fragment.NavHostFragment -import com.firebase.ui.auth.AuthUI import com.udacity.project4.databinding.ActivityRemindersBinding /** 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 107783cd1..5ac3904a6 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 @@ -1,75 +1,62 @@ package com.udacity.project4.locationreminders.geofence -import android.app.NotificationManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.util.Log -import androidx.core.content.ContextCompat -import com.google.android.gms.location.Geofence -import com.google.android.gms.location.GeofencingEvent +import android.widget.Toast +import com.udacity.project4.MyApp import com.udacity.project4.R -import com.udacity.project4.utils.errorMessage +import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem +import com.udacity.project4.locationreminders.savereminder.SaveReminderFragment.Companion.ACTION_GEOFENCE_EVENT +import com.udacity.project4.locationreminders.savereminder.SaveReminderFragment.Companion.GEO_FENCE_ID +import com.udacity.project4.locationreminders.savereminder.SaveReminderFragment.Companion.GEO_FENCE_LAT +import com.udacity.project4.locationreminders.savereminder.SaveReminderFragment.Companion.GEO_FENCE_LNG +import com.udacity.project4.utils.TAG +import com.udacity.project4.utils.sendNotification /** - * 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() { - private val TAG = "GeofenceReceiver" - - override fun onReceive(context: Context, intent: Intent) { - // TODO: Implement the onReceive method to receive the geofencing events at the background. - val geofencingEvent = GeofencingEvent.fromIntent(intent) - - if (geofencingEvent != null) { - if (geofencingEvent.hasError()) { - val errorMessage = errorMessage(context, geofencingEvent.errorCode) - Log.e(TAG, errorMessage) - return - } - } - - /* - if (geofencingEvent != null) { - if (geofencingEvent.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) { - Log.v(TAG, context.getString(R.string.geofence_entered)) - - val fenceId = when { - geofencingEvent.triggeringGeofences!!.isNotEmpty() -> - geofencingEvent.triggeringGeofences!![0].requestId - else -> { - Log.e(TAG, "No Geofence Trigger Found! Abort mission!") - return - } - } - // Check geofence against the constants listed in GeofenceUtil.kt to see if the - // user has entered any of the locations we track for geofences. - val foundIndex = GeofencingConstants.LANDMARK_DATA.indexOfFirst { - it.id == fenceId - } - - // Unknown Geofences aren't helpful to us - if ( -1 == foundIndex ) { - Log.e(TAG, "Unknown Geofence: Abort Mission") - return - } - - val notificationManager = ContextCompat.getSystemService( - context, - NotificationManager::class.java - ) as NotificationManager - notificationManager.sendGeofenceEnteredNotification( - context, foundIndex - ) - } - } - */ - } + override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "GeofenceBroadcastReceiver.onReceive().") + if (intent.action == ACTION_GEOFENCE_EVENT) { + val extras = intent.extras + var id = "" + var lat = 0.0 + var lng = 0.0 + if (extras != null) { + id = extras.getString(GEO_FENCE_ID).toString() + lat = extras.getDouble(GEO_FENCE_LAT) + lng = extras.getDouble(GEO_FENCE_LNG) + } + callNotification(context, + ReminderDataItem( + title = id, + description = id, + location = id, + latitude = lat, + longitude = lng + ) + ) + } + } + + private fun callNotification(context: Context, reminderDataItem: ReminderDataItem) { + val hasPermission = (context.applicationContext as MyApp).hasNotificationPermission + if (hasPermission) { + 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 fee6971e1..5c71bca30 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,6 +1,9 @@ package com.udacity.project4.locationreminders.reminderslist +import android.Manifest +import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -8,51 +11,75 @@ 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.getFakeReminderItem +import com.udacity.project4.utils.permissionGranted +import com.udacity.project4.utils.sendNotification 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 -> [1]") + parent.toast(R.string.notification_permission_granted) + } else { + Log.d(TAG, "ReminderListFragment.requestPermissionLauncher -> [2]") + parent.toast(R.string.no_notification_permission) +// AuthUI.getInstance().signOut(requireContext()) + parent.onBackPressedDispatcher.onBackPressed() + } + } + + //-------------------------------------------------- + // Lifecycle Methods + //-------------------------------------------------- override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = DataBindingUtil.inflate(inflater, - R.layout.fragment_reminders, container, false - ) + val layoutId = R.layout.fragment_reminders + binding = DataBindingUtil.inflate(inflater, layoutId, container, false) binding.viewModel = _viewModel - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) - -// setHasOptionsMenu(true) - val menuHost: MenuHost = requireActivity() - addMenu(menuHost) + init() - setDisplayHomeAsUpEnabled(false) - setTitle(getString(R.string.app_name)) - - binding.refreshLayout.setOnRefreshListener { _viewModel.loadReminders() } 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 { @@ -60,28 +87,37 @@ class ReminderListFragment : BaseFragment() { } } - /** - * 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() - } - } - override fun onResume() { super.onResume() + 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() { + Log.d(TAG, "ReminderListFragment.navigateToAddReminder().") // Use the navigationCommand live data to navigate between the fragments _viewModel.navigationCommand.postValue( NavigationCommand.To( @@ -91,12 +127,50 @@ class ReminderListFragment : BaseFragment() { } private fun setupRecyclerView() { + Log.d(TAG, "ReminderListFragment.setupRecyclerView().") val adapter = RemindersListAdapter {} // Setup the recycler view using the extension function binding.reminderssRecyclerView.setup(adapter) } + 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!") + sendNotification(parent, getFakeReminderItem()) + } +// shouldShowRequestPermissionRationale(POST) -> { +// } + else -> { + // The registered ActivityResultCallback gets the result of this request. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch(POST) + } + } + } + } + + //-------------------------------------------------- + // 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 @@ -109,35 +183,14 @@ class ReminderListFragment : BaseFragment() { when (menuItem.itemId) { R.id.logout -> { logoutClicked = true - requireActivity().onBackPressedDispatcher.onBackPressed() + parent.onBackPressedDispatcher.onBackPressed() } android.R.id.home -> { - requireActivity().onBackPressedDispatcher.onBackPressed() + parent.onBackPressedDispatcher.onBackPressed() } } return true } }, viewLifecycleOwner, Lifecycle.State.RESUMED) } - - /* - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - // Display logout as menu item - inflater.inflate(R.menu.main_menu, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.logout -> { - logoutClicked = true - requireActivity().onBackPressedDispatcher.onBackPressed() - } - android.R.id.home -> { - requireActivity().onBackPressedDispatcher.onBackPressed() - } - } - return super.onOptionsItemSelected(item) - } - */ } \ No newline at end of file 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 7bc302e81..6fa93c383 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,5 +1,10 @@ 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.util.Log import android.view.LayoutInflater @@ -12,34 +17,85 @@ import androidx.activity.OnBackPressedCallback 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.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.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 import com.udacity.project4.locationreminders.savereminder.selectreminderlocation.SelectLocationFragment.Companion.ARGUMENTS +import com.udacity.project4.utils.LandmarkDataObject +import com.udacity.project4.utils.TAG import com.udacity.project4.utils.getNavigationResult +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 +import java.util.concurrent.TimeUnit class SaveReminderFragment : BaseFragment() { + companion object { + internal const val ACTION_GEOFENCE_EVENT = "SaveReminderFragment.ACTION_GEOFENCE_EVENT" + const val GEOFENCE_RADIUS_IN_METERS = 100f + val GEOFENCE_EXPIRATION_IN_MILLISECONDS: Long = TimeUnit.HOURS.toMillis(1) + 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 geofencingClient: GeofencingClient + private lateinit var parent: FragmentActivity + private lateinit var geofenceData: LandmarkDataObject + + // 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) { + intentFlagTypeUpdateCurrent = PendingIntent.FLAG_IMMUTABLE + } + PendingIntent.getBroadcast(requireContext(), 0, intent, intentFlagTypeUpdateCurrent) + } + + //-------------------------------------------------- + // Lifecycle Methods + //-------------------------------------------------- override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - Log.d(SelectLocationFragment.TAG, "SaveReminderFragment.onCreateView().") + Log.d(TAG, "SaveReminderFragment.onCreateView().") val layoutId = R.layout.fragment_save_reminder binding = DataBindingUtil.inflate(inflater, layoutId, container, false) - setDisplayHomeAsUpEnabled(true) binding.viewModel = _viewModel - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) + init() + + return binding.root + } + + private fun init() { + Log.d(TAG, "SaveReminderFragment.init().") + setDisplayHomeAsUpEnabled(true) + parent = requireActivity() + parent.onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) getNavigationResult(ARGUMENTS)?.observe(viewLifecycleOwner) { result -> val triple = result as Triple @@ -48,16 +104,17 @@ class SaveReminderFragment : BaseFragment() { _viewModel.longitude.value = triple.component3() } - val menuHost: MenuHost = requireActivity() + val menuHost: MenuHost = parent addMenu(menuHost) - return binding.root + geofencingClient = LocationServices.getGeofencingClient(parent) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.lifecycleOwner = this + Log.d(TAG, "SaveReminderFragment.onViewCreated().") + binding.lifecycleOwner = this binding.selectLocation.setOnClickListener { // Navigate to another fragment to get the user location val directions = SaveReminderFragmentDirections @@ -65,26 +122,131 @@ class SaveReminderFragment : BaseFragment() { _viewModel.navigationCommand.value = NavigationCommand.To(directions) } + // Use the user entered reminder details to: + // 1) Add a geofencing request. + // 2) Save the reminder to the local db. binding.saveReminder.setOnClickListener { - val title = _viewModel.reminderTitle.value - val description = _viewModel.reminderDescription.value + Log.d(TAG, "SaveReminderFragment.onViewCreated() -> saveReminder.setOnClickListener.") val location = _viewModel.reminderSelectedLocationStr.value - val latitude = _viewModel.latitude.value - val longitude = _viewModel.longitude.value - // TODO: Use the user entered reminder details to: - // [DOING] 1) Add a geofencing request - // [DONE] 2) Save the reminder to the local db - val dataItem = ReminderDataItem( - title = title, - description = description, - location = location, - latitude = latitude, - longitude = longitude - ) - _viewModel.validateAndSaveReminder(dataItem) + val lat = _viewModel.latitude.value + val lng = _viewModel.longitude.value + saveReminderOnDatabase(location, lat, lng) + } + } + + override fun onDestroy() { + super.onDestroy() + // Make sure to clear the view model after destroy, as it's a single view model. + _viewModel.onClear() + } + + //-------------------------------------------------- + // Database Methods + //-------------------------------------------------- + + private fun saveReminderOnDatabase(location: String?, lat: Double?, lng: Double?) { + Log.d(TAG, "SaveReminderFragment.saveReminderOnDatabase().") + val title = _viewModel.reminderTitle.value + val description = _viewModel.reminderDescription.value + val dataItem = ReminderDataItem( + title = title, + description = description, + location = location, + latitude = lat, + longitude = lng + ) + val validationOk = _viewModel.validateAndSaveReminder(dataItem) + if (validationOk) { + if (location != null && lat != null && lng != null) { + addGeoFence(location, lat, lng) + } else { + Log.e(TAG, "Couldn't create a Geofence.") + } } } + //-------------------------------------------------- + // GeoFence Methods + //-------------------------------------------------- + + /** + * Adds a GeoFence for the current clue if needed, and removes any existing GeoFence. This + * method should be called after the user has granted the location permission. If there are + * no more GeoFences, we remove the GeoFence and let the viewModel know that the ending hint + * is now "active." + */ + private fun addGeoFence(location: String, lat: Double, lng: Double) { + Log.d(TAG, "SaveReminderFragment.addGeoFence().") + // Build the Geofence Object + val currentGeofenceData = LandmarkDataObject(location, LatLng(lat, lng)) + 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() + + // First, remove any existing GeoFences that use our pending intent + geofencingClient.removeGeofences(geofencePendingIntent).run { + // Regardless of success/failure of the removal, add the new geofence + addOnCompleteListener { + // Add the new geofence request with the new geofence + if (parent.permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) { + addGeoFenceRequest(geofencingRequest, geofence) + } + } + } + } + + @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, "Adding GeoFence: ${geofence.requestId}") + } + addOnFailureListener { + // Failed to add GeoFences. + parent.toast(R.string.geofences_not_added) + if ((it.message != null)) { + Log.d(TAG, it.message!!) + } + } + } + } + + 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. This geofence gets automatically removed + // after this period of time. + .setExpirationDuration(GEOFENCE_EXPIRATION_IN_MILLISECONDS) + // 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() + } + + //-------------------------------------------------- + // Menu Methods + //-------------------------------------------------- + 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 @@ -94,7 +256,7 @@ class SaveReminderFragment : BaseFragment() { override fun onMenuItemSelected(menuItem: MenuItem): Boolean { when (menuItem.itemId) { android.R.id.home -> { - requireActivity().onBackPressedDispatcher.onBackPressed() + parent.onBackPressedDispatcher.onBackPressed() } } return true @@ -109,10 +271,4 @@ class SaveReminderFragment : BaseFragment() { ) } } - - override fun onDestroy() { - super.onDestroy() - // Make sure to clear the view model after destroy, as it's a single view model. - _viewModel.onClear() - } } \ 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 126dce151..78d6cd244 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 @@ -41,10 +41,12 @@ class SaveReminderViewModel( /** * Validate the entered data then saves the reminder data to the DataSource */ - fun validateAndSaveReminder(reminderData: ReminderDataItem) { + fun validateAndSaveReminder(reminderData: ReminderDataItem) : Boolean { if (validateEnteredData(reminderData)) { saveReminder(reminderData) + return true } + return false } /** 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 cc923121f..e63d8fa6d 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 @@ -39,6 +39,7 @@ 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.TAG import com.udacity.project4.utils.permissionGranted import com.udacity.project4.utils.setDisplayHomeAsUpEnabled import com.udacity.project4.utils.setNavigationResult @@ -56,7 +57,6 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { //-------------------------------------------------- companion object { - val TAG = SelectLocationFragment::class.java.simpleName private const val UPDATE_INTERVAL = (10 * 1000).toLong() // 10 secs private const val FASTEST_INTERVAL: Long = 2000 // 2 secs private const val FINE = Manifest.permission.ACCESS_FINE_LOCATION @@ -90,7 +90,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { private var locationCallback: LocationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { - Log.d(TAG, "locationCallback.onLocationResult().") + Log.d(TAG, "SelectLocationFragment.locationCallback.onLocationResult().") val locationList = locationResult.locations if (locationList.isNotEmpty()) { // The last location in the list is the newest. @@ -119,7 +119,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { private val requestFinePermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> - Log.d(TAG, "requestFinePermissionLauncher() -> isGranted: $isGranted") + Log.d(TAG, "SelectLocationFragment.requestFinePermissionLauncher() -> isGranted: $isGranted") if (isGranted) { // Permission is granted. Continue the action or workflow in your app. checkBackgroundLocationPermission() @@ -135,7 +135,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { private val requestBackPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> - Log.d(TAG, "requestBackPermissionLauncher() -> isGranted: $isGranted") + Log.d(TAG, "SelectLocationFragment.requestBackPermissionLauncher() -> isGranted: $isGranted") if (isGranted) { lifecycleScope.launch { delay(2000) @@ -152,7 +152,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { */ private var activityResultLauncher = registerForActivityResult(ActivityResultContracts .StartActivityForResult()) { - Log.d(TAG, "activityResultLauncher().") + Log.d(TAG, "SelectLocationFragment.activityResultLauncher.") if (isGpsEnabled()) { parent.toast(R.string.gps_enabled) requestTracking() @@ -188,7 +188,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { override fun onPause() { super.onPause() - Log.d(TAG, "onPause().") + Log.d(TAG, "SelectLocationFragment.onPause().") if (parent.permissionGranted(FINE)) { fusedLocationProvider?.removeLocationUpdates(locationCallback) } @@ -196,7 +196,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { override fun onDestroy() { super.onDestroy() - Log.d(TAG, "onDestroy().") + Log.d(TAG, "SelectLocationFragment.onDestroy().") disableDialogs() } @@ -271,11 +271,11 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { //-------------------------------------------------- private fun checkLocationPermission() { - Log.d(TAG, "checkLocationPermission().") + Log.d(TAG, "SelectLocationFragment.checkLocationPermission().") when { parent.permissionGranted(FINE) -> { // You can use the API that requires the permission. - Log.d(TAG, "checkLocationPermission() -> [1]") + Log.d(TAG, "SelectLocationFragment.checkLocationPermission() -> [1]") checkBackgroundLocationPermission() } parent.showRequestPermissionRationale(FINE) -> { @@ -287,7 +287,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { // 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. - Log.d(TAG, "checkLocationPermission() -> [2]") + Log.d(TAG, "SelectLocationFragment.checkLocationPermission() -> [2]") val title = this.getString(R.string.location_permission_needed) val message = this.getString(R.string.location_permission_explanation) AlertDialog.Builder(parent) @@ -303,14 +303,14 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { else -> { // You can directly ask for the permission. // The registered ActivityResultCallback gets the result of this request. - Log.d(TAG, "checkLocationPermission() -> [3]") + Log.d(TAG, "SelectLocationFragment.checkLocationPermission() -> [3]") requestFinePermissionLauncher.launch(FINE) } } } private fun checkBackgroundLocationPermission() { - Log.d(TAG, "checkBackgroundLocationPermission().") + Log.d(TAG, "SelectLocationFragment.checkBackgroundLocationPermission().") when { parent.permissionGranted(BACK) -> { // You can use the API that requires the permission. @@ -339,7 +339,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { * (in order to track the user GPS's location). */ private fun requestBackgroundLocationPermission() { - Log.d(TAG, "requestBackgroundLocationPermission().") + Log.d(TAG, "SelectLocationFragment.requestBackgroundLocationPermission().") val builder = AlertDialog.Builder(parent) builder.setMessage(R.string.please_accept_allow_all_time) .setCancelable(false) @@ -363,7 +363,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { //-------------------------------------------------- private fun startMapFeature() { - Log.d(TAG, "startMapFeature().") + Log.d(TAG, "SelectLocationFragment.startMapFeature().") val mapFragment = childFragmentManager .findFragmentById(R.id.map) as SupportMapFragment mapFragment.getMapAsync(this) @@ -378,7 +378,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { * installed Google Play services and returned to the app. */ override fun onMapReady(googleMap: GoogleMap) { - Log.d(TAG, "onMapReady().") + Log.d(TAG, "SelectLocationFragment.onMapReady().") map = googleMap // Add a marker in Sydney and move the camera @@ -485,24 +485,24 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { * https://stackoverflow.com/a/25175756/1354788 */ private fun checkGpsEnabled() { - Log.d(TAG, "checkGpsEnabled().") + Log.d(TAG, "SelectLocationFragment.checkGpsEnabled().") if (!isGpsEnabled()) { - Log.d(TAG, "checkGpsEnabled() -> [1]") + Log.d(TAG, "SelectLocationFragment.checkGpsEnabled() -> [1]") buildAlertMessageNoGps() } else { - Log.d(TAG, "checkGpsEnabled() -> [2]") + Log.d(TAG, "SelectLocationFragment.checkGpsEnabled() -> [2]") requestTracking() } } private fun isGpsEnabled(): Boolean { - Log.d(TAG, "isGpsEnabled().") + Log.d(TAG, "SelectLocationFragment.isGpsEnabled().") val manager = parent.getSystemService(Context.LOCATION_SERVICE) as LocationManager return manager.isProviderEnabled(LocationManager.GPS_PROVIDER) } private fun buildAlertMessageNoGps() { - Log.d(TAG, "buildAlertMessageNoGps().") + Log.d(TAG, "SelectLocationFragment.buildAlertMessageNoGps().") val builder = AlertDialog.Builder(parent) builder.setMessage(R.string.should_enable_gps) .setCancelable(false) @@ -518,7 +518,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } private fun goToLocation(lat: Double, lng: Double) { - Log.d(TAG, "goToLocation().") + Log.d(TAG, "SelectLocationFragment.goToLocation().") map?.let { val latLng = LatLng(lat, lng) // Zoom to the user location after taking his permission. @@ -529,14 +529,14 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } private fun permissionDeniedFeedback() { - Log.d(TAG, "permissionDeniedFeedback().") + Log.d(TAG, "SelectLocationFragment.permissionDeniedFeedback().") parent.toast(R.string.allow_all_time_did_not_accepted) requireActivity().onBackPressedDispatcher.onBackPressed() } @SuppressLint("MissingPermission") private fun requestTracking() { - Log.d(TAG, "requestTracking().") + Log.d(TAG, "SelectLocationFragment.requestTracking().") fusedLocationProvider?.requestLocationUpdates( locationRequest, locationCallback, @@ -545,7 +545,8 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { } private fun onLocationSelected(location: String, latitude: Double, longitude: Double) { - Log.d(TAG, "onLocationSelected() -> location: $location, latitude: $latitude, longitude: $longitude") + Log.d(TAG, "SelectLocationFragment.onLocationSelected() -> " + + "location: $location, latitude: $latitude, longitude: $longitude") // 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. 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 be66c0b6a..38aff6e88 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 @@ -15,8 +15,12 @@ import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.gms.location.GeofenceStatusCodes +import com.google.android.gms.maps.model.LatLng import com.udacity.project4.R import com.udacity.project4.base.BaseRecyclerViewAdapter +import com.udacity.project4.locationreminders.reminderslist.ReminderDataItem + +const val TAG = "Project4" /** * Extension function to setup the RecyclerView. @@ -125,4 +129,16 @@ fun errorMessage(context: Context, errorCode: Int): String { ) else -> resources.getString(R.string.unknown_geofence_error) } -} \ No newline at end of file +} + +fun getFakeReminderItem() : ReminderDataItem { + return ReminderDataItem( + title = "some title", + description = "some description", + location = "some location", + latitude = -34.0, // Sydney, Australia + longitude = 151.0 // Sydney, Australia + ) +} + +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/NotificationUtils.kt b/starter/app/src/main/java/com/udacity/project4/utils/NotificationUtils.kt index c504a48fe..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,39 +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/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/fragment_reminders.xml b/starter/app/src/main/res/layout/fragment_reminders.xml index 75f545efd..b6a312904 100644 --- a/starter/app/src/main/res/layout/fragment_reminders.xml +++ b/starter/app/src/main/res/layout/fragment_reminders.xml @@ -16,6 +16,7 @@ tools:context=".locationreminders.reminderslist.ReminderListFragment"> diff --git a/starter/app/src/main/res/layout/it_reminder.xml b/starter/app/src/main/res/layout/it_reminder.xml index 72a0e97b6..5111bf390 100644 --- a/starter/app/src/main/res/layout/it_reminder.xml +++ b/starter/app/src/main/res/layout/it_reminder.xml @@ -26,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" diff --git a/starter/app/src/main/res/values/strings.xml b/starter/app/src/main/res/values/strings.xml index e4c3a7c07..f66e2f746 100644 --- a/starter/app/src/main/res/values/strings.xml +++ b/starter/app/src/main/res/values/strings.xml @@ -43,11 +43,15 @@ Location Reminder Please select title Please select location - Problem in adding geofences. Welcome Please enter title Please select location Unknown error: the Geofence service is not available now + Clue Geofence added. + Problem in adding geofences. + + Since the Notification permission was denied, the app will logout you from the system. + Notification permission granted! From 81ca5d27360b7b1711b3af16167f930d443431be Mon Sep 17 00:00:00 2001 From: Rodrigo Cericatto Konzen Date: Sat, 29 Apr 2023 00:42:21 -0400 Subject: [PATCH 12/36] Save button added on SelectLocationFragment --- .../reminderslist/ReminderListFragment.kt | 2 +- .../SelectLocationFragment.kt | 36 +++++++++++++------ .../res/layout/fragment_select_location.xml | 14 ++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) 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 5c71bca30..4c8d065e1 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 @@ -139,7 +139,7 @@ class ReminderListFragment : BaseFragment() { parent.permissionGranted(POST) -> { // You can use the API that requires the permission. Log.d(TAG, "ReminderListFragment.requestPermissionListener() -> Permission granted!") - sendNotification(parent, getFakeReminderItem()) +// sendNotification(parent, getFakeReminderItem()) } // shouldShowRequestPermissionRationale(POST) -> { // } 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 e63d8fa6d..8a65ca7a9 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 @@ -74,6 +74,9 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { private lateinit var binding: FragmentSelectLocationBinding private lateinit var parent: FragmentActivity + private lateinit var latLng : LatLng + private var poiLocation = "" + //-------------------------------------------------- // GPS Location Attributes //-------------------------------------------------- @@ -173,6 +176,12 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { binding.viewModel = _viewModel binding.lifecycleOwner = this + init() + + return binding.root + } + + private fun init() { parent = requireActivity() requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressHandler) @@ -183,7 +192,13 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { initMenus() checkLocationPermission() - return binding.root + binding.saveButton.setOnClickListener { + onLocationSelected( + location = poiLocation, + latitude = latLng.latitude, + longitude = latLng.longitude + ) + } } override fun onPause() { @@ -433,8 +448,8 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { ) // poiMarker?.showInfoWindow() - val latLng = poi.latLng - val poiLocation = poi.name + latLng = poi.latLng + poiLocation = poi.name val snippet = String.format( Locale.getDefault(), @@ -451,11 +466,7 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)) ) - onLocationSelected( - location = poiLocation, - latitude = latLng.latitude, - longitude = latLng.longitude - ) + binding.saveButton.visibility = View.VISIBLE } } @@ -544,12 +555,15 @@ class SelectLocationFragment : BaseFragment(), OnMapReadyCallback { ) } + /** + * 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(location: String, latitude: Double, longitude: Double) { Log.d(TAG, "SelectLocationFragment.onLocationSelected() -> " + "location: $location, latitude: $latitude, longitude: $longitude") - // 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. + parent.toast(R.string.poi_selected) lifecycleScope.launch { delay(2000) 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 c7e017c48..da8794970 100644 --- a/starter/app/src/main/res/layout/fragment_select_location.xml +++ b/starter/app/src/main/res/layout/fragment_select_location.xml @@ -1,8 +1,10 @@ + @@ -20,5 +22,17 @@ android:layout_height="match_parent" tools:layout_editor_absoluteX="64dp" tools:layout_editor_absoluteY="16dp" /> + +