diff --git a/GDGFinderFinal/LICENSE b/GDGFinderFinal/LICENSE new file mode 100755 index 0000000..261eeb9 --- /dev/null +++ b/GDGFinderFinal/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/GDGFinderFinal/README.md b/GDGFinderFinal/README.md new file mode 100755 index 0000000..351f510 --- /dev/null +++ b/GDGFinderFinal/README.md @@ -0,0 +1 @@ +# andfun-kotlin-gdg-finder \ No newline at end of file diff --git a/GDGFinderFinal/app/.gitignore b/GDGFinderFinal/app/.gitignore new file mode 100755 index 0000000..796b96d --- /dev/null +++ b/GDGFinderFinal/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/GDGFinderFinal/app/build.gradle b/GDGFinderFinal/app/build.gradle new file mode 100755 index 0000000..2ea67d4 --- /dev/null +++ b/GDGFinderFinal/app/build.gradle @@ -0,0 +1,96 @@ +/* + * Copyright 2018, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: "androidx.navigation.safeargs" + +android { + compileSdkVersion 28 + dataBinding { + enabled = true + } + defaultConfig { + applicationId "com.example.android.gdgfinder" + minSdkVersion 19 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + androidExtensions { + experimental = true + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$version_kotlin" + + // Constraint Layout + implementation "androidx.constraintlayout:constraintlayout:$version_constraint_layout" + + // ViewModel and LiveData + implementation "androidx.lifecycle:lifecycle-extensions:$version_lifecycle_extensions" + // use viewModelScope from lifecycle-viewmodel-ktx + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-alpha03" + + // Navigation + implementation "android.arch.navigation:navigation-fragment-ktx:$version_navigation" + implementation "android.arch.navigation:navigation-ui-ktx:$version_navigation" + + // Core with Ktx + implementation "androidx.core:core-ktx:$version_core" + + // Moshi + implementation "com.squareup.moshi:moshi:$version_moshi" + implementation "com.squareup.moshi:moshi-kotlin:$version_moshi" + + // Retrofit + implementation "com.squareup.retrofit2:retrofit:$version_retrofit" + implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit" + + // Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_kotlin_coroutines" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version_kotlin_coroutines" + + // Retrofit Coroutines Support + implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$version_retrofit_coroutines_adapter" + + // Glide + implementation "com.github.bumptech.glide:glide:$version_glide" + + // RecyclerView + implementation "androidx.recyclerview:recyclerview:$version_recyclerview" + + // material design components + implementation 'com.google.android.material:material:1.1.0-alpha04' + + // Client for retrieving location + implementation "com.google.android.gms:play-services-location:16.0.0" +} \ No newline at end of file diff --git a/GDGFinderFinal/app/proguard-rules.pro b/GDGFinderFinal/app/proguard-rules.pro new file mode 100755 index 0000000..f1b4245 --- /dev/null +++ b/GDGFinderFinal/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/GDGFinderFinal/app/src/main/AndroidManifest.xml b/GDGFinderFinal/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..22d85cb --- /dev/null +++ b/GDGFinderFinal/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/BindingAdapters.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/BindingAdapters.kt new file mode 100755 index 0000000..55ccfb1 --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/BindingAdapters.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder + +import android.view.LayoutInflater +import android.view.View +import androidx.cardview.widget.CardView +import androidx.databinding.BindingAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.android.gdgfinder.network.GdgChapter +import com.example.android.gdgfinder.search.GdgListAdapter +import com.google.android.material.chip.ChipGroup + +/** + * When there is no Mars property data (data is null), hide the [RecyclerView], otherwise show it. + */ +@BindingAdapter("listData") +fun bindRecyclerView(recyclerView: RecyclerView, data: List?) { + val adapter = recyclerView.adapter as GdgListAdapter + adapter.submitList(data) { + // scroll the list to the top after the diffs are calculated and posted + recyclerView.scrollToPosition(0) + } +} + +@BindingAdapter("showOnlyWhenEmpty") +fun View.showOnlyWhenEmpty(data: List?) { + visibility = when { + data == null || data.isEmpty() -> View.VISIBLE + else -> View.GONE + } +} \ No newline at end of file diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/MainActivity.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/MainActivity.kt new file mode 100755 index 0000000..fd1b031 --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/MainActivity.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder + +import android.os.Bundle +import android.transition.TransitionManager +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.databinding.DataBindingUtil +import androidx.navigation.NavDestination +import androidx.navigation.findNavController +import androidx.navigation.ui.NavigationUI.navigateUp +import androidx.navigation.ui.NavigationUI.setupWithNavController +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController +import com.example.android.gdgfinder.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + + lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + + setupNavigation() + + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + } + + /** + * Called when the hamburger menu or back button are pressed on the Toolbar + * + * Delegate this to Navigation. + */ + override fun onSupportNavigateUp() = navigateUp(findNavController(R.id.nav_host_fragment), binding.drawerLayout) + + /** + * Setup Navigation for this Activity + */ + private fun setupNavigation() { + // first find the nav controller + val navController = findNavController(R.id.nav_host_fragment) + + setSupportActionBar(binding.toolbar) + + // then setup the action bar, tell it about the DrawerLayout + setupActionBarWithNavController(navController, binding.drawerLayout) + + + // finally setup the left drawer (called a NavigationView) + binding.navigationView.setupWithNavController(navController) + + navController.addOnDestinationChangedListener { _, destination: NavDestination, _ -> + val toolBar = supportActionBar ?: return@addOnDestinationChangedListener + when(destination.id) { + R.id.home -> { + toolBar.setDisplayShowTitleEnabled(false) + binding.heroImage.visibility = View.VISIBLE + } + else -> { + toolBar.setDisplayShowTitleEnabled(true) + binding.heroImage.visibility = View.GONE + } + } + } + } +} diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/add/AddGdgFragment.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/add/AddGdgFragment.kt new file mode 100755 index 0000000..570721b --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/add/AddGdgFragment.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.add + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.example.android.gdgfinder.R +import com.example.android.gdgfinder.databinding.AddGdgFragmentBinding +import com.google.android.material.snackbar.Snackbar + +class AddGdgFragment : Fragment() { + + private val viewModel: AddGdgViewModel by lazy { + ViewModelProviders.of(this).get(AddGdgViewModel::class.java) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val binding = AddGdgFragmentBinding.inflate(inflater) + + // Allows Data Binding to Observe LiveData with the lifecycle of this Fragment + binding.setLifecycleOwner(this) + + binding.viewModel = viewModel + + viewModel.showSnackBarEvent.observe(this, Observer { + if (it == true) { // Observed state is true. + Snackbar.make( + activity!!.findViewById(android.R.id.content), + getString(R.string.application_submitted), + Snackbar.LENGTH_SHORT // How long to display the message. + ).show() + viewModel.doneShowingSnackbar() + binding.button.contentDescription=getString(R.string.submitted) + binding.button.text=getString(R.string.done) + } + }) + + setHasOptionsMenu(true) + return binding.root + } +} diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/add/AddGdgViewModel.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/add/AddGdgViewModel.kt new file mode 100755 index 0000000..46520c1 --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/add/AddGdgViewModel.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.add + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel; + +class AddGdgViewModel : ViewModel() { + + /** + * Request a toast by setting this value to true. + * + * This is private because we don't want to expose setting this value to the Fragment. + */ + private var _showSnackbarEvent = MutableLiveData() + + /** + * If this is true, immediately `show()` a toast and call `doneShowingSnackbar()`. + */ + val showSnackBarEvent: LiveData + get() = _showSnackbarEvent + + /** + * Call this immediately after calling `show()` on a toast. + * + * It will clear the toast request, so if the user rotates their phone it won't show a duplicate + * toast. + */ + fun doneShowingSnackbar() { + _showSnackbarEvent.value = null + } + + fun onSubmitApplication() { + _showSnackbarEvent.value = true + + } +} diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/home/HomeFragment.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/home/HomeFragment.kt new file mode 100755 index 0000000..bbbabc9 --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/home/HomeFragment.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.navigation.fragment.findNavController +import com.example.android.gdgfinder.R +import com.example.android.gdgfinder.databinding.HomeFragmentBinding + +class HomeFragment : Fragment() { + + companion object { + fun newInstance() = HomeFragment() + } + + private lateinit var viewModel: HomeViewModel + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val binding = HomeFragmentBinding.inflate(inflater) + viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java) + + binding.viewModel = viewModel + + viewModel.navigateToSearch.observe(viewLifecycleOwner, + Observer { navigate -> + if(navigate) { + val navController = findNavController() + navController.navigate(R.id.action_homeFragment_to_gdgListFragment) + viewModel.onNavigatedToSearch() + } + }) + + return binding.root + } +} diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/home/HomeViewModel.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/home/HomeViewModel.kt new file mode 100755 index 0000000..5dbf65c --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/home/HomeViewModel.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.home + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class HomeViewModel : ViewModel() { + + private val _navigateToSearch = MutableLiveData() + val navigateToSearch: LiveData + get() = _navigateToSearch + + fun onFabClicked() { + _navigateToSearch.value = true + } + + fun onNavigatedToSearch() { + _navigateToSearch.value = false + } +} \ No newline at end of file diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/network/GdgApiService.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/network/GdgApiService.kt new file mode 100755 index 0000000..d345524 --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/network/GdgApiService.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.network + +import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import kotlinx.coroutines.Deferred +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.http.GET + +// The alternative URL is for a server with a recent snapshot. If you are having problems +// with the given URL (app crashing), use the alternative. +//private const val BASE_URL = "https://android-kotlin-fun-mars-server.appspot.com/" +private const val BASE_URL = "https://developers.google.com/community/gdg/directory/" + +interface GdgApiService { + //@GET("gdg-directory.json") + @GET("directory.json") + + fun getChapters(): + // The Coroutine Call Adapter allows us to return a Deferred, a Job with a result + Deferred +} + +private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + +private val retrofit = Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(CoroutineCallAdapterFactory()) + .baseUrl(BASE_URL) + .build() + +object GdgApi { + val retrofitService: GdgApiService by lazy { retrofit.create(GdgApiService::class.java) } +} diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/network/GdgChapter.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/network/GdgChapter.kt new file mode 100755 index 0000000..c15e06f --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/network/GdgChapter.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.network + +import android.os.Parcelable +import com.squareup.moshi.Json +import kotlinx.android.parcel.Parcelize + + +@Parcelize +data class GdgChapter( + @Json(name = "chapter_name") val name: String, + @Json(name = "cityarea") val city: String, + val country: String, + val region: String, + val website: String, + val geo: LatLong + ): Parcelable + +@Parcelize +data class LatLong( + val lat: Double, + @Json(name = "lng") + val long: Double +) : Parcelable + +@Parcelize +data class GdgResponse( + @Json(name = "filters_") val filters: Filter, + @Json(name = "data") val chapters: List +): Parcelable + +@Parcelize +data class Filter( + @Json(name = "region") val regions: List +): Parcelable + +//"chapter_name": "GDG Bordj Bou-Arréridj", +//"cityarea": "Burj Bu Arririj", +//"country": "Algeria", +//"region": "Africa", +//"website": "https://www.meetup.com/GDG-BBA", +//"geo": { +// "lat": 36.06000137, +// "lng": 4.630000114 +//} \ No newline at end of file diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgChapterRepository.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgChapterRepository.kt new file mode 100755 index 0000000..72a4dea --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgChapterRepository.kt @@ -0,0 +1,200 @@ +package com.example.android.gdgfinder.search +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import android.location.Location +import com.example.android.gdgfinder.network.GdgApiService +import com.example.android.gdgfinder.network.GdgChapter +import com.example.android.gdgfinder.network.GdgResponse +import com.example.android.gdgfinder.network.LatLong +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext + +class GdgChapterRepository(gdgApiService: GdgApiService) { + + /** + * A single network request, the results won't change. For this lesson we did not add an offline cache for simplicity + * and the result will be cached in memory. + */ + private val request = gdgApiService.getChapters() + + /** + * An in-progress (or potentially completed) sort, this may be null or cancelled at any time. + * + * If this is non-null, calling await will get the result of the last sorting request. + * + * This will be cancelled whenever location changes, as the old results are no longer valid. + */ + private var inProgressSort: Deferred? = null + + var isFullyInitialized = false + private set + + + /** + * Get the chapters list for a specified filter. + * + * This will be cancel if a new location is sent before the result is available. + * + * This works by first waiting for any previously in-progress sorts, and if a sort has not yet started + * it will start a new sort (which may happen if location is disabled on the device) + */ + suspend fun getChaptersForFilter(filter: String?): List { + val data = sortedData() + return when(filter) { + null -> data.chapters + else -> data.chaptersByRegion.getOrElse(filter) { emptyList() } + } + } + + /** + * Get the filters sorted by distance from the last location. + * + * This will cancel if a new location is sent before the result is available. + * + * This works by first waiting for any previously in-progress sorts, and if a sort has not yet started + * it will start a new sort (which may happen if location is disabled on the device) + */ + suspend fun getFilters(): List = sortedData().filters + + /** + * Get the computed sorted data after it completes, or start a new sort if none are running. + * + * This will always cancel if the location changes while the sort is in progress. + */ + private suspend fun sortedData(): SortedData = withContext(Dispatchers.Main) { + // We need to ensure we're on Dispatchers.Main so that this is not running on multiple Dispatchers and we + // modify the member inProgressSort. + + // Since this was called from viewModelScope, that will always be a simple if check (not expensive), but + // by specifying the dispatcher we can protect against incorrect usage. + + // if there's currently a sort running (or completed) wait for it to complete and return that value + // otherwise, start a new sort with no location (the user has likely not given us permission to use location + // yet) + inProgressSort?.await() ?: doSortData() + } + + /** + * Call this to force a new sort to start. + * + * This will start a new coroutine to perform the sort. Future requests to sorted data can use the deferred in + * [inProgressSort] to get the result of the last sort without sorting the data again. This guards against multiple + * sorts being performed on the same data, which is inefficient. + * + * This will always cancel if the location changes while the sort is in progress. + * + * @return the result of the started sort + */ + private suspend fun doSortData(location: Location? = null): SortedData { + // since we'll need to launch a new coroutine for the sorting use coroutineScope. + // coroutineScope will automatically wait for anything started via async {} or await{} in it's block to + // complete. + val result = coroutineScope { + // launch a new coroutine to do the sort (so other requests can wait for this sort to complete) + val deferred = async { SortedData.from(request.await(), location) } + // cache the Deferred so any future requests can wait for this sort + inProgressSort = deferred + // and return the result of this sort + deferred.await() + } + return result + } + + /** + * Call when location changes. + * + * This will cancel any previous queries, so it's important to re-request the data after calling this function. + * + * @param location the location to sort by + */ + suspend fun onLocationChanged(location: Location) { + // We need to ensure we're on Dispatchers.Main so that this is not running on multiple Dispatchers and we + // modify the member inProgressSort. + + // Since this was called from viewModelScope, that will always be a simple if check (not expensive), but + // by specifying the dispatcher we can protect against incorrect usage. + withContext(Dispatchers.Main) { + isFullyInitialized = true + + // cancel any in progress sorts, their result is not valid anymore. + inProgressSort?.cancel() + + doSortData(location) + } + } + + /** + * Holds data sorted by the distance from the last location. + * + * Note, by convention this class won't sort on the Main thread. This is not a public API and should + * only be called by [doSortData]. + */ + private class SortedData private constructor( + val chapters: List, + val filters: List, + val chaptersByRegion: Map> + ) { + + companion object { + /** + * Sort the data from a [GdgResponse] by the specified location. + * + * @param response the response to sort + * @param location the location to sort by, if null the data will not be sorted. + */ + suspend fun from(response: GdgResponse, location: Location?): SortedData { + return withContext(Dispatchers.Default) { + // this sorting is too expensive to do on the main thread, so do thread confinement here. + val chapters: List = response.chapters.sortByDistanceFrom(location) + // use distinctBy which will maintain the input order - this will have the effect of making + // a filter list sorted by the distance from the current location + val filters: List = chapters.map { it.region } .distinctBy { it } + // group the chapters by region so that filter queries don't require any work + val chaptersByRegion: Map> = chapters.groupBy { it.region } + // return the sorted result + SortedData(chapters, filters, chaptersByRegion) + } + + } + + + /** + * Sort a list of GdgChapter by their distance from the specified location. + * + * @param currentLocation returned list will be sorted by the distance, or unsorted if null + */ + private fun List.sortByDistanceFrom(currentLocation: Location?): List { + currentLocation ?: return this + + return sortedBy { distanceBetween(it.geo, currentLocation)} + } + + /** + * Calculate the distance (in meters) between a LatLong and a Location. + */ + private fun distanceBetween(start: LatLong, currentLocation: Location): Float { + val results = FloatArray(3) + Location.distanceBetween(start.lat, start.long, currentLocation.latitude, currentLocation.longitude, results) + return results[0] + } + } + } +} \ No newline at end of file diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListAdapter.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListAdapter.kt new file mode 100755 index 0000000..d947927 --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListAdapter.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.search + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.android.gdgfinder.network.GdgChapter +import com.example.android.gdgfinder.search.GdgListAdapter.GdgListViewHolder +import com.example.android.gdgfinder.databinding.ListItemBinding + +class GdgListAdapter(val clickListener: GdgClickListener): ListAdapter(DiffCallback){ + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: GdgChapter, newItem: GdgChapter): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame(oldItem: GdgChapter, newItem: GdgChapter): Boolean { + return oldItem == newItem + } + } + + class GdgListViewHolder(private var binding: ListItemBinding): + RecyclerView.ViewHolder(binding.root) { + fun bind(listener: GdgClickListener, gdgChapter: GdgChapter) { + binding.chapter = gdgChapter + binding.clickListener = listener + // This is important, because it forces the data binding to execute immediately, + // which allows the RecyclerView to make the correct view size measurements + binding.executePendingBindings() + } + + companion object { + fun from(parent: ViewGroup): GdgListViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = ListItemBinding.inflate(layoutInflater, parent, false) + return GdgListViewHolder(binding) + } + } + } + + /** + * Part of the RecyclerView adapter, called when RecyclerView needs a new [ViewHolder]. + * + * A ViewHolder holds a view for the [RecyclerView] as well as providing additional information + * to the RecyclerView such as where on the screen it was last drawn during scrolling. + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GdgListViewHolder { + return GdgListViewHolder.from(parent) + + } + + /** + * Part of the RecyclerView adapter, called when RecyclerView needs to show an item. + * + * The ViewHolder passed may be recycled, so make sure that this sets any properties that + * may have been set previously. + */ + override fun onBindViewHolder(holder: GdgListViewHolder, position: Int) { + holder.bind(clickListener, getItem(position)) + } +} + +class GdgClickListener(val clickListener: (chapter: GdgChapter) -> Unit) { + fun onClick(chapter: GdgChapter) = clickListener(chapter) +} diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListFragment.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListFragment.kt new file mode 100755 index 0000000..b08d49d --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListFragment.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.search + +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.example.android.gdgfinder.R +import com.example.android.gdgfinder.databinding.FragmentGdgListBinding +import com.google.android.gms.location.FusedLocationProviderClient +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.material.chip.Chip +import com.google.android.material.snackbar.Snackbar + +private const val LOCATION_PERMISSION_REQUEST = 1 + +private const val LOCATION_PERMISSION = "android.permission.ACCESS_FINE_LOCATION" + +class GdgListFragment : Fragment() { + + + private val viewModel: GdgListViewModel by lazy { + ViewModelProviders.of(this).get(GdgListViewModel::class.java) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val binding = FragmentGdgListBinding.inflate(inflater) + + // Allows Data Binding to Observe LiveData with the lifecycle of this Fragment + binding.setLifecycleOwner(this) + + // Giving the binding access to the OverviewViewModel + binding.viewModel = viewModel + + val adapter = GdgListAdapter(GdgClickListener { chapter -> + val destination = Uri.parse(chapter.website) + startActivity(Intent(Intent.ACTION_VIEW, destination)) + }) + + // Sets the adapter of the RecyclerView + binding.gdgChapterList.adapter = adapter + + viewModel.showNeedLocation.observe(viewLifecycleOwner, object : Observer { + override fun onChanged(show: Boolean?) { + // Snackbar is like Toast but it lets us show forever + if (show == true) { + Snackbar.make( + binding.root, + "No location. Enable location in settings (hint: test with Maps) then check app permissions!", + Snackbar.LENGTH_LONG + ).show() + } + } + }) + + viewModel.regionList.observe(viewLifecycleOwner, object : Observer> { + override fun onChanged(data: List?) { + data ?: return + val chipGroup = binding.regionList + val inflator = LayoutInflater.from(chipGroup.context) + val children = data.map { regionName -> + val chip = inflator.inflate(R.layout.region, chipGroup, false) as Chip + chip.text = regionName + chip.tag = regionName + chip.setOnCheckedChangeListener { button, isChecked -> + viewModel.onFilterChanged(button.tag as String, isChecked) + } + chip + } + chipGroup.removeAllViews() + + for (chip in children) { + chipGroup.addView(chip) + } + } + }) + + setHasOptionsMenu(true) + return binding.root + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + requestLastLocationOrStartLocationUpdates() + } + + /** + * Show the user a dialog asking for permission to use location. + */ + private fun requestLocationPermission() { + requestPermissions(arrayOf(LOCATION_PERMISSION), LOCATION_PERMISSION_REQUEST) + } + + /** + * Request the last location of this device, if known, otherwise start location updates. + * + * The last location is cached from the last application to request location. + */ + private fun requestLastLocationOrStartLocationUpdates() { + // if we don't have permission ask for it and wait until the user grants it + if (ContextCompat.checkSelfPermission(requireContext(), LOCATION_PERMISSION) != PackageManager.PERMISSION_GRANTED) { + requestLocationPermission() + return + } + + val fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()) + + fusedLocationClient.lastLocation.addOnSuccessListener { location -> + if (location == null) { + startLocationUpdates(fusedLocationClient) + } else { + viewModel.onLocationUpdated(location) + } + } + } + + /** + * Start location updates, this will ask the operating system to figure out the devices location. + */ + private fun startLocationUpdates(fusedLocationClient: FusedLocationProviderClient) { + // if we don't have permission ask for it and wait until the user grants it + if (ContextCompat.checkSelfPermission(requireContext(), LOCATION_PERMISSION) != PackageManager.PERMISSION_GRANTED) { + requestLocationPermission() + return + } + + + val request = LocationRequest().setPriority(LocationRequest.PRIORITY_LOW_POWER) + val callback = object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult?) { + val location = locationResult?.lastLocation ?: return + viewModel.onLocationUpdated(location) + } + } + fusedLocationClient.requestLocationUpdates(request, callback, null) + } + + /** + * This will be called by Android when the user responds to the permission request. + * + * If granted, continue with the operation that the user gave us permission to do. + */ + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + LOCATION_PERMISSION_REQUEST -> { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + requestLastLocationOrStartLocationUpdates() + } + } + } + } +} + + diff --git a/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListViewModel.kt b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListViewModel.kt new file mode 100755 index 0000000..1ea171e --- /dev/null +++ b/GDGFinderFinal/app/src/main/java/com/example/android/gdgfinder/search/GdgListViewModel.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.gdgfinder.search + +import android.location.Location +import androidx.lifecycle.* +import com.example.android.gdgfinder.network.GdgApi +import com.example.android.gdgfinder.network.GdgChapter +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.io.IOException + + +class GdgListViewModel: ViewModel() { + + private val repository = GdgChapterRepository(GdgApi.retrofitService) + + private var filter = FilterHolder() + + private var currentJob: Job? = null + + private val _gdgList = MutableLiveData>() + private val _regionList = MutableLiveData>() + private val _showNeedLocation = MutableLiveData() + + // The external LiveData interface to the property is immutable, so only this class can modify + val gdgList: LiveData< List> + get() = _gdgList + + val regionList: LiveData> + get() = _regionList + + val showNeedLocation: LiveData + get() = _showNeedLocation + + init { + // process the initial filter + onQueryChanged() + + viewModelScope.launch { + delay(5_000) + _showNeedLocation.value = !repository.isFullyInitialized + } + } + + private fun onQueryChanged() { + currentJob?.cancel() // if a previous query is running cancel it before starting another + currentJob = viewModelScope.launch { + try { + _gdgList.value = repository.getChaptersForFilter(filter.currentValue) + repository.getFilters().let { + // only update the filters list if it's changed since the last time + if (it != _regionList.value) { + _regionList.value = it + } + } + } catch (e: IOException) { + _gdgList.value = listOf() + } + } + } + + fun onLocationUpdated(location: Location) { + viewModelScope.launch { + repository.onLocationChanged(location) + onQueryChanged() + } + } + + fun onFilterChanged(filter: String, isChecked: Boolean) { + if (this.filter.update(filter, isChecked)) { + onQueryChanged() + } + } + + private class FilterHolder { + var currentValue: String? = null + private set + + fun update(changedFilter: String, isChecked: Boolean): Boolean { + if (isChecked) { + currentValue = changedFilter + return true + } else if (currentValue == changedFilter) { + currentValue = null + return true + } + return false + } + } +} + diff --git a/GDGFinderFinal/app/src/main/res/color/selected_highlight.xml b/GDGFinderFinal/app/src/main/res/color/selected_highlight.xml new file mode 100644 index 0000000..6aeeac6 --- /dev/null +++ b/GDGFinderFinal/app/src/main/res/color/selected_highlight.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/GDGFinderFinal/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/GDGFinderFinal/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100755 index 0000000..6348baa --- /dev/null +++ b/GDGFinderFinal/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/GDGFinderFinal/app/src/main/res/drawable/behind.JPG b/GDGFinderFinal/app/src/main/res/drawable/behind.JPG new file mode 100755 index 0000000..f1a648c Binary files /dev/null and b/GDGFinderFinal/app/src/main/res/drawable/behind.JPG differ diff --git a/GDGFinderFinal/app/src/main/res/drawable/cover.jpg b/GDGFinderFinal/app/src/main/res/drawable/cover.jpg new file mode 100755 index 0000000..a3b570b Binary files /dev/null and b/GDGFinderFinal/app/src/main/res/drawable/cover.jpg differ diff --git a/GDGFinderFinal/app/src/main/res/drawable/ic_gdg.xml b/GDGFinderFinal/app/src/main/res/drawable/ic_gdg.xml new file mode 100755 index 0000000..7a244b4 --- /dev/null +++ b/GDGFinderFinal/app/src/main/res/drawable/ic_gdg.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GDGFinderFinal/app/src/main/res/drawable/ic_launcher_background.xml b/GDGFinderFinal/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100755 index 0000000..a0ad202 --- /dev/null +++ b/GDGFinderFinal/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GDGFinderFinal/app/src/main/res/drawable/logo.png b/GDGFinderFinal/app/src/main/res/drawable/logo.png new file mode 100755 index 0000000..d6040fa Binary files /dev/null and b/GDGFinderFinal/app/src/main/res/drawable/logo.png differ diff --git a/GDGFinderFinal/app/src/main/res/drawable/session.jpg b/GDGFinderFinal/app/src/main/res/drawable/session.jpg new file mode 100755 index 0000000..f1e763f Binary files /dev/null and b/GDGFinderFinal/app/src/main/res/drawable/session.jpg differ diff --git a/GDGFinderFinal/app/src/main/res/drawable/wtm.JPG b/GDGFinderFinal/app/src/main/res/drawable/wtm.JPG new file mode 100755 index 0000000..0d034a8 Binary files /dev/null and b/GDGFinderFinal/app/src/main/res/drawable/wtm.JPG differ diff --git a/GDGFinderFinal/app/src/main/res/font/lobster_two.xml b/GDGFinderFinal/app/src/main/res/font/lobster_two.xml new file mode 100644 index 0000000..6490006 --- /dev/null +++ b/GDGFinderFinal/app/src/main/res/font/lobster_two.xml @@ -0,0 +1,7 @@ + + + diff --git a/GDGFinderFinal/app/src/main/res/layout/activity_main.xml b/GDGFinderFinal/app/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..c71bfe2 --- /dev/null +++ b/GDGFinderFinal/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + diff --git a/GDGFinderFinal/app/src/main/res/layout/add_gdg_fragment.xml b/GDGFinderFinal/app/src/main/res/layout/add_gdg_fragment.xml new file mode 100755 index 0000000..075ba3a --- /dev/null +++ b/GDGFinderFinal/app/src/main/res/layout/add_gdg_fragment.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +