From 80fb6a495d3c805dc1ff7288b1d054af402b96bd Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Thu, 6 Sep 2018 15:42:39 -0700 Subject: [PATCH 1/8] Move everything to a Java subpackage Change-Id: I3995228bb5b924cf3d63a1906dd8b461088759c0 --- firestore/app/build.gradle | 5 +++++ .../example/fireeats/MainActivityTest.java | 5 ++++- firestore/app/src/main/AndroidManifest.xml | 4 ++-- .../fireeats/{ => java}/FilterDialogFragment.java | 5 +++-- .../example/fireeats/{ => java}/Filters.java | 7 ++++--- .../example/fireeats/{ => java}/MainActivity.java | 15 ++++++++------- .../fireeats/{ => java}/RatingDialogFragment.java | 5 +++-- .../{ => java}/RestaurantDetailActivity.java | 11 ++++++----- .../{ => java}/adapter/FirestoreAdapter.java | 2 +- .../{ => java}/adapter/RatingAdapter.java | 4 ++-- .../{ => java}/adapter/RestaurantAdapter.java | 6 +++--- .../example/fireeats/{ => java}/model/Rating.java | 2 +- .../fireeats/{ => java}/model/Restaurant.java | 2 +- .../fireeats/{ => java}/util/RatingUtil.java | 4 ++-- .../fireeats/{ => java}/util/RestaurantUtil.java | 4 ++-- .../viewmodel/MainActivityViewModel.java | 4 ++-- internal/build.gradle | 8 ++++---- 17 files changed, 53 insertions(+), 40 deletions(-) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/FilterDialogFragment.java (96%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/Filters.java (92%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/MainActivity.java (95%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/RatingDialogFragment.java (93%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/RestaurantDetailActivity.java (95%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/adapter/FirestoreAdapter.java (98%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/adapter/RatingAdapter.java (94%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/adapter/RestaurantAdapter.java (94%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/model/Rating.java (96%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/model/Restaurant.java (97%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/util/RatingUtil.java (93%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/util/RestaurantUtil.java (96%) rename firestore/app/src/main/java/com/google/firebase/example/fireeats/{ => java}/viewmodel/MainActivityViewModel.java (85%) diff --git a/firestore/app/build.gradle b/firestore/app/build.gradle index 20a3821d41..17a7037514 100644 --- a/firestore/app/build.gradle +++ b/firestore/app/build.gradle @@ -1,4 +1,6 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' android { testBuildType "release" @@ -31,6 +33,9 @@ android { } dependencies { + implementation project(':internal') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.50" + // Firestore implementation 'com.google.firebase:firebase-core:16.0.3' implementation 'com.google.firebase:firebase-firestore:17.1.0' diff --git a/firestore/app/src/androidTest/java/com/google/firebase/example/fireeats/MainActivityTest.java b/firestore/app/src/androidTest/java/com/google/firebase/example/fireeats/MainActivityTest.java index e7704ed8ef..66bdfd5f1c 100644 --- a/firestore/app/src/androidTest/java/com/google/firebase/example/fireeats/MainActivityTest.java +++ b/firestore/app/src/androidTest/java/com/google/firebase/example/fireeats/MainActivityTest.java @@ -5,11 +5,14 @@ import android.support.test.runner.AndroidJUnit4; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject; -import android.support.test.uiautomator.UiSelector; import android.support.test.uiautomator.UiScrollable; +import android.support.test.uiautomator.UiSelector; import android.test.suitebuilder.annotation.LargeTest; import android.view.accessibility.AccessibilityWindowInfo; + import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.example.fireeats.java.MainActivity; + import org.junit.Assert; import org.junit.Before; import org.junit.Rule; diff --git a/firestore/app/src/main/AndroidManifest.xml b/firestore/app/src/main/AndroidManifest.xml index c64d452911..bea7533918 100644 --- a/firestore/app/src/main/AndroidManifest.xml +++ b/firestore/app/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ android:theme="@style/AppTheme" android:name="android.support.multidex.MultiDexApplication"> @@ -19,7 +19,7 @@ diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/FilterDialogFragment.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java similarity index 96% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/FilterDialogFragment.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java index 50412efce1..1397166baf 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/FilterDialogFragment.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats; +package com.google.firebase.example.fireeats.java; import android.content.Context; import android.os.Bundle; @@ -9,7 +9,8 @@ import android.view.ViewGroup; import android.widget.Spinner; -import com.google.firebase.example.fireeats.model.Restaurant; +import com.google.firebase.example.fireeats.R; +import com.google.firebase.example.fireeats.java.model.Restaurant; import com.google.firebase.firestore.Query; import butterknife.BindView; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/Filters.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/Filters.java similarity index 92% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/Filters.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/Filters.java index 1c0128ad34..54cf94ce31 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/Filters.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/Filters.java @@ -1,10 +1,11 @@ -package com.google.firebase.example.fireeats; +package com.google.firebase.example.fireeats.java; import android.content.Context; import android.text.TextUtils; -import com.google.firebase.example.fireeats.model.Restaurant; -import com.google.firebase.example.fireeats.util.RestaurantUtil; +import com.google.firebase.example.fireeats.R; +import com.google.firebase.example.fireeats.java.model.Restaurant; +import com.google.firebase.example.fireeats.java.util.RestaurantUtil; import com.google.firebase.firestore.Query; /** diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/MainActivity.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainActivity.java similarity index 95% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/MainActivity.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainActivity.java index a81d07486b..33b1a04c73 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/MainActivity.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainActivity.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats; +package com.google.firebase.example.fireeats.java; import android.arch.lifecycle.ViewModelProviders; import android.content.DialogInterface; @@ -26,12 +26,13 @@ import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.auth.FirebaseAuth; -import com.google.firebase.example.fireeats.adapter.RestaurantAdapter; -import com.google.firebase.example.fireeats.model.Rating; -import com.google.firebase.example.fireeats.model.Restaurant; -import com.google.firebase.example.fireeats.util.RatingUtil; -import com.google.firebase.example.fireeats.util.RestaurantUtil; -import com.google.firebase.example.fireeats.viewmodel.MainActivityViewModel; +import com.google.firebase.example.fireeats.R; +import com.google.firebase.example.fireeats.java.adapter.RestaurantAdapter; +import com.google.firebase.example.fireeats.java.model.Rating; +import com.google.firebase.example.fireeats.java.model.Restaurant; +import com.google.firebase.example.fireeats.java.util.RatingUtil; +import com.google.firebase.example.fireeats.java.util.RestaurantUtil; +import com.google.firebase.example.fireeats.java.viewmodel.MainActivityViewModel; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestore; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/RatingDialogFragment.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RatingDialogFragment.java similarity index 93% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/RatingDialogFragment.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RatingDialogFragment.java index e297172f60..8d67a222d0 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/RatingDialogFragment.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RatingDialogFragment.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats; +package com.google.firebase.example.fireeats.java; import android.content.Context; import android.os.Bundle; @@ -10,7 +10,8 @@ import android.widget.EditText; import com.google.firebase.auth.FirebaseAuth; -import com.google.firebase.example.fireeats.model.Rating; +import com.google.firebase.example.fireeats.R; +import com.google.firebase.example.fireeats.java.model.Rating; import butterknife.BindView; import butterknife.ButterKnife; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/RestaurantDetailActivity.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailActivity.java similarity index 95% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/RestaurantDetailActivity.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailActivity.java index 4efb56e0d9..5c5349ee55 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/RestaurantDetailActivity.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailActivity.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats; +package com.google.firebase.example.fireeats.java; import android.content.Context; import android.os.Bundle; @@ -18,10 +18,11 @@ import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; import com.google.android.gms.tasks.Task; -import com.google.firebase.example.fireeats.adapter.RatingAdapter; -import com.google.firebase.example.fireeats.model.Rating; -import com.google.firebase.example.fireeats.model.Restaurant; -import com.google.firebase.example.fireeats.util.RestaurantUtil; +import com.google.firebase.example.fireeats.R; +import com.google.firebase.example.fireeats.java.adapter.RatingAdapter; +import com.google.firebase.example.fireeats.java.model.Rating; +import com.google.firebase.example.fireeats.java.model.Restaurant; +import com.google.firebase.example.fireeats.java.util.RestaurantUtil; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.EventListener; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/FirestoreAdapter.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/FirestoreAdapter.java similarity index 98% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/FirestoreAdapter.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/FirestoreAdapter.java index f9c31b8975..1f67d46377 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/FirestoreAdapter.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/FirestoreAdapter.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats.adapter; +package com.google.firebase.example.fireeats.java.adapter; import android.support.v7.widget.RecyclerView; import android.util.Log; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/RatingAdapter.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/RatingAdapter.java similarity index 94% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/RatingAdapter.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/RatingAdapter.java index 31a22d34c1..4f9c1e6130 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/RatingAdapter.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/RatingAdapter.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats.adapter; +package com.google.firebase.example.fireeats.java.adapter; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -7,7 +7,7 @@ import android.widget.TextView; import com.google.firebase.example.fireeats.R; -import com.google.firebase.example.fireeats.model.Rating; +import com.google.firebase.example.fireeats.java.model.Rating; import com.google.firebase.firestore.Query; import java.text.SimpleDateFormat; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/RestaurantAdapter.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/RestaurantAdapter.java similarity index 94% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/RestaurantAdapter.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/RestaurantAdapter.java index f687b57166..22b3e4ca1b 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/adapter/RestaurantAdapter.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/RestaurantAdapter.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats.adapter; +package com.google.firebase.example.fireeats.java.adapter; import android.content.res.Resources; import android.support.v7.widget.RecyclerView; @@ -10,8 +10,8 @@ import com.bumptech.glide.Glide; import com.google.firebase.example.fireeats.R; -import com.google.firebase.example.fireeats.model.Restaurant; -import com.google.firebase.example.fireeats.util.RestaurantUtil; +import com.google.firebase.example.fireeats.java.model.Restaurant; +import com.google.firebase.example.fireeats.java.util.RestaurantUtil; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.Query; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/model/Rating.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/model/Rating.java similarity index 96% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/model/Rating.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/model/Rating.java index f9fc55df07..98fef372f3 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/model/Rating.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/model/Rating.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats.model; +package com.google.firebase.example.fireeats.java.model; import android.text.TextUtils; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/model/Restaurant.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/model/Restaurant.java similarity index 97% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/model/Restaurant.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/model/Restaurant.java index 8b2cbc1f54..3d402b4b05 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/model/Restaurant.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/model/Restaurant.java @@ -1,4 +1,4 @@ -package com.google.firebase.example.fireeats.model; +package com.google.firebase.example.fireeats.java.model; import com.google.firebase.firestore.IgnoreExtraProperties; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/util/RatingUtil.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/util/RatingUtil.java similarity index 93% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/util/RatingUtil.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/util/RatingUtil.java index ab3817bb10..06b3b25794 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/util/RatingUtil.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/util/RatingUtil.java @@ -1,6 +1,6 @@ -package com.google.firebase.example.fireeats.util; +package com.google.firebase.example.fireeats.java.util; -import com.google.firebase.example.fireeats.model.Rating; +import com.google.firebase.example.fireeats.java.model.Rating; import java.util.ArrayList; import java.util.List; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/util/RestaurantUtil.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/util/RestaurantUtil.java similarity index 96% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/util/RestaurantUtil.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/util/RestaurantUtil.java index c90e2fcbac..8a7fcaa4d7 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/util/RestaurantUtil.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/util/RestaurantUtil.java @@ -1,9 +1,9 @@ -package com.google.firebase.example.fireeats.util; +package com.google.firebase.example.fireeats.java.util; import android.content.Context; import com.google.firebase.example.fireeats.R; -import com.google.firebase.example.fireeats.model.Restaurant; +import com.google.firebase.example.fireeats.java.model.Restaurant; import java.util.Arrays; import java.util.Locale; diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/viewmodel/MainActivityViewModel.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/viewmodel/MainActivityViewModel.java similarity index 85% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/viewmodel/MainActivityViewModel.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/viewmodel/MainActivityViewModel.java index 4cd7e8267a..e393ccc4e9 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/viewmodel/MainActivityViewModel.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/viewmodel/MainActivityViewModel.java @@ -1,8 +1,8 @@ -package com.google.firebase.example.fireeats.viewmodel; +package com.google.firebase.example.fireeats.java.viewmodel; import android.arch.lifecycle.ViewModel; -import com.google.firebase.example.fireeats.Filters; +import com.google.firebase.example.fireeats.java.Filters; /** * ViewModel for {@link com.google.firebase.example.fireeats.MainActivity}. diff --git a/internal/build.gradle b/internal/build.gradle index b4f03dd117..44398d9c8a 100644 --- a/internal/build.gradle +++ b/internal/build.gradle @@ -23,8 +23,8 @@ android { } dependencies { - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support:cardview-v7:27.1.1' - implementation 'com.android.support:recyclerview-v7:27.1.1' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' + api 'com.android.support:appcompat-v7:27.1.1' + api 'com.android.support:cardview-v7:27.1.1' + api 'com.android.support:recyclerview-v7:27.1.1' + api 'com.android.support.constraint:constraint-layout:1.1.3' } From 2489f355fc2dce5664e3f24dea0460866cff1e76 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Fri, 7 Sep 2018 09:46:09 -0700 Subject: [PATCH 2/8] Minium viable Kotlin Change-Id: Ieeeebb59750672b9126f9d039a8736341a640492 --- firestore/app/src/main/AndroidManifest.xml | 24 +- .../example/fireeats/EntryChoiceActivity.kt | 22 ++ .../fireeats/kotlin/FilterDialogFragment.kt | 174 +++++++++++ .../example/fireeats/kotlin/Filters.kt | 93 ++++++ .../example/fireeats/kotlin/MainActivity.kt | 290 ++++++++++++++++++ .../fireeats/kotlin/RatingDialogFragment.kt | 85 +++++ .../kotlin/RestaurantDetailActivity.kt | 224 ++++++++++++++ .../kotlin/adapter/FirestoreAdapter.kt | 111 +++++++ .../fireeats/kotlin/adapter/RatingAdapter.kt | 66 ++++ .../kotlin/adapter/RestaurantAdapter.kt | 92 ++++++ .../example/fireeats/kotlin/model/Rating.kt | 32 ++ .../fireeats/kotlin/model/Restaurant.kt | 40 +++ .../fireeats/kotlin/util/RatingUtil.kt | 73 +++++ .../fireeats/kotlin/util/RestaurantUtil.kt | 96 ++++++ .../kotlin/viewmodel/MainActivityViewModel.kt | 15 + firestore/app/src/main/res/values/styles.xml | 6 + 16 files changed, 1440 insertions(+), 3 deletions(-) create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/EntryChoiceActivity.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/FilterDialogFragment.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/Filters.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainActivity.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RatingDialogFragment.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RestaurantDetailActivity.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/FirestoreAdapter.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RatingAdapter.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RestaurantAdapter.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Rating.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Restaurant.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RatingUtil.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RestaurantUtil.kt create mode 100644 firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/viewmodel/MainActivityViewModel.kt diff --git a/firestore/app/src/main/AndroidManifest.xml b/firestore/app/src/main/AndroidManifest.xml index bea7533918..1c12ddd752 100644 --- a/firestore/app/src/main/AndroidManifest.xml +++ b/firestore/app/src/main/AndroidManifest.xml @@ -8,9 +8,11 @@ android:supportsRtl="true" android:theme="@style/AppTheme" android:name="android.support.multidex.MultiDexApplication"> - + + + @@ -18,9 +20,25 @@ + + + + + + + + + + diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/EntryChoiceActivity.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/EntryChoiceActivity.kt new file mode 100644 index 0000000000..c6096388c1 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/EntryChoiceActivity.kt @@ -0,0 +1,22 @@ +package com.google.firebase.example.fireeats + +import android.content.Intent +import com.firebase.example.internal.BaseEntryChoiceActivity +import com.firebase.example.internal.Choice + +class EntryChoiceActivity : BaseEntryChoiceActivity() { + + override fun getChoices(): List { + return listOf( + Choice( + "Java", + "Run the Cloud Firestore quickstart written in Java.", + Intent(this, com.google.firebase.example.fireeats.java.MainActivity::class.java)), + Choice( + "Kotlin", + "Run the Cloud Firestore quickstart written in Kotlin.", + Intent(this, com.google.firebase.example.fireeats.kotlin.MainActivity::class.java)) + ) + } + +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/FilterDialogFragment.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/FilterDialogFragment.kt new file mode 100644 index 0000000000..b2c47fd27a --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/FilterDialogFragment.kt @@ -0,0 +1,174 @@ +package com.google.firebase.example.fireeats.kotlin + +import android.content.Context +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Spinner +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Restaurant +import com.google.firebase.firestore.Query + +/** + * Dialog Fragment containing filter form. + */ +class FilterDialogFragment : DialogFragment() { + + private var mRootView: View? = null + + @BindView(R.id.spinner_category) + internal var mCategorySpinner: Spinner? = null + + @BindView(R.id.spinner_city) + internal var mCitySpinner: Spinner? = null + + @BindView(R.id.spinner_sort) + internal var mSortSpinner: Spinner? = null + + @BindView(R.id.spinner_price) + internal var mPriceSpinner: Spinner? = null + + private var mFilterListener: FilterListener? = null + + private val selectedCategory: String? + get() { + val selected = mCategorySpinner!!.selectedItem as String + return if (getString(R.string.value_any_category) == selected) { + null + } else { + selected + } + } + + private val selectedCity: String? + get() { + val selected = mCitySpinner!!.selectedItem as String + return if (getString(R.string.value_any_city) == selected) { + null + } else { + selected + } + } + + private val selectedPrice: Int + get() { + val selected = mPriceSpinner!!.selectedItem as String + return if (selected == getString(R.string.price_1)) { + 1 + } else if (selected == getString(R.string.price_2)) { + 2 + } else if (selected == getString(R.string.price_3)) { + 3 + } else { + -1 + } + } + + private val selectedSortBy: String? + get() { + val selected = mSortSpinner!!.selectedItem as String + if (getString(R.string.sort_by_rating) == selected) { + return Restaurant.FIELD_AVG_RATING + } + if (getString(R.string.sort_by_price) == selected) { + return Restaurant.FIELD_PRICE + } + return if (getString(R.string.sort_by_popularity) == selected) { + Restaurant.FIELD_POPULARITY + } else null + + } + + private val sortDirection: Query.Direction? + get() { + val selected = mSortSpinner!!.selectedItem as String + if (getString(R.string.sort_by_rating) == selected) { + return Query.Direction.DESCENDING + } + if (getString(R.string.sort_by_price) == selected) { + return Query.Direction.ASCENDING + } + return if (getString(R.string.sort_by_popularity) == selected) { + Query.Direction.DESCENDING + } else null + + } + + val filters: Filters + get() { + val filters = Filters() + + if (mRootView != null) { + filters.category = selectedCategory + filters.city = selectedCity + filters.price = selectedPrice + filters.sortBy = selectedSortBy + filters.sortDirection = sortDirection + } + + return filters + } + + interface FilterListener { + + fun onFilter(filters: Filters) + + } + + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View? { + mRootView = inflater.inflate(R.layout.dialog_filters, container, false) + ButterKnife.bind(this, mRootView!!) + + return mRootView + } + + override fun onAttach(context: Context?) { + super.onAttach(context) + + if (context is FilterListener) { + mFilterListener = context + } + } + + override fun onResume() { + super.onResume() + dialog.window!!.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + } + + @OnClick(R.id.button_search) + fun onSearchClicked() { + if (mFilterListener != null) { + mFilterListener!!.onFilter(filters) + } + + dismiss() + } + + @OnClick(R.id.button_cancel) + fun onCancelClicked() { + dismiss() + } + + fun resetFilters() { + if (mRootView != null) { + mCategorySpinner!!.setSelection(0) + mCitySpinner!!.setSelection(0) + mPriceSpinner!!.setSelection(0) + mSortSpinner!!.setSelection(0) + } + } + + companion object { + + val TAG = "FilterDialog" + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/Filters.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/Filters.kt new file mode 100644 index 0000000000..e9d56b751c --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/Filters.kt @@ -0,0 +1,93 @@ +package com.google.firebase.example.fireeats.kotlin + +import android.content.Context +import android.text.TextUtils +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Restaurant +import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil +import com.google.firebase.firestore.Query + +/** + * Object for passing filters around. + */ +class Filters { + + var category: String? = null + var city: String? = null + var price = -1 + var sortBy: String? = null + var sortDirection: Query.Direction? = null + + fun hasCategory(): Boolean { + return !TextUtils.isEmpty(category) + } + + fun hasCity(): Boolean { + return !TextUtils.isEmpty(city) + } + + fun hasPrice(): Boolean { + return price > 0 + } + + fun hasSortBy(): Boolean { + return !TextUtils.isEmpty(sortBy) + } + + fun getSearchDescription(context: Context): String { + val desc = StringBuilder() + + if (category == null && city == null) { + desc.append("") + desc.append(context.getString(R.string.all_restaurants)) + desc.append("") + } + + if (category != null) { + desc.append("") + desc.append(category) + desc.append("") + } + + if (category != null && city != null) { + desc.append(" in ") + } + + if (city != null) { + desc.append("") + desc.append(city) + desc.append("") + } + + if (price > 0) { + desc.append(" for ") + desc.append("") + desc.append(RestaurantUtil.getPriceString(price)) + desc.append("") + } + + return desc.toString() + } + + fun getOrderDescription(context: Context): String { + return if (Restaurant.FIELD_PRICE == sortBy) { + context.getString(R.string.sorted_by_price) + } else if (Restaurant.FIELD_POPULARITY == sortBy) { + context.getString(R.string.sorted_by_popularity) + } else { + context.getString(R.string.sorted_by_rating) + } + } + + companion object { + + val default: Filters + get() { + val filters = Filters() + filters.sortBy = Restaurant.FIELD_AVG_RATING + filters.sortDirection = Query.Direction.DESCENDING + + return filters + } + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainActivity.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainActivity.kt new file mode 100644 index 0000000000..03bb05984c --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainActivity.kt @@ -0,0 +1,290 @@ +package com.google.firebase.example.fireeats.kotlin + +import android.app.Activity +import android.arch.lifecycle.ViewModelProviders +import android.content.Intent +import android.os.Bundle +import android.support.annotation.StringRes +import android.support.design.widget.Snackbar +import android.support.v7.app.AlertDialog +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.Toolbar +import android.text.Html +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import com.firebase.ui.auth.AuthUI +import com.firebase.ui.auth.ErrorCodes +import com.firebase.ui.auth.IdpResponse +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.adapter.RestaurantAdapter +import com.google.firebase.example.fireeats.kotlin.model.Restaurant +import com.google.firebase.example.fireeats.kotlin.util.RatingUtil +import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil +import com.google.firebase.example.fireeats.kotlin.viewmodel.MainActivityViewModel +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.FirebaseFirestoreException +import com.google.firebase.firestore.Query + +class MainActivity : AppCompatActivity(), FilterDialogFragment.FilterListener, RestaurantAdapter.OnRestaurantSelectedListener { + + @BindView(R.id.toolbar) + lateinit var mToolbar: Toolbar + + @BindView(R.id.text_current_search) + lateinit var mCurrentSearchView: TextView + + @BindView(R.id.text_current_sort_by) + lateinit var mCurrentSortByView: TextView + + @BindView(R.id.recycler_restaurants) + lateinit var mRestaurantsRecycler: RecyclerView + + @BindView(R.id.view_empty) + lateinit var mEmptyView: ViewGroup + + lateinit var mFirestore: FirebaseFirestore + lateinit var mQuery: Query + + lateinit var mFilterDialog: FilterDialogFragment + lateinit var mAdapter: RestaurantAdapter + + lateinit var mViewModel: MainActivityViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + ButterKnife.bind(this) + setSupportActionBar(mToolbar) + + // View model + mViewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java) + + // Enable Firestore logging + FirebaseFirestore.setLoggingEnabled(true) + + // Firestore + mFirestore = FirebaseFirestore.getInstance() + + // Get ${LIMIT} restaurants + mQuery = mFirestore.collection("restaurants") + .orderBy("avgRating", Query.Direction.DESCENDING) + .limit(LIMIT.toLong()) + + // RecyclerView + mAdapter = object : RestaurantAdapter(mQuery, this@MainActivity) { + override fun onDataChanged() { + // Show/hide content if the query returns empty. + if (itemCount == 0) { + mRestaurantsRecycler.visibility = View.GONE + mEmptyView.visibility = View.VISIBLE + } else { + mRestaurantsRecycler.visibility = View.VISIBLE + mEmptyView.visibility = View.GONE + } + } + + override fun onError(e: FirebaseFirestoreException) { + // Show a snackbar on errors + Snackbar.make(findViewById(android.R.id.content), + "Error: check logs for info.", Snackbar.LENGTH_LONG).show() + } + } + + mRestaurantsRecycler.layoutManager = LinearLayoutManager(this) + mRestaurantsRecycler.adapter = mAdapter + + // Filter Dialog + mFilterDialog = FilterDialogFragment() + } + + public override fun onStart() { + super.onStart() + + // Start sign in if necessary + if (shouldStartSignIn()) { + startSignIn() + return + } + + // Apply filters + onFilter(mViewModel!!.filters) + + // Start listening for Firestore updates + mAdapter.startListening() + } + + public override fun onStop() { + super.onStop() + mAdapter.stopListening() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add_items -> onAddItemsClicked() + R.id.menu_sign_out -> { + AuthUI.getInstance().signOut(this) + startSignIn() + } + } + return super.onOptionsItemSelected(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == RC_SIGN_IN) { + val response = IdpResponse.fromResultIntent(data) + mViewModel.isSigningIn = false + + if (resultCode != Activity.RESULT_OK) { + if (response == null) { + // User pressed the back button. + finish() + } else if (response.error != null && response.error!!.errorCode == ErrorCodes.NO_NETWORK) { + showSignInErrorDialog(R.string.message_no_network) + } else { + showSignInErrorDialog(R.string.message_unknown) + } + } + } + } + + @OnClick(R.id.filter_bar) + fun onFilterClicked() { + // Show the dialog containing filter options + mFilterDialog.show(supportFragmentManager, FilterDialogFragment.TAG) + } + + @OnClick(R.id.button_clear_filter) + fun onClearFilterClicked() { + mFilterDialog.resetFilters() + + onFilter(Filters.default) + } + + override fun onRestaurantSelected(restaurant: DocumentSnapshot) { + // Go to the details page for the selected restaurant + val intent = Intent(this, RestaurantDetailActivity::class.java) + intent.putExtra(RestaurantDetailActivity.KEY_RESTAURANT_ID, restaurant.id) + + startActivity(intent) + overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left) + } + + override fun onFilter(filters: Filters) { + // Construct query basic query + var query: Query = mFirestore.collection("restaurants") + + // Category (equality filter) + if (filters.hasCategory()) { + query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category) + } + + // City (equality filter) + if (filters.hasCity()) { + query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city) + } + + // Price (equality filter) + if (filters.hasPrice()) { + query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price) + } + + // Sort by (orderBy with direction) + if (filters.hasSortBy()) { + query = query.orderBy(filters.sortBy!!, filters.sortDirection!!) + } + + // Limit items + query = query.limit(LIMIT.toLong()) + + // Update the query + mAdapter.setQuery(query) + + // Set header + mCurrentSearchView.text = Html.fromHtml(filters.getSearchDescription(this)) + mCurrentSortByView.text = filters.getOrderDescription(this) + + // Save filters + mViewModel.filters = filters + } + + private fun shouldStartSignIn(): Boolean { + return !mViewModel.isSigningIn && FirebaseAuth.getInstance().currentUser == null + } + + private fun startSignIn() { + // Sign in with FirebaseUI + val intent = AuthUI.getInstance().createSignInIntentBuilder() + .setAvailableProviders(listOf(AuthUI.IdpConfig.EmailBuilder().build())) + .setIsSmartLockEnabled(false) + .build() + + startActivityForResult(intent, RC_SIGN_IN) + mViewModel.isSigningIn = true + } + + private fun onAddItemsClicked() { + // Add a bunch of random restaurants + val batch = mFirestore.batch() + for (i in 0..9) { + val restRef = mFirestore.collection("restaurants").document() + + // Create random restaurant / ratings + val randomRestaurant = RestaurantUtil.getRandom(this) + val randomRatings = RatingUtil.getRandomList(randomRestaurant.numRatings) + randomRestaurant.avgRating = RatingUtil.getAverageRating(randomRatings) + + // Add restaurant + batch.set(restRef, randomRestaurant) + + // Add ratings to subcollection + for (rating in randomRatings) { + batch.set(restRef.collection("ratings").document(), rating) + } + } + + batch.commit().addOnCompleteListener { task -> + if (task.isSuccessful) { + Log.d(TAG, "Write batch succeeded.") + } else { + Log.w(TAG, "write batch failed.", task.exception) + } + } + } + + private fun showSignInErrorDialog(@StringRes message: Int) { + val dialog = AlertDialog.Builder(this) + .setTitle(R.string.title_sign_in_error) + .setMessage(message) + .setCancelable(false) + .setPositiveButton(R.string.option_retry) { dialogInterface, i -> startSignIn() } + .setNegativeButton(R.string.option_exit) { dialogInterface, i -> finish() }.create() + + dialog.show() + } + + companion object { + + private val TAG = "MainActivity" + + private val RC_SIGN_IN = 9001 + + private val LIMIT = 50 + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RatingDialogFragment.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RatingDialogFragment.kt new file mode 100644 index 0000000000..c250574a52 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RatingDialogFragment.kt @@ -0,0 +1,85 @@ +package com.google.firebase.example.fireeats.kotlin + +import android.content.Context +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Rating +import me.zhanghai.android.materialratingbar.MaterialRatingBar + +/** + * Dialog Fragment containing rating form. + */ +class RatingDialogFragment : DialogFragment() { + + @BindView(R.id.restaurant_form_rating) + internal var mRatingBar: MaterialRatingBar? = null + + @BindView(R.id.restaurant_form_text) + internal var mRatingText: EditText? = null + + private var mRatingListener: RatingListener? = null + + internal interface RatingListener { + + fun onRating(rating: Rating) + + } + + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val v = inflater.inflate(R.layout.dialog_rating, container, false) + ButterKnife.bind(this, v) + + return v + } + + override fun onAttach(context: Context?) { + super.onAttach(context) + + if (context is RatingListener) { + mRatingListener = context + } + } + + override fun onResume() { + super.onResume() + dialog.window!!.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + + } + + @OnClick(R.id.restaurant_form_button) + fun onSubmitClicked(view: View) { + val rating = Rating( + FirebaseAuth.getInstance().currentUser!!, + mRatingBar!!.rating.toDouble(), + mRatingText!!.text.toString()) + + if (mRatingListener != null) { + mRatingListener!!.onRating(rating) + } + + dismiss() + } + + @OnClick(R.id.restaurant_form_cancel) + fun onCancelClicked(view: View) { + dismiss() + } + + companion object { + + val TAG = "RatingDialog" + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RestaurantDetailActivity.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RestaurantDetailActivity.kt new file mode 100644 index 0000000000..d780e67fc2 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RestaurantDetailActivity.kt @@ -0,0 +1,224 @@ +package com.google.firebase.example.fireeats.kotlin + +import android.content.Context +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.ImageView +import android.widget.TextView +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import com.bumptech.glide.Glide +import com.google.android.gms.tasks.Task +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.adapter.RatingAdapter +import com.google.firebase.example.fireeats.kotlin.model.Rating +import com.google.firebase.example.fireeats.kotlin.model.Restaurant +import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil +import com.google.firebase.firestore.* +import me.zhanghai.android.materialratingbar.MaterialRatingBar + +class RestaurantDetailActivity : AppCompatActivity(), EventListener, RatingDialogFragment.RatingListener { + + @BindView(R.id.restaurant_image) + internal var mImageView: ImageView? = null + + @BindView(R.id.restaurant_name) + internal var mNameView: TextView? = null + + @BindView(R.id.restaurant_rating) + internal var mRatingIndicator: MaterialRatingBar? = null + + @BindView(R.id.restaurant_num_ratings) + internal var mNumRatingsView: TextView? = null + + @BindView(R.id.restaurant_city) + internal var mCityView: TextView? = null + + @BindView(R.id.restaurant_category) + internal var mCategoryView: TextView? = null + + @BindView(R.id.restaurant_price) + internal var mPriceView: TextView? = null + + @BindView(R.id.view_empty_ratings) + internal var mEmptyView: ViewGroup? = null + + @BindView(R.id.recycler_ratings) + internal var mRatingsRecycler: RecyclerView? = null + + private var mRatingDialog: RatingDialogFragment? = null + + private var mFirestore: FirebaseFirestore? = null + private var mRestaurantRef: DocumentReference? = null + private var mRestaurantRegistration: ListenerRegistration? = null + + private var mRatingAdapter: RatingAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_restaurant_detail) + ButterKnife.bind(this) + + // Get restaurant ID from extras + val restaurantId = intent.extras!!.getString(KEY_RESTAURANT_ID) + ?: throw IllegalArgumentException("Must pass extra $KEY_RESTAURANT_ID") + + // Initialize Firestore + mFirestore = FirebaseFirestore.getInstance() + + // Get reference to the restaurant + mRestaurantRef = mFirestore!!.collection("restaurants").document(restaurantId) + + // Get ratings + val ratingsQuery = mRestaurantRef!! + .collection("ratings") + .orderBy("timestamp", Query.Direction.DESCENDING) + .limit(50) + + // RecyclerView + mRatingAdapter = object : RatingAdapter(ratingsQuery) { + override fun onDataChanged() { + if (itemCount == 0) { + mRatingsRecycler!!.visibility = View.GONE + mEmptyView!!.visibility = View.VISIBLE + } else { + mRatingsRecycler!!.visibility = View.VISIBLE + mEmptyView!!.visibility = View.GONE + } + } + } + mRatingsRecycler!!.layoutManager = LinearLayoutManager(this) + mRatingsRecycler!!.adapter = mRatingAdapter + + mRatingDialog = RatingDialogFragment() + } + + public override fun onStart() { + super.onStart() + + mRatingAdapter!!.startListening() + mRestaurantRegistration = mRestaurantRef!!.addSnapshotListener(this) + } + + public override fun onStop() { + super.onStop() + + mRatingAdapter!!.stopListening() + + if (mRestaurantRegistration != null) { + mRestaurantRegistration!!.remove() + mRestaurantRegistration = null + } + } + + override fun finish() { + super.finish() + overridePendingTransition(R.anim.slide_in_from_left, R.anim.slide_out_to_right) + } + + /** + * Listener for the Restaurant document ([.mRestaurantRef]). + */ + override fun onEvent(snapshot: DocumentSnapshot?, e: FirebaseFirestoreException?) { + if (e != null) { + Log.w(TAG, "restaurant:onEvent", e) + return + } + + onRestaurantLoaded(snapshot!!.toObject(Restaurant::class.java)!!) + } + + private fun onRestaurantLoaded(restaurant: Restaurant) { + mNameView!!.text = restaurant.name + mRatingIndicator!!.rating = restaurant.avgRating.toFloat() + mNumRatingsView!!.text = getString(R.string.fmt_num_ratings, restaurant.numRatings) + mCityView!!.text = restaurant.city + mCategoryView!!.text = restaurant.category + mPriceView!!.text = RestaurantUtil.getPriceString(restaurant) + + // Background image + Glide.with(mImageView!!.context) + .load(restaurant.photo) + .into(mImageView!!) + } + + @OnClick(R.id.restaurant_button_back) + fun onBackArrowClicked(view: View) { + onBackPressed() + } + + @OnClick(R.id.fab_show_rating_dialog) + fun onAddRatingClicked(view: View) { + mRatingDialog!!.show(supportFragmentManager, RatingDialogFragment.TAG) + } + + override fun onRating(rating: Rating) { + // In a transaction, add the new rating and update the aggregate totals + addRating(mRestaurantRef!!, rating) + .addOnSuccessListener(this) { + Log.d(TAG, "Rating added") + + // Hide keyboard and scroll to top + hideKeyboard() + mRatingsRecycler!!.smoothScrollToPosition(0) + } + .addOnFailureListener(this) { e -> + Log.w(TAG, "Add rating failed", e) + + // Show failure message and hide keyboard + hideKeyboard() + Snackbar.make(findViewById(android.R.id.content), "Failed to add rating", + Snackbar.LENGTH_SHORT).show() + } + } + + private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task { + // Create reference for new rating, for use inside the transaction + val ratingRef = restaurantRef.collection("ratings").document() + + // In a transaction, add the new rating and update the aggregate totals + return mFirestore!!.runTransaction { transaction -> + val restaurant = transaction.get(restaurantRef).toObject(Restaurant::class.java) + + // Compute new number of ratings + val newNumRatings = restaurant!!.numRatings + 1 + + // Compute new average rating + val oldRatingTotal = restaurant.avgRating * restaurant.numRatings + val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings + + // Set new restaurant info + restaurant.numRatings = newNumRatings + restaurant.avgRating = newAvgRating + + // Commit to Firestore + transaction.set(restaurantRef, restaurant) + transaction.set(ratingRef, rating) + + null + } + } + + private fun hideKeyboard() { + val view = currentFocus + if (view != null) { + (getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .hideSoftInputFromWindow(view.windowToken, 0) + } + } + + companion object { + + private val TAG = "RestaurantDetail" + + val KEY_RESTAURANT_ID = "key_restaurant_id" + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/FirestoreAdapter.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/FirestoreAdapter.kt new file mode 100644 index 0000000000..b092680e15 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/FirestoreAdapter.kt @@ -0,0 +1,111 @@ +package com.google.firebase.example.fireeats.kotlin.adapter + +import android.support.v7.widget.RecyclerView +import android.util.Log +import com.google.firebase.firestore.* +import com.google.firebase.firestore.EventListener +import java.util.* + +/** + * RecyclerView adapter for displaying the results of a Firestore [Query]. + * + * Note that this class forgoes some efficiency to gain simplicity. For example, the result of + * [DocumentSnapshot.toObject] is not cached so the same object may be deserialized + * many times as the user scrolls. + */ +abstract class FirestoreAdapter(private var mQuery: Query?) : RecyclerView.Adapter(), EventListener { + private var mRegistration: ListenerRegistration? = null + + private val mSnapshots = ArrayList() + + override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) { + if (e != null) { + Log.w(TAG, "onEvent:error", e) + onError(e) + return + } + + // Dispatch the event + Log.d(TAG, "onEvent:numChanges:" + documentSnapshots!!.documentChanges.size) + for (change in documentSnapshots.documentChanges) { + when (change.type) { + DocumentChange.Type.ADDED -> onDocumentAdded(change) + DocumentChange.Type.MODIFIED -> onDocumentModified(change) + DocumentChange.Type.REMOVED -> onDocumentRemoved(change) + } + } + + onDataChanged() + } + + fun startListening() { + if (mQuery != null && mRegistration == null) { + mRegistration = mQuery!!.addSnapshotListener(this) + } + } + + fun stopListening() { + if (mRegistration != null) { + mRegistration!!.remove() + mRegistration = null + } + + mSnapshots.clear() + notifyDataSetChanged() + } + + fun setQuery(query: Query) { + // Stop listening + stopListening() + + // Clear existing data + mSnapshots.clear() + notifyDataSetChanged() + + // Listen to new query + mQuery = query + startListening() + } + + open fun onError(e: FirebaseFirestoreException) { + Log.w(TAG, "onError", e) + } + + open fun onDataChanged() {} + + override fun getItemCount(): Int { + return mSnapshots.size + } + + protected fun getSnapshot(index: Int): DocumentSnapshot { + return mSnapshots[index] + } + + protected fun onDocumentAdded(change: DocumentChange) { + mSnapshots.add(change.newIndex, change.document) + notifyItemInserted(change.newIndex) + } + + protected fun onDocumentModified(change: DocumentChange) { + if (change.oldIndex == change.newIndex) { + // Item changed but remained in same position + mSnapshots[change.oldIndex] = change.document + notifyItemChanged(change.oldIndex) + } else { + // Item changed and changed position + mSnapshots.removeAt(change.oldIndex) + mSnapshots.add(change.newIndex, change.document) + notifyItemMoved(change.oldIndex, change.newIndex) + } + } + + protected fun onDocumentRemoved(change: DocumentChange) { + mSnapshots.removeAt(change.oldIndex) + notifyItemRemoved(change.oldIndex) + } + + companion object { + + private val TAG = "FirestoreAdapter" + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RatingAdapter.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RatingAdapter.kt new file mode 100644 index 0000000000..5ddf7638a9 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RatingAdapter.kt @@ -0,0 +1,66 @@ +package com.google.firebase.example.fireeats.kotlin.adapter + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import butterknife.BindView +import butterknife.ButterKnife +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Rating +import com.google.firebase.firestore.Query +import me.zhanghai.android.materialratingbar.MaterialRatingBar +import java.text.SimpleDateFormat +import java.util.* + +/** + * RecyclerView adapter for a list of [Rating]. + */ +open class RatingAdapter(query: Query) : FirestoreAdapter(query) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_rating, parent, false)) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getSnapshot(position).toObject(Rating::class.java)) + } + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + @BindView(R.id.rating_item_name) + var nameView: TextView? = null + + @BindView(R.id.rating_item_rating) + var ratingBar: MaterialRatingBar? = null + + @BindView(R.id.rating_item_text) + var textView: TextView? = null + + @BindView(R.id.rating_item_date) + var dateView: TextView? = null + + init { + ButterKnife.bind(this, itemView) + } + + fun bind(rating: Rating?) { + nameView!!.text = rating!!.userName + ratingBar!!.rating = rating.rating.toFloat() + textView!!.text = rating.text + + if (rating.timestamp != null) { + dateView!!.text = FORMAT.format(rating.timestamp) + } + } + + companion object { + + private val FORMAT = SimpleDateFormat( + "MM/dd/yyyy", Locale.US) + } + } + +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RestaurantAdapter.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RestaurantAdapter.kt new file mode 100644 index 0000000000..02ba5a95db --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RestaurantAdapter.kt @@ -0,0 +1,92 @@ +package com.google.firebase.example.fireeats.kotlin.adapter + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import butterknife.BindView +import butterknife.ButterKnife +import com.bumptech.glide.Glide +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Restaurant +import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.Query +import me.zhanghai.android.materialratingbar.MaterialRatingBar + +/** + * RecyclerView adapter for a list of Restaurants. + */ +open class RestaurantAdapter(query: Query, val mListener: OnRestaurantSelectedListener) : FirestoreAdapter(query) { + + interface OnRestaurantSelectedListener { + + fun onRestaurantSelected(restaurant: DocumentSnapshot) + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return ViewHolder(inflater.inflate(R.layout.item_restaurant, parent, false)) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getSnapshot(position), mListener) + } + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + @BindView(R.id.restaurant_item_image) + var imageView: ImageView? = null + + @BindView(R.id.restaurant_item_name) + var nameView: TextView? = null + + @BindView(R.id.restaurant_item_rating) + var ratingBar: MaterialRatingBar? = null + + @BindView(R.id.restaurant_item_num_ratings) + var numRatingsView: TextView? = null + + @BindView(R.id.restaurant_item_price) + var priceView: TextView? = null + + @BindView(R.id.restaurant_item_category) + var categoryView: TextView? = null + + @BindView(R.id.restaurant_item_city) + var cityView: TextView? = null + + init { + ButterKnife.bind(this, itemView) + } + + fun bind(snapshot: DocumentSnapshot, + listener: OnRestaurantSelectedListener?) { + + val restaurant = snapshot.toObject(Restaurant::class.java) + val resources = itemView.resources + + // Load image + Glide.with(imageView!!.context) + .load(restaurant!!.photo) + .into(imageView!!) + + nameView!!.text = restaurant.name + ratingBar!!.rating = restaurant.avgRating.toFloat() + cityView!!.text = restaurant.city + categoryView!!.text = restaurant.category + numRatingsView!!.text = resources.getString(R.string.fmt_num_ratings, + restaurant.numRatings) + priceView!!.text = RestaurantUtil.getPriceString(restaurant) + + // Click listener + itemView.setOnClickListener { + listener?.onRestaurantSelected(snapshot) + } + } + + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Rating.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Rating.kt new file mode 100644 index 0000000000..9e50a2fc0a --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Rating.kt @@ -0,0 +1,32 @@ +package com.google.firebase.example.fireeats.kotlin.model + +import android.text.TextUtils +import com.google.firebase.auth.FirebaseUser +import com.google.firebase.firestore.ServerTimestamp +import java.util.* + +/** + * Model POJO for a rating. + */ +class Rating { + + var userId: String? = null + var userName: String? = null + var rating: Double = 0.toDouble() + var text: String? = null + @ServerTimestamp + var timestamp: Date? = null + + constructor() {} + + constructor(user: FirebaseUser, rating: Double, text: String) { + this.userId = user.uid + this.userName = user.displayName + if (TextUtils.isEmpty(this.userName)) { + this.userName = user.email + } + + this.rating = rating + this.text = text + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Restaurant.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Restaurant.kt new file mode 100644 index 0000000000..d74742c7e3 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Restaurant.kt @@ -0,0 +1,40 @@ +package com.google.firebase.example.fireeats.kotlin.model + +import com.google.firebase.firestore.IgnoreExtraProperties + +/** + * Restaurant POJO. + */ +@IgnoreExtraProperties +class Restaurant { + + var name: String? = null + var city: String? = null + var category: String? = null + var photo: String? = null + var price: Int = 0 + var numRatings: Int = 0 + var avgRating: Double = 0.toDouble() + + constructor() {} + + constructor(name: String, city: String, category: String, photo: String, + price: Int, numRatings: Int, avgRating: Double) { + this.name = name + this.city = city + this.category = category + this.price = price + this.numRatings = numRatings + this.avgRating = avgRating + } + + companion object { + + val FIELD_CITY = "city" + val FIELD_CATEGORY = "category" + val FIELD_PRICE = "price" + val FIELD_POPULARITY = "numRatings" + val FIELD_AVG_RATING = "avgRating" + + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RatingUtil.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RatingUtil.kt new file mode 100644 index 0000000000..36f74f4ab3 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RatingUtil.kt @@ -0,0 +1,73 @@ +package com.google.firebase.example.fireeats.kotlin.util + +import com.google.firebase.example.fireeats.kotlin.model.Rating +import java.util.* + +/** + * Utilities for Ratings. + */ +object RatingUtil { + + val REVIEW_CONTENTS = arrayOf( + // 0 - 1 stars + "This was awful! Totally inedible.", + + // 1 - 2 stars + "This was pretty bad, would not go back.", + + // 2 - 3 stars + "I was fed, so that's something.", + + // 3 - 4 stars + "This was a nice meal, I'd go back.", + + // 4 - 5 stars + "This was fantastic! Best ever!") + + /** + * Create a random Rating POJO. + */ + val random: Rating + get() { + val rating = Rating() + + val random = Random() + + val score = random.nextDouble() * 5.0 + val text = REVIEW_CONTENTS[Math.floor(score).toInt()] + + rating.userId = UUID.randomUUID().toString() + rating.userName = "Random User" + rating.rating = score + rating.text = text + + return rating + } + + /** + * Get a list of random Rating POJOs. + */ + fun getRandomList(length: Int): List { + val result = ArrayList() + + for (i in 0 until length) { + result.add(random) + } + + return result + } + + /** + * Get the average rating of a List. + */ + fun getAverageRating(ratings: List): Double { + var sum = 0.0 + + for (rating in ratings) { + sum += rating.rating + } + + return sum / ratings.size + } + +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RestaurantUtil.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RestaurantUtil.kt new file mode 100644 index 0000000000..78cf2a6093 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RestaurantUtil.kt @@ -0,0 +1,96 @@ +package com.google.firebase.example.fireeats.kotlin.util + +import android.content.Context +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Restaurant +import java.util.* + +/** + * Utilities for Restaurants. + */ +object RestaurantUtil { + + private val TAG = "RestaurantUtil" + + private val RESTAURANT_URL_FMT = "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_%d.png" + private val MAX_IMAGE_NUM = 22 + + private val NAME_FIRST_WORDS = arrayOf("Foo", "Bar", "Baz", "Qux", "Fire", "Sam's", "World Famous", "Google", "The Best") + + private val NAME_SECOND_WORDS = arrayOf("Restaurant", "Cafe", "Spot", "Eatin' Place", "Eatery", "Drive Thru", "Diner") + + /** + * Create a random Restaurant POJO. + */ + fun getRandom(context: Context): Restaurant { + val restaurant = Restaurant() + val random = Random() + + // Cities (first elemnt is 'Any') + var cities = context.resources.getStringArray(R.array.cities) + cities = Arrays.copyOfRange(cities, 1, cities.size) + + // Categories (first element is 'Any') + var categories = context.resources.getStringArray(R.array.categories) + categories = Arrays.copyOfRange(categories, 1, categories.size) + + val prices = intArrayOf(1, 2, 3) + + restaurant.name = getRandomName(random) + restaurant.city = getRandomString(cities, random) + restaurant.category = getRandomString(categories, random) + restaurant.photo = getRandomImageUrl(random) + restaurant.price = getRandomInt(prices, random) + restaurant.numRatings = random.nextInt(20) + + // Note: average rating intentionally not set + + return restaurant + } + + + /** + * Get a random image. + */ + private fun getRandomImageUrl(random: Random): String { + // Integer between 1 and MAX_IMAGE_NUM (inclusive) + val id = random.nextInt(MAX_IMAGE_NUM) + 1 + + return String.format(Locale.getDefault(), RESTAURANT_URL_FMT, id) + } + + /** + * Get price represented as dollar signs. + */ + fun getPriceString(restaurant: Restaurant): String { + return getPriceString(restaurant.price) + } + + /** + * Get price represented as dollar signs. + */ + fun getPriceString(priceInt: Int): String { + when (priceInt) { + 1 -> return "$" + 2 -> return "$$" + 3 -> return "$$$" + else -> return "$$$" + } + } + + private fun getRandomName(random: Random): String { + return (getRandomString(NAME_FIRST_WORDS, random) + " " + + getRandomString(NAME_SECOND_WORDS, random)) + } + + private fun getRandomString(array: Array, random: Random): String { + val ind = random.nextInt(array.size) + return array[ind] + } + + private fun getRandomInt(array: IntArray, random: Random): Int { + val ind = random.nextInt(array.size) + return array[ind] + } + +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/viewmodel/MainActivityViewModel.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/viewmodel/MainActivityViewModel.kt new file mode 100644 index 0000000000..9aea9bf2ca --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/viewmodel/MainActivityViewModel.kt @@ -0,0 +1,15 @@ +package com.google.firebase.example.fireeats.kotlin.viewmodel + +import android.arch.lifecycle.ViewModel +import com.google.firebase.example.fireeats.kotlin.Filters + +/** + * ViewModel for [com.google.firebase.example.fireeats.MainActivity]. + */ + +class MainActivityViewModel : ViewModel() { + + var isSigningIn: Boolean = false + var filters: Filters = Filters.default + +} diff --git a/firestore/app/src/main/res/values/styles.xml b/firestore/app/src/main/res/values/styles.xml index ade57d6a8e..6521eeae4a 100644 --- a/firestore/app/src/main/res/values/styles.xml +++ b/firestore/app/src/main/res/values/styles.xml @@ -1,5 +1,11 @@ + +