diff --git a/firestore/app/build.gradle b/firestore/app/build.gradle index 20a3821d4..0f461d8e9 100644 --- a/firestore/app/build.gradle +++ b/firestore/app/build.gradle @@ -1,4 +1,7 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'org.jetbrains.kotlin.android.extensions' android { testBuildType "release" @@ -30,7 +33,14 @@ android { } } +androidExtensions { + experimental = true +} + 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 e7704ed8e..b5553701a 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; @@ -55,7 +58,7 @@ .clickAndWaitForNewWindow(TIMEOUT); // Click add review - getById("fab_show_rating_dialog").click(); + getById("fabShowRatingDialog").click(); //Write a review getById("restaurant_form_text").setText("\uD83D\uDE0E\uD83D\uDE00"); @@ -65,11 +68,11 @@ getById("restaurant_form_button").clickAndWaitForNewWindow(TIMEOUT); // Assert that the review exists - UiScrollable ratingsList = new UiScrollable(getIdSelector("recycler_ratings")); + UiScrollable ratingsList = new UiScrollable(getIdSelector("recyclerRatings")); ratingsList.waitForExists(TIMEOUT); ratingsList.scrollToBeginning(100); Assert.assertTrue( - getById("recycler_ratings") + getById("recyclerRatings") .getChild(new UiSelector().text("\uD83D\uDE0E\uD83D\uDE00")) .waitForExists(TIMEOUT)); } diff --git a/firestore/app/src/main/AndroidManifest.xml b/firestore/app/src/main/AndroidManifest.xml index c64d45291..1c12ddd75 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"> - + + + @@ -19,7 +21,23 @@ + + + + + + + + + + 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 000000000..c6096388c --- /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/FilterDialogFragment.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java similarity index 92% 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 50412efce..39b93dc6d 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; @@ -31,16 +32,16 @@ interface FilterListener { private View mRootView; - @BindView(R.id.spinner_category) + @BindView(R.id.spinnerCategory) Spinner mCategorySpinner; - @BindView(R.id.spinner_city) + @BindView(R.id.spinnerCity) Spinner mCitySpinner; - @BindView(R.id.spinner_sort) + @BindView(R.id.spinnerSort) Spinner mSortSpinner; - @BindView(R.id.spinner_price) + @BindView(R.id.spinnerPrice) Spinner mPriceSpinner; private FilterListener mFilterListener; @@ -73,7 +74,7 @@ public void onResume() { ViewGroup.LayoutParams.WRAP_CONTENT); } - @OnClick(R.id.button_search) + @OnClick(R.id.buttonSearch) public void onSearchClicked() { if (mFilterListener != null) { mFilterListener.onFilter(getFilters()); @@ -82,7 +83,7 @@ public void onSearchClicked() { dismiss(); } - @OnClick(R.id.button_cancel) + @OnClick(R.id.buttonCancel) public void onCancelClicked() { dismiss(); } 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 1c0128ad3..54cf94ce3 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 93% 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 a81d07486..ad3c5b8b3 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; @@ -59,16 +60,16 @@ public class MainActivity extends AppCompatActivity implements @BindView(R.id.toolbar) Toolbar mToolbar; - @BindView(R.id.text_current_search) + @BindView(R.id.textCurrentSearch) TextView mCurrentSearchView; - @BindView(R.id.text_current_sort_by) + @BindView(R.id.textCurrentSortBy) TextView mCurrentSortByView; - @BindView(R.id.recycler_restaurants) + @BindView(R.id.recyclerRestaurants) RecyclerView mRestaurantsRecycler; - @BindView(R.id.view_empty) + @BindView(R.id.viewEmpty) ViewGroup mEmptyView; private FirebaseFirestore mFirestore; @@ -197,13 +198,13 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } } - @OnClick(R.id.filter_bar) + @OnClick(R.id.filterBar) public void onFilterClicked() { // Show the dialog containing filter options mFilterDialog.show(getSupportFragmentManager(), FilterDialogFragment.TAG); } - @OnClick(R.id.button_clear_filter) + @OnClick(R.id.buttonClearFilter) public void onClearFilterClicked() { mFilterDialog.resetFilters(); 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 86% 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 e297172f6..34d1fe2a6 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; @@ -24,10 +25,10 @@ public class RatingDialogFragment extends DialogFragment { public static final String TAG = "RatingDialog"; - @BindView(R.id.restaurant_form_rating) + @BindView(R.id.restaurantFormRating) MaterialRatingBar mRatingBar; - @BindView(R.id.restaurant_form_text) + @BindView(R.id.restaurantFormText) EditText mRatingText; interface RatingListener { @@ -67,7 +68,7 @@ public void onResume() { } - @OnClick(R.id.restaurant_form_button) + @OnClick(R.id.restaurantFormButton) public void onSubmitClicked(View view) { Rating rating = new Rating( FirebaseAuth.getInstance().getCurrentUser(), @@ -81,7 +82,7 @@ public void onSubmitClicked(View view) { dismiss(); } - @OnClick(R.id.restaurant_form_cancel) + @OnClick(R.id.restaurantFormCancel) public void onCancelClicked(View view) { dismiss(); } 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 91% 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 4efb56e0d..719094749 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; @@ -43,31 +44,31 @@ public class RestaurantDetailActivity extends AppCompatActivity public static final String KEY_RESTAURANT_ID = "key_restaurant_id"; - @BindView(R.id.restaurant_image) + @BindView(R.id.restaurantImage) ImageView mImageView; - @BindView(R.id.restaurant_name) + @BindView(R.id.restaurantName) TextView mNameView; - @BindView(R.id.restaurant_rating) + @BindView(R.id.restaurantRating) MaterialRatingBar mRatingIndicator; - @BindView(R.id.restaurant_num_ratings) + @BindView(R.id.restaurantNumRatings) TextView mNumRatingsView; - @BindView(R.id.restaurant_city) + @BindView(R.id.restaurantCity) TextView mCityView; - @BindView(R.id.restaurant_category) + @BindView(R.id.restaurantCategory) TextView mCategoryView; - @BindView(R.id.restaurant_price) + @BindView(R.id.restaurantPrice) TextView mPriceView; - @BindView(R.id.view_empty_ratings) + @BindView(R.id.viewEmptyRatings) ViewGroup mEmptyView; - @BindView(R.id.recycler_ratings) + @BindView(R.id.recyclerRatings) RecyclerView mRatingsRecycler; private RatingDialogFragment mRatingDialog; @@ -174,12 +175,12 @@ private void onRestaurantLoaded(Restaurant restaurant) { .into(mImageView); } - @OnClick(R.id.restaurant_button_back) + @OnClick(R.id.restaurantButtonBack) public void onBackArrowClicked(View view) { onBackPressed(); } - @OnClick(R.id.fab_show_rating_dialog) + @OnClick(R.id.fabShowRatingDialog) public void onAddRatingClicked(View view) { mRatingDialog.show(getSupportFragmentManager(), RatingDialogFragment.TAG); } 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 f9c31b897..1f67d4637 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 86% 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 31a22d34c..5f5eb7153 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; @@ -42,16 +42,16 @@ static class ViewHolder extends RecyclerView.ViewHolder { private static final SimpleDateFormat FORMAT = new SimpleDateFormat( "MM/dd/yyyy", Locale.US); - @BindView(R.id.rating_item_name) + @BindView(R.id.ratingItemName) TextView nameView; - @BindView(R.id.rating_item_rating) + @BindView(R.id.ratingItemRating) MaterialRatingBar ratingBar; - @BindView(R.id.rating_item_text) + @BindView(R.id.ratingItemText) TextView textView; - @BindView(R.id.rating_item_date) + @BindView(R.id.ratingItemDate) TextView dateView; public ViewHolder(View itemView) { 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 85% 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 f687b5716..a9bb2f7ee 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; @@ -50,25 +50,25 @@ public void onBindViewHolder(ViewHolder holder, int position) { static class ViewHolder extends RecyclerView.ViewHolder { - @BindView(R.id.restaurant_item_image) + @BindView(R.id.restaurantItemImage) ImageView imageView; - @BindView(R.id.restaurant_item_name) + @BindView(R.id.restaurantItemName) TextView nameView; - @BindView(R.id.restaurant_item_rating) + @BindView(R.id.restaurantItemRating) MaterialRatingBar ratingBar; - @BindView(R.id.restaurant_item_num_ratings) + @BindView(R.id.restaurantItemNumRatings) TextView numRatingsView; - @BindView(R.id.restaurant_item_price) + @BindView(R.id.restaurantItemPrice) TextView priceView; - @BindView(R.id.restaurant_item_category) + @BindView(R.id.restaurantItemCategory) TextView categoryView; - @BindView(R.id.restaurant_item_city) + @BindView(R.id.restaurantItemCity) TextView cityView; public ViewHolder(View itemView) { 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 f9fc55df0..98fef372f 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 95% 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 8b2cbc1f5..03c21b229 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; @@ -29,6 +29,7 @@ public Restaurant(String name, String city, String category, String photo, this.name = name; this.city = city; this.category = category; + this.photo = photo; this.price = price; this.numRatings = numRatings; this.avgRating = avgRating; 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 ab3817bb1..06b3b2579 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 c90e2fcba..8a7fcaa4d 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 4cd7e8267..e393ccc4e 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/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 000000000..e9b9aea58 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/FilterDialogFragment.kt @@ -0,0 +1,154 @@ +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 com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Restaurant +import com.google.firebase.firestore.Query +import kotlinx.android.synthetic.main.dialog_filters.* +import kotlinx.android.synthetic.main.dialog_filters.view.* + +/** + * Dialog Fragment containing filter form. + */ +class FilterDialogFragment : DialogFragment() { + + private lateinit var rootView: View + + private var filterListener: FilterListener? = null + + private val selectedCategory: String? + get() { + val selected = spinnerCategory.selectedItem as String + return if (getString(R.string.value_any_category) == selected) { + null + } else { + selected + } + } + + private val selectedCity: String? + get() { + val selected = spinnerCity.selectedItem as String + return if (getString(R.string.value_any_city) == selected) { + null + } else { + selected + } + } + + private val selectedPrice: Int + get() { + val selected = spinnerPrice.selectedItem as String + return when (selected) { + getString(R.string.price_1) -> 1 + getString(R.string.price_2) -> 2 + getString(R.string.price_3) -> 3 + else -> -1 + } + } + + private val selectedSortBy: String? + get() { + val selected = spinnerSort.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 = spinnerSort.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 { + Query.Direction.DESCENDING + } + + } + + val filters: Filters + get() { + val filters = Filters() + + 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? { + rootView = inflater.inflate(R.layout.dialog_filters, container, false) + + rootView.buttonSearch.setOnClickListener { onSearchClicked() } + rootView.buttonCancel.setOnClickListener { onCancelClicked() } + + return rootView + } + + override fun onAttach(context: Context?) { + super.onAttach(context) + + if (context is FilterListener) { + filterListener = context + } + } + + override fun onResume() { + super.onResume() + dialog.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + } + + fun onSearchClicked() { + filterListener?.onFilter(filters) + dismiss() + } + + fun onCancelClicked() { + dismiss() + } + + fun resetFilters() { + spinnerCategory?.setSelection(0) + spinnerCity?.setSelection(0) + spinnerPrice?.setSelection(0) + spinnerSort?.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 000000000..b0d2edad5 --- /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 = Query.Direction.DESCENDING + + 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 000000000..a559517c8 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainActivity.kt @@ -0,0 +1,269 @@ +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.text.Html +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +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 +import kotlinx.android.synthetic.main.activity_main.* + +class MainActivity : AppCompatActivity(), FilterDialogFragment.FilterListener, RestaurantAdapter.OnRestaurantSelectedListener { + + lateinit var firestore: FirebaseFirestore + lateinit var query: Query + + lateinit var filterDialog: FilterDialogFragment + lateinit var adapter: RestaurantAdapter + + lateinit var viewModel: MainActivityViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + setSupportActionBar(toolbar) + + // View model + viewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java) + + // Enable Firestore logging + FirebaseFirestore.setLoggingEnabled(true) + + // Firestore + firestore = FirebaseFirestore.getInstance() + + // Get ${LIMIT} restaurants + query = firestore.collection("restaurants") + .orderBy("avgRating", Query.Direction.DESCENDING) + .limit(LIMIT.toLong()) + + // RecyclerView + adapter = object : RestaurantAdapter(query, this@MainActivity) { + override fun onDataChanged() { + // Show/hide content if the query returns empty. + if (itemCount == 0) { + recyclerRestaurants.visibility = View.GONE + viewEmpty.visibility = View.VISIBLE + } else { + recyclerRestaurants.visibility = View.VISIBLE + viewEmpty.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() + } + } + + recyclerRestaurants.layoutManager = LinearLayoutManager(this) + recyclerRestaurants.adapter = adapter + + // Filter Dialog + filterDialog = FilterDialogFragment() + + filterBar.setOnClickListener { onFilterClicked() } + buttonClearFilter.setOnClickListener { onClearFilterClicked() } + } + + public override fun onStart() { + super.onStart() + + // Start sign in if necessary + if (shouldStartSignIn()) { + startSignIn() + return + } + + // Apply filters + onFilter(viewModel.filters) + + // Start listening for Firestore updates + adapter.startListening() + } + + public override fun onStop() { + super.onStop() + adapter.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) + viewModel.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) + } + } + } + } + + fun onFilterClicked() { + // Show the dialog containing filter options + filterDialog.show(supportFragmentManager, FilterDialogFragment.TAG) + } + + fun onClearFilterClicked() { + filterDialog.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 = firestore.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.toString(), filters.sortDirection) + } + + // Limit items + query = query.limit(LIMIT.toLong()) + + // Update the query + adapter.setQuery(query) + + // Set header + textCurrentSearch.text = Html.fromHtml(filters.getSearchDescription(this)) + textCurrentSortBy.text = filters.getOrderDescription(this) + + // Save filters + viewModel.filters = filters + } + + private fun shouldStartSignIn(): Boolean { + return !viewModel.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) + viewModel.isSigningIn = true + } + + private fun onAddItemsClicked() { + // Add a bunch of random restaurants + val batch = firestore.batch() + for (i in 0..9) { + val restRef = firestore.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) { _, _ -> startSignIn() } + .setNegativeButton(R.string.option_exit) { _, _ -> 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 000000000..d7dcddfe6 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RatingDialogFragment.kt @@ -0,0 +1,77 @@ +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 com.google.firebase.auth.FirebaseAuth +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Rating +import kotlinx.android.synthetic.main.dialog_rating.* +import kotlinx.android.synthetic.main.dialog_rating.view.* + +/** + * Dialog Fragment containing rating form. + */ +class RatingDialogFragment : DialogFragment() { + + private var ratingListener: 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) + + v.restaurantFormButton.setOnClickListener { onSubmitClicked() } + v.restaurantFormCancel.setOnClickListener { onCancelClicked() } + + return v + } + + override fun onAttach(context: Context?) { + super.onAttach(context) + + if (context is RatingListener) { + ratingListener = context + } + } + + override fun onResume() { + super.onResume() + dialog.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + + } + + fun onSubmitClicked() { + val user = FirebaseAuth.getInstance().currentUser + user?.let { + val rating = Rating( + user, + restaurantFormRating.rating.toDouble(), + restaurantFormText.text.toString()) + + ratingListener?.onRating(rating) + } + + dismiss() + } + + fun onCancelClicked() { + 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 000000000..be4463400 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RestaurantDetailActivity.kt @@ -0,0 +1,196 @@ +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.util.Log +import android.view.View +import android.view.inputmethod.InputMethodManager +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 kotlinx.android.synthetic.main.activity_restaurant_detail.* + +class RestaurantDetailActivity : AppCompatActivity(), EventListener, RatingDialogFragment.RatingListener { + + private var ratingDialog: RatingDialogFragment? = null + + private lateinit var firestore: FirebaseFirestore + private lateinit var restaurantRef: DocumentReference + private lateinit var ratingAdapter: RatingAdapter + + private var restaurantRegistration: ListenerRegistration? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_restaurant_detail) + + // Get restaurant ID from extras + val restaurantId = intent.extras?.getString(KEY_RESTAURANT_ID) + ?: throw IllegalArgumentException("Must pass extra $KEY_RESTAURANT_ID") + + // Initialize Firestore + firestore = FirebaseFirestore.getInstance() + + // Get reference to the restaurant + restaurantRef = firestore.collection("restaurants").document(restaurantId) + + // Get ratings + val ratingsQuery = restaurantRef + .collection("ratings") + .orderBy("timestamp", Query.Direction.DESCENDING) + .limit(50) + + // RecyclerView + ratingAdapter = object : RatingAdapter(ratingsQuery) { + override fun onDataChanged() { + if (itemCount == 0) { + recyclerRatings.visibility = View.GONE + viewEmptyRatings.visibility = View.VISIBLE + } else { + recyclerRatings.visibility = View.VISIBLE + viewEmptyRatings.visibility = View.GONE + } + } + } + recyclerRatings.layoutManager = LinearLayoutManager(this) + recyclerRatings.adapter = ratingAdapter + + ratingDialog = RatingDialogFragment() + + restaurantButtonBack.setOnClickListener { onBackArrowClicked() } + fabShowRatingDialog.setOnClickListener { onAddRatingClicked() } + } + + public override fun onStart() { + super.onStart() + + ratingAdapter.startListening() + restaurantRegistration = restaurantRef.addSnapshotListener(this) + } + + public override fun onStop() { + super.onStop() + + ratingAdapter.stopListening() + + restaurantRegistration?.remove() + restaurantRegistration = null + } + + override fun finish() { + super.finish() + overridePendingTransition(R.anim.slide_in_from_left, R.anim.slide_out_to_right) + } + + /** + * Listener for the Restaurant document ([.restaurantRef]). + */ + override fun onEvent(snapshot: DocumentSnapshot?, e: FirebaseFirestoreException?) { + if (e != null) { + Log.w(TAG, "restaurant:onEvent", e) + return + } + + snapshot?.let{ + val restaurant = snapshot.toObject(Restaurant::class.java) + if (restaurant != null) { + onRestaurantLoaded(restaurant) + } + } + } + + private fun onRestaurantLoaded(restaurant: Restaurant) { + restaurantName.text = restaurant.name + restaurantRating.rating = restaurant.avgRating.toFloat() + restaurantNumRatings.text = getString(R.string.fmt_num_ratings, restaurant.numRatings) + restaurantCity.text = restaurant.city + restaurantCategory.text = restaurant.category + restaurantPrice.text = RestaurantUtil.getPriceString(restaurant) + + // Background image + Glide.with(restaurantImage.context) + .load(restaurant.photo) + .into(restaurantImage) + } + + private fun onBackArrowClicked() { + onBackPressed() + } + + private fun onAddRatingClicked() { + ratingDialog?.show(supportFragmentManager, RatingDialogFragment.TAG) + } + + override fun onRating(rating: Rating) { + // In a transaction, add the new rating and update the aggregate totals + addRating(restaurantRef, rating) + .addOnSuccessListener(this) { + Log.d(TAG, "Rating added") + + // Hide keyboard and scroll to top + hideKeyboard() + recyclerRatings.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 firestore.runTransaction { transaction -> + val restaurant = transaction.get(restaurantRef).toObject(Restaurant::class.java) + if (restaurant == null) { + throw Exception("Resraurant not found at ${restaurantRef.path}") + } + + // 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 const val TAG = "RestaurantDetail" + + const 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 000000000..2f7d92372 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/FirestoreAdapter.kt @@ -0,0 +1,114 @@ +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 query: Query?) : RecyclerView.Adapter(), EventListener { + + private var registration: ListenerRegistration? = null + + private val snapshots = ArrayList() + + override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) { + if (e != null) { + Log.w(TAG, "onEvent:error", e) + onError(e) + return + } + + if (documentSnapshots == null) { + 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 (query != null && registration == null) { + registration = query!!.addSnapshotListener(this) + } + } + + fun stopListening() { + registration?.remove() + registration = null + + snapshots.clear() + notifyDataSetChanged() + } + + fun setQuery(query: Query) { + // Stop listening + stopListening() + + // Clear existing data + snapshots.clear() + notifyDataSetChanged() + + // Listen to new query + this.query = query + startListening() + } + + open fun onError(e: FirebaseFirestoreException) { + Log.w(TAG, "onError", e) + } + + open fun onDataChanged() {} + + override fun getItemCount(): Int { + return snapshots.size + } + + protected fun getSnapshot(index: Int): DocumentSnapshot { + return snapshots[index] + } + + protected fun onDocumentAdded(change: DocumentChange) { + snapshots.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 + snapshots[change.oldIndex] = change.document + notifyItemChanged(change.oldIndex) + } else { + // Item changed and changed position + snapshots.removeAt(change.oldIndex) + snapshots.add(change.newIndex, change.document) + notifyItemMoved(change.oldIndex, change.newIndex) + } + } + + protected fun onDocumentRemoved(change: DocumentChange) { + snapshots.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 000000000..73f9c7064 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RatingAdapter.kt @@ -0,0 +1,51 @@ +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 com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.kotlin.model.Rating +import com.google.firebase.firestore.Query +import kotlinx.android.synthetic.main.item_rating.view.* +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) { + + fun bind(rating: Rating?) { + if (rating == null) { + return + } + + itemView.ratingItemName.text = rating.userName + itemView.ratingItemRating.rating = rating.rating.toFloat() + itemView.ratingItemText.text = rating.text + + if (rating.timestamp != null) { + itemView.ratingItemDate.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 000000000..f73669aa0 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RestaurantAdapter.kt @@ -0,0 +1,70 @@ +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 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 kotlinx.android.synthetic.main.item_restaurant.view.* + +/** + * 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) { + + fun bind(snapshot: DocumentSnapshot, + listener: OnRestaurantSelectedListener?) { + + val restaurant = snapshot.toObject(Restaurant::class.java) + if (restaurant == null) { + return + } + + val resources = itemView.resources + + // Load image + Glide.with(itemView.restaurantItemImage.context) + .load(restaurant.photo) + .into(itemView.restaurantItemImage) + + val numRatings: Int = restaurant.numRatings + + itemView.restaurantItemName.text = restaurant.name + itemView.restaurantItemRating.rating = restaurant.avgRating.toFloat() + itemView.restaurantItemCity.text = restaurant.city + itemView.restaurantItemCategory.text = restaurant.category + itemView.restaurantItemNumRatings.text = resources.getString( + R.string.fmt_num_ratings, + numRatings) + itemView.restaurantItemPrice.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 000000000..9b61da86e --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Rating.kt @@ -0,0 +1,29 @@ +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. + */ +data 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(user: FirebaseUser, rating: Double, text: String) : this() { + 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 000000000..4f9b6cf43 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Restaurant.kt @@ -0,0 +1,27 @@ +package com.google.firebase.example.fireeats.kotlin.model + +import com.google.firebase.firestore.IgnoreExtraProperties + +/** + * Restaurant POJO. + */ +@IgnoreExtraProperties +data 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()) { + + 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 000000000..36f74f4ab --- /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 000000000..c8113b60e --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RestaurantUtil.kt @@ -0,0 +1,94 @@ +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 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 000000000..9aea9bf2c --- /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/layout/activity_main.xml b/firestore/app/src/main/res/layout/activity_main.xml index 2edbe83d0..4454815be 100644 --- a/firestore/app/src/main/res/layout/activity_main.xml +++ b/firestore/app/src/main/res/layout/activity_main.xml @@ -4,14 +4,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="#E0E0E0" - tools:context="com.google.firebase.example.fireeats.MainActivity"> + android:background="#E0E0E0"> @@ -110,12 +109,12 @@ diff --git a/firestore/app/src/main/res/layout/activity_restaurant_detail.xml b/firestore/app/src/main/res/layout/activity_restaurant_detail.xml index b643cdf2c..8840c2894 100644 --- a/firestore/app/src/main/res/layout/activity_restaurant_detail.xml +++ b/firestore/app/src/main/res/layout/activity_restaurant_detail.xml @@ -4,8 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="#E0E0E0" - tools:context="com.google.firebase.example.fireeats.RestaurantDetailActivity"> + android:background="#E0E0E0">