diff --git a/analytics/build.gradle b/analytics/build.gradle index 13eea4901..53847afca 100644 --- a/analytics/build.gradle +++ b/analytics/build.gradle @@ -2,9 +2,9 @@ buildscript { repositories { - jcenter() mavenLocal() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -17,7 +17,7 @@ allprojects { repositories { //mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/app-indexing/build.gradle b/app-indexing/build.gradle index 13eea4901..382cbac3c 100644 --- a/app-indexing/build.gradle +++ b/app-indexing/build.gradle @@ -2,9 +2,9 @@ buildscript { repositories { - jcenter() mavenLocal() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -15,9 +15,9 @@ buildscript { allprojects { repositories { - //mavenLocal() must be listed at the top to facilitate testing + // mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/auth/build.gradle b/auth/build.gradle index b76b33748..693acab59 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -2,9 +2,9 @@ buildscript { repositories { - jcenter() mavenLocal() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -17,8 +17,8 @@ allprojects { repositories { //mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/build.gradle b/build.gradle index 4ce962357..d517cb570 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ buildscript { classpath 'com.google.gms:google-services:4.3.5' classpath 'com.google.firebase:perf-plugin:1.3.5' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1' + classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4' } } diff --git a/config/build.gradle b/config/build.gradle index bb9fb3a62..47c5f9296 100644 --- a/config/build.gradle +++ b/config/build.gradle @@ -2,9 +2,9 @@ buildscript { repositories { - jcenter() mavenLocal() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -17,7 +17,7 @@ allprojects { repositories { //mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/crash/build.gradle b/crash/build.gradle index 78fa52eb3..169b757de 100644 --- a/crash/build.gradle +++ b/crash/build.gradle @@ -2,9 +2,9 @@ buildscript { repositories { - jcenter() mavenLocal() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -18,8 +18,8 @@ allprojects { repositories { //mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/database/build.gradle b/database/build.gradle index 0186fd8a8..e41879df6 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -2,9 +2,9 @@ buildscript { repositories { - jcenter() mavenLocal() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -17,8 +17,8 @@ allprojects { repositories { //mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/firestore/app/build.gradle b/firestore/app/build.gradle index c3f18f66d..70e280660 100644 --- a/firestore/app/build.gradle +++ b/firestore/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'androidx.navigation.safeargs' android { testBuildType "release" @@ -62,7 +63,7 @@ dependencies { // Support Libs implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.core:core:1.3.2' + implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.browser:browser:1.0.0' @@ -70,9 +71,11 @@ dependencies { implementation 'androidx.media:media:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.4' // Android architecture components - implementation 'androidx.lifecycle:lifecycle-runtime:2.3.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.3.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 deleted file mode 100644 index 9682814f9..000000000 --- a/firestore/app/src/androidTest/java/com/google/firebase/example/fireeats/MainActivityTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.google.firebase.example.fireeats; - -import android.content.Intent; -import androidx.test.filters.LargeTest; -import androidx.test.rule.ActivityTestRule; -import androidx.test.runner.AndroidJUnit4; -import androidx.test.uiautomator.UiDevice; -import androidx.test.uiautomator.UiObject; -import androidx.test.uiautomator.UiScrollable; -import androidx.test.uiautomator.UiSelector; -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; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static androidx.test.InstrumentationRegistry.getInstrumentation; -import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; - -@LargeTest @RunWith(AndroidJUnit4.class) public class MainActivityTest { - - @Rule public ActivityTestRule mActivityTestRule = - new ActivityTestRule<>(MainActivity.class, false, false); - - UiDevice device; - final long TIMEOUT = 300000; // Five minute timeout because our CI is slooow. - - @Before public void before() { - // Sign out of any existing sessions - FirebaseAuth.getInstance().signOut(); - device = UiDevice.getInstance(getInstrumentation()); - } - - @Test public void testAddItemsAndReview() throws Exception { - mActivityTestRule.launchActivity(new Intent()); - - // Input email for account created in the setup.sh - getById("email").setText("test@mailinator.com"); - closeKeyboard(); - getById("button_next").clickAndWaitForNewWindow(TIMEOUT); - - //Input password - getById("password").setText("password"); - closeKeyboard(); - getById("button_done").clickAndWaitForNewWindow(TIMEOUT); - - // Add random items - getActionBarItem(new UiSelector().textContains("Add Random Items"), TIMEOUT).click(); - device.waitForIdle(TIMEOUT); - - // Click on the first restaurant - getById("recycler_restaurants").getChild(new UiSelector().index(0)) - .clickAndWaitForNewWindow(TIMEOUT); - - // Click add review - getById("fabShowRatingDialog").click(); - - //Write a review - getById("restaurant_form_text").setText("\uD83D\uDE0E\uD83D\uDE00"); - closeKeyboard(); - - //Submit the review - getById("restaurant_form_button").clickAndWaitForNewWindow(TIMEOUT); - - // Assert that the review exists - UiScrollable ratingsList = new UiScrollable(getIdSelector("recyclerRatings")); - ratingsList.waitForExists(TIMEOUT); - ratingsList.scrollToBeginning(100); - Assert.assertTrue( - getById("recyclerRatings") - .getChild(new UiSelector().text("\uD83D\uDE0E\uD83D\uDE00")) - .waitForExists(TIMEOUT)); - } - - private UiObject getById(String id) { - UiObject obj = device.findObject(getIdSelector(id)); - obj.waitForExists(TIMEOUT); - return obj; - } - - private UiSelector getIdSelector(String id) { - return new UiSelector().resourceId("com.google.firebase.example.fireeats:id/" + id); - } - - private void closeKeyboard() { - for (AccessibilityWindowInfo w : getInstrumentation().getUiAutomation().getWindows()) { - if (w.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD) { - device.pressBack(); - return; - } - } - } - - private UiObject getActionBarItem(UiSelector selector, long timeout) throws InterruptedException { - final long STEP_TIMEOUT = 5000; - UiObject item = device.findObject(selector); - - for (long i = 0; i < timeout; i += STEP_TIMEOUT) { - openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); - - if (item.waitForExists(STEP_TIMEOUT)) { - break; - } - } - - return item; - } -} diff --git a/firestore/app/src/main/AndroidManifest.xml b/firestore/app/src/main/AndroidManifest.xml index f17357263..33a73527f 100644 --- a/firestore/app/src/main/AndroidManifest.xml +++ b/firestore/app/src/main/AndroidManifest.xml @@ -22,22 +22,10 @@ - - - - - - - - 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 index d1babb98b..e7726f468 100644 --- 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 @@ -3,6 +3,7 @@ package com.google.firebase.example.fireeats import android.content.Intent import com.firebase.example.internal.BaseEntryChoiceActivity import com.firebase.example.internal.Choice +import com.google.firebase.example.fireeats.kotlin.MainActivity class EntryChoiceActivity : BaseEntryChoiceActivity() { diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java index e2a4f3eb4..595580177 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java @@ -55,8 +55,8 @@ public void onDestroyView() { public void onAttach(Context context) { super.onAttach(context); - if (context instanceof FilterListener) { - mFilterListener = (FilterListener) context; + if (getParentFragment() instanceof FilterListener) { + mFilterListener = (FilterListener) getParentFragment(); } } diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainActivity.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainActivity.java index 496b23579..a8677bf7f 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainActivity.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainActivity.java @@ -1,331 +1,22 @@ package com.google.firebase.example.fireeats.java; -import android.content.DialogInterface; -import android.content.Intent; import android.os.Bundle; -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 androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; -import androidx.core.text.HtmlCompat; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; +import androidx.navigation.Navigation; -import com.firebase.ui.auth.AuthUI; -import com.firebase.ui.auth.ErrorCodes; -import com.firebase.ui.auth.IdpResponse; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.Task; -import com.google.android.material.snackbar.Snackbar; -import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.example.fireeats.R; -import com.google.firebase.example.fireeats.databinding.ActivityMainBinding; -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; -import com.google.firebase.firestore.FirebaseFirestoreException; -import com.google.firebase.firestore.Query; -import com.google.firebase.firestore.WriteBatch; -import java.util.Collections; -import java.util.List; - -public class MainActivity extends AppCompatActivity implements - FilterDialogFragment.FilterListener, - RestaurantAdapter.OnRestaurantSelectedListener, View.OnClickListener { - - private static final String TAG = "MainActivity"; - - private static final int RC_SIGN_IN = 9001; - - private static final int LIMIT = 50; - - private ActivityMainBinding mBinding; - - private FirebaseFirestore mFirestore; - private Query mQuery; - - private FilterDialogFragment mFilterDialog; - private RestaurantAdapter mAdapter; - - private MainActivityViewModel mViewModel; +public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mBinding = ActivityMainBinding.inflate(getLayoutInflater()); - setContentView(mBinding.getRoot()); - - setSupportActionBar(mBinding.toolbar); - - mBinding.filterBar.setOnClickListener(this); - mBinding.buttonClearFilter.setOnClickListener(this); - - // View model - mViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class); - - // Enable Firestore logging - FirebaseFirestore.setLoggingEnabled(true); - - // Firestore - mFirestore = FirebaseFirestore.getInstance(); - - // Get ${LIMIT} restaurants - mQuery = mFirestore.collection("restaurants") - .orderBy("avgRating", Query.Direction.DESCENDING) - .limit(LIMIT); - - // RecyclerView - mAdapter = new RestaurantAdapter(mQuery, this) { - @Override - protected void onDataChanged() { - // Show/hide content if the query returns empty. - if (getItemCount() == 0) { - mBinding.recyclerRestaurants.setVisibility(View.GONE); - mBinding.viewEmpty.setVisibility(View.VISIBLE); - } else { - mBinding.recyclerRestaurants.setVisibility(View.VISIBLE); - mBinding.viewEmpty.setVisibility(View.GONE); - } - } - - @Override - protected void onError(FirebaseFirestoreException e) { - // Show a snackbar on errors - Snackbar.make(mBinding.getRoot(), - "Error: check logs for info.", Snackbar.LENGTH_LONG).show(); - } - }; - - mBinding.recyclerRestaurants.setLayoutManager(new LinearLayoutManager(this)); - mBinding.recyclerRestaurants.setAdapter(mAdapter); - - // Filter Dialog - mFilterDialog = new FilterDialogFragment(); - } + setContentView(R.layout.activity_main); + setSupportActionBar(this.findViewById(R.id.toolbar)); - @Override - public void onStart() { - super.onStart(); - - // Start sign in if necessary - if (shouldStartSignIn()) { - startSignIn(); - return; - } - - // Apply filters - onFilter(mViewModel.getFilters()); - - // Start listening for Firestore updates - if (mAdapter != null) { - mAdapter.startListening(); - } - } - - @Override - public void onStop() { - super.onStop(); - if (mAdapter != null) { - mAdapter.stopListening(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_main, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_add_items: - onAddItemsClicked(); - break; - case R.id.menu_sign_out: - AuthUI.getInstance().signOut(this); - startSignIn(); - break; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == RC_SIGN_IN) { - IdpResponse response = IdpResponse.fromResultIntent(data); - mViewModel.setIsSigningIn(false); - - if (resultCode != RESULT_OK) { - if (response == null) { - // User pressed the back button. - finish(); - } else if (response.getError() != null - && response.getError().getErrorCode() == ErrorCodes.NO_NETWORK) { - showSignInErrorDialog(R.string.message_no_network); - } else { - showSignInErrorDialog(R.string.message_unknown); - } - } - } - } - - public void onFilterClicked() { - // Show the dialog containing filter options - mFilterDialog.show(getSupportFragmentManager(), FilterDialogFragment.TAG); - } - - public void onClearFilterClicked() { - mFilterDialog.resetFilters(); - - onFilter(Filters.getDefault()); - } - - @Override - public void onRestaurantSelected(DocumentSnapshot restaurant) { - // Go to the details page for the selected restaurant - Intent intent = new Intent(this, RestaurantDetailActivity.class); - intent.putExtra(RestaurantDetailActivity.KEY_RESTAURANT_ID, restaurant.getId()); - - startActivity(intent); - overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left); - } - - @Override - public void onFilter(Filters filters) { - // Construct query basic query - Query query = mFirestore.collection("restaurants"); - - // Category (equality filter) - if (filters.hasCategory()) { - query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.getCategory()); - } - - // City (equality filter) - if (filters.hasCity()) { - query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.getCity()); - } - - // Price (equality filter) - if (filters.hasPrice()) { - query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.getPrice()); - } - - // Sort by (orderBy with direction) - if (filters.hasSortBy()) { - query = query.orderBy(filters.getSortBy(), filters.getSortDirection()); - } - - // Limit items - query = query.limit(LIMIT); - - // Update the query - mAdapter.setQuery(query); - - // Set header - mBinding.textCurrentSearch.setText(HtmlCompat.fromHtml(filters.getSearchDescription(this), - HtmlCompat.FROM_HTML_MODE_LEGACY)); - mBinding.textCurrentSortBy.setText(filters.getOrderDescription(this)); - - // Save filters - mViewModel.setFilters(filters); - } - - private boolean shouldStartSignIn() { - return (!mViewModel.getIsSigningIn() && FirebaseAuth.getInstance().getCurrentUser() == null); - } - - private void startSignIn() { - // Sign in with FirebaseUI - Intent intent = AuthUI.getInstance().createSignInIntentBuilder() - .setAvailableProviders(Collections.singletonList( - new AuthUI.IdpConfig.EmailBuilder().build())) - .setIsSmartLockEnabled(false) - .build(); - - startActivityForResult(intent, RC_SIGN_IN); - mViewModel.setIsSigningIn(true); - } - - private void onAddItemsClicked() { - // Add a bunch of random restaurants - WriteBatch batch = mFirestore.batch(); - for (int i = 0; i < 10; i++) { - DocumentReference restRef = mFirestore.collection("restaurants").document(); - - // Create random restaurant / ratings - Restaurant randomRestaurant = RestaurantUtil.getRandom(this); - List randomRatings = RatingUtil.getRandomList(randomRestaurant.getNumRatings()); - randomRestaurant.setAvgRating(RatingUtil.getAverageRating(randomRatings)); - - // Add restaurant - batch.set(restRef, randomRestaurant); - - // Add ratings to subcollection - for (Rating rating : randomRatings) { - batch.set(restRef.collection("ratings").document(), rating); - } - } - - batch.commit().addOnCompleteListener(new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - Log.d(TAG, "Write batch succeeded."); - } else { - Log.w(TAG, "write batch failed.", task.getException()); - } - } - }); - } - - private void showSignInErrorDialog(@StringRes int message) { - AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle(R.string.title_sign_in_error) - .setMessage(message) - .setCancelable(false) - .setPositiveButton(R.string.option_retry, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - startSignIn(); - } - }) - .setNegativeButton(R.string.option_exit, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - finish(); - } - }).create(); - - dialog.show(); - } - - @Override - public void onClick(View v) { - switch (v.getId()) { - case R.id.filterBar: - onFilterClicked(); - break; - case R.id.buttonClearFilter: - onClearFilterClicked(); - break; - } + Navigation.findNavController(this, R.id.nav_host_fragment) + .setGraph(R.navigation.nav_graph_java); } } diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainFragment.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainFragment.java new file mode 100644 index 000000000..9f01773c1 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainFragment.java @@ -0,0 +1,341 @@ +package com.google.firebase.example.fireeats.java; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.text.HtmlCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavAction; +import androidx.navigation.NavController; +import androidx.navigation.NavOptions; +import androidx.navigation.NavOptionsBuilder; +import androidx.navigation.Navigation; +import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.firebase.ui.auth.AuthUI; +import com.firebase.ui.auth.ErrorCodes; +import com.firebase.ui.auth.IdpResponse; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.android.material.snackbar.Snackbar; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.example.fireeats.R; +import com.google.firebase.example.fireeats.databinding.FragmentMainBinding; +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; +import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.WriteBatch; + +import java.util.Collections; +import java.util.List; + +public class MainFragment extends Fragment implements + FilterDialogFragment.FilterListener, + RestaurantAdapter.OnRestaurantSelectedListener, View.OnClickListener { + + private static final String TAG = "MainActivity"; + + private static final int RC_SIGN_IN = 9001; + + private static final int LIMIT = 50; + + private FragmentMainBinding mBinding; + + private FirebaseFirestore mFirestore; + private Query mQuery; + + private FilterDialogFragment mFilterDialog; + private RestaurantAdapter mAdapter; + + private MainActivityViewModel mViewModel; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mBinding = FragmentMainBinding.inflate(inflater, container, false); + return mBinding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mBinding.filterBar.setOnClickListener(this); + mBinding.buttonClearFilter.setOnClickListener(this); + + // View model + mViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class); + + // Enable Firestore logging + FirebaseFirestore.setLoggingEnabled(true); + + // Firestore + mFirestore = FirebaseFirestore.getInstance(); + + // Get ${LIMIT} restaurants + mQuery = mFirestore.collection("restaurants") + .orderBy("avgRating", Query.Direction.DESCENDING) + .limit(LIMIT); + + // RecyclerView + mAdapter = new RestaurantAdapter(mQuery, this) { + @Override + protected void onDataChanged() { + // Show/hide content if the query returns empty. + if (getItemCount() == 0) { + mBinding.recyclerRestaurants.setVisibility(View.GONE); + mBinding.viewEmpty.setVisibility(View.VISIBLE); + } else { + mBinding.recyclerRestaurants.setVisibility(View.VISIBLE); + mBinding.viewEmpty.setVisibility(View.GONE); + } + } + + @Override + protected void onError(FirebaseFirestoreException e) { + // Show a snackbar on errors + Snackbar.make(mBinding.getRoot(), + "Error: check logs for info.", Snackbar.LENGTH_LONG).show(); + } + }; + + mBinding.recyclerRestaurants.setLayoutManager(new LinearLayoutManager(requireContext())); + mBinding.recyclerRestaurants.setAdapter(mAdapter); + + // Filter Dialog + mFilterDialog = new FilterDialogFragment(); + } + + @Override + public void onStart() { + super.onStart(); + + // Start sign in if necessary + if (shouldStartSignIn()) { + startSignIn(); + return; + } + + // Apply filters + onFilter(mViewModel.getFilters()); + + // Start listening for Firestore updates + if (mAdapter != null) { + mAdapter.startListening(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (mAdapter != null) { + mAdapter.stopListening(); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_main, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_add_items: + onAddItemsClicked(); + break; + case R.id.menu_sign_out: + AuthUI.getInstance().signOut(requireContext()); + startSignIn(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == RC_SIGN_IN) { + IdpResponse response = IdpResponse.fromResultIntent(data); + mViewModel.setIsSigningIn(false); + + if (resultCode != Activity.RESULT_OK) { + if (response == null) { + // User pressed the back button. + requireActivity().finish(); + } else if (response.getError() != null + && response.getError().getErrorCode() == ErrorCodes.NO_NETWORK) { + showSignInErrorDialog(R.string.message_no_network); + } else { + showSignInErrorDialog(R.string.message_unknown); + } + } + } + } + + public void onFilterClicked() { + // Show the dialog containing filter options + mFilterDialog.show(getChildFragmentManager(), FilterDialogFragment.TAG); + } + + public void onClearFilterClicked() { + mFilterDialog.resetFilters(); + + onFilter(Filters.getDefault()); + } + + @Override + public void onRestaurantSelected(DocumentSnapshot restaurant) { + // Go to the details page for the selected restaurant + MainFragmentDirections.ActionMainFragmentToRestaurantDetailFragment action = MainFragmentDirections + .actionMainFragmentToRestaurantDetailFragment(restaurant.getId()); + + NavHostFragment.findNavController(this) + .navigate(action); + } + + @Override + public void onFilter(Filters filters) { + // Construct query basic query + Query query = mFirestore.collection("restaurants"); + + // Category (equality filter) + if (filters.hasCategory()) { + query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.getCategory()); + } + + // City (equality filter) + if (filters.hasCity()) { + query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.getCity()); + } + + // Price (equality filter) + if (filters.hasPrice()) { + query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.getPrice()); + } + + // Sort by (orderBy with direction) + if (filters.hasSortBy()) { + query = query.orderBy(filters.getSortBy(), filters.getSortDirection()); + } + + // Limit items + query = query.limit(LIMIT); + + // Update the query + mAdapter.setQuery(query); + + // Set header + mBinding.textCurrentSearch.setText(HtmlCompat.fromHtml(filters.getSearchDescription(requireContext()), + HtmlCompat.FROM_HTML_MODE_LEGACY)); + mBinding.textCurrentSortBy.setText(filters.getOrderDescription(requireContext())); + + // Save filters + mViewModel.setFilters(filters); + } + + private boolean shouldStartSignIn() { + return (!mViewModel.getIsSigningIn() && FirebaseAuth.getInstance().getCurrentUser() == null); + } + + private void startSignIn() { + // Sign in with FirebaseUI + Intent intent = AuthUI.getInstance().createSignInIntentBuilder() + .setAvailableProviders(Collections.singletonList( + new AuthUI.IdpConfig.EmailBuilder().build())) + .setIsSmartLockEnabled(false) + .build(); + + startActivityForResult(intent, RC_SIGN_IN); + mViewModel.setIsSigningIn(true); + } + + private void onAddItemsClicked() { + // Add a bunch of random restaurants + WriteBatch batch = mFirestore.batch(); + for (int i = 0; i < 10; i++) { + DocumentReference restRef = mFirestore.collection("restaurants").document(); + + // Create random restaurant / ratings + Restaurant randomRestaurant = RestaurantUtil.getRandom(requireContext()); + List randomRatings = RatingUtil.getRandomList(randomRestaurant.getNumRatings()); + randomRestaurant.setAvgRating(RatingUtil.getAverageRating(randomRatings)); + + // Add restaurant + batch.set(restRef, randomRestaurant); + + // Add ratings to subcollection + for (Rating rating : randomRatings) { + batch.set(restRef.collection("ratings").document(), rating); + } + } + + batch.commit().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "Write batch succeeded."); + } else { + Log.w(TAG, "write batch failed.", task.getException()); + } + } + }); + } + + private void showSignInErrorDialog(@StringRes int message) { + AlertDialog dialog = new AlertDialog.Builder(requireContext()) + .setTitle(R.string.title_sign_in_error) + .setMessage(message) + .setCancelable(false) + .setPositiveButton(R.string.option_retry, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + startSignIn(); + } + }) + .setNegativeButton(R.string.option_exit, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + requireActivity().finish(); + } + }).create(); + + dialog.show(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.filterBar: + onFilterClicked(); + break; + case R.id.buttonClearFilter: + onClearFilterClicked(); + break; + } + } +} diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RatingDialogFragment.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RatingDialogFragment.java index d8db89f9f..b0dd21e96 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RatingDialogFragment.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RatingDialogFragment.java @@ -57,8 +57,8 @@ public void onDestroyView() { public void onAttach(Context context) { super.onAttach(context); - if (context instanceof RatingListener) { - mRatingListener = (RatingListener) context; + if (getParentFragment() instanceof RatingListener) { + mRatingListener = (RatingListener) getParentFragment(); } } diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailActivity.java b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailFragment.java similarity index 85% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailActivity.java rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailFragment.java index d4242271c..6eee3cb82 100644 --- a/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailActivity.java +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailFragment.java @@ -3,11 +3,15 @@ import android.content.Context; import android.os.Bundle; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; @@ -16,7 +20,7 @@ import com.google.android.gms.tasks.Task; import com.google.android.material.snackbar.Snackbar; import com.google.firebase.example.fireeats.R; -import com.google.firebase.example.fireeats.databinding.ActivityRestaurantDetailBinding; +import com.google.firebase.example.fireeats.databinding.FragmentRestaurantDetailBinding; 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; @@ -30,14 +34,12 @@ import com.google.firebase.firestore.Query; import com.google.firebase.firestore.Transaction; -public class RestaurantDetailActivity extends AppCompatActivity +public class RestaurantDetailFragment extends Fragment implements EventListener, RatingDialogFragment.RatingListener, View.OnClickListener { private static final String TAG = "RestaurantDetail"; - public static final String KEY_RESTAURANT_ID = "key_restaurant_id"; - - private ActivityRestaurantDetailBinding mBinding; + private FragmentRestaurantDetailBinding mBinding; private RatingDialogFragment mRatingDialog; @@ -47,20 +49,21 @@ public class RestaurantDetailActivity extends AppCompatActivity private RatingAdapter mRatingAdapter; + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mBinding = FragmentRestaurantDetailBinding.inflate(inflater, container, false); + return mBinding.getRoot(); + } + @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mBinding = ActivityRestaurantDetailBinding.inflate(getLayoutInflater()); - setContentView(mBinding.getRoot()); + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); mBinding.restaurantButtonBack.setOnClickListener(this); mBinding.fabShowRatingDialog.setOnClickListener(this); - // Get restaurant ID from extras - String restaurantId = getIntent().getExtras().getString(KEY_RESTAURANT_ID); - if (restaurantId == null) { - throw new IllegalArgumentException("Must pass extra " + KEY_RESTAURANT_ID); - } + String restaurantId = RestaurantDetailFragmentArgs.fromBundle(getArguments()).getKeyRestaurantId(); // Initialize Firestore mFirestore = FirebaseFirestore.getInstance(); @@ -87,7 +90,7 @@ protected void onDataChanged() { } } }; - mBinding.recyclerRatings.setLayoutManager(new LinearLayoutManager(this)); + mBinding.recyclerRatings.setLayoutManager(new LinearLayoutManager(requireContext())); mBinding.recyclerRatings.setAdapter(mRatingAdapter); mRatingDialog = new RatingDialogFragment(); @@ -113,12 +116,6 @@ public void onStop() { } } - @Override - public void finish() { - super.finish(); - overridePendingTransition(R.anim.slide_in_from_left, R.anim.slide_out_to_right); - } - /** * Listener for the Restaurant document ({@link #mRestaurantRef}). */ @@ -147,18 +144,18 @@ private void onRestaurantLoaded(Restaurant restaurant) { } public void onBackArrowClicked(View view) { - onBackPressed(); + requireActivity().onBackPressed(); } public void onAddRatingClicked(View view) { - mRatingDialog.show(getSupportFragmentManager(), RatingDialogFragment.TAG); + mRatingDialog.show(getChildFragmentManager(), RatingDialogFragment.TAG); } @Override public void onRating(Rating rating) { // In a transaction, add the new rating and update the aggregate totals addRating(mRestaurantRef, rating) - .addOnSuccessListener(this, new OnSuccessListener() { + .addOnSuccessListener(requireActivity(), new OnSuccessListener() { @Override public void onSuccess(Void aVoid) { Log.d(TAG, "Rating added"); @@ -168,7 +165,7 @@ public void onSuccess(Void aVoid) { mBinding.recyclerRatings.smoothScrollToPosition(0); } }) - .addOnFailureListener(this, new OnFailureListener() { + .addOnFailureListener(requireActivity(), new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.w(TAG, "Add rating failed", e); @@ -212,9 +209,9 @@ public Void apply(Transaction transaction) throws FirebaseFirestoreException { } private void hideKeyboard() { - View view = getCurrentFocus(); + View view = requireActivity().getCurrentFocus(); if (view != null) { - ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)) + ((InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(view.getWindowToken(), 0); } } 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 index 281203a11..5ff11aca0 100644 --- 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 @@ -122,8 +122,8 @@ class FilterDialogFragment : DialogFragment() { override fun onAttach(context: Context) { super.onAttach(context) - if (context is FilterListener) { - filterListener = context + if (parentFragment is FilterListener) { + filterListener = parentFragment as FilterListener } } 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 index 91f4b3d6f..5ff7f3654 100644 --- 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 @@ -1,276 +1,16 @@ package com.google.firebase.example.fireeats.kotlin -import android.app.Activity -import android.content.Intent import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.annotation.StringRes -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.core.text.HtmlCompat -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import com.firebase.ui.auth.AuthUI -import com.firebase.ui.auth.ErrorCodes -import com.firebase.ui.auth.IdpResponse -import com.google.android.material.snackbar.Snackbar -import com.google.firebase.auth.ktx.auth +import androidx.navigation.Navigation import com.google.firebase.example.fireeats.R -import com.google.firebase.example.fireeats.databinding.ActivityMainBinding -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 com.google.firebase.firestore.ktx.firestore -import com.google.firebase.ktx.Firebase - -class MainActivity : AppCompatActivity(), - FilterDialogFragment.FilterListener, - RestaurantAdapter.OnRestaurantSelectedListener { - - lateinit var firestore: FirebaseFirestore - lateinit var query: Query - - private lateinit var binding: ActivityMainBinding - private lateinit var filterDialog: FilterDialogFragment - lateinit var adapter: RestaurantAdapter - - private lateinit var viewModel: MainActivityViewModel +class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - setSupportActionBar(binding.toolbar) - - // View model - viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java) - - // Enable Firestore logging - FirebaseFirestore.setLoggingEnabled(true) - - // Firestore - firestore = Firebase.firestore - - // 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) { - binding.recyclerRestaurants.visibility = View.GONE - binding.viewEmpty.visibility = View.VISIBLE - } else { - binding.recyclerRestaurants.visibility = View.VISIBLE - binding.viewEmpty.visibility = View.GONE - } - } - - override fun onError(e: FirebaseFirestoreException) { - // Show a snackbar on errors - Snackbar.make(binding.root, - "Error: check logs for info.", Snackbar.LENGTH_LONG).show() - } - } - - binding.recyclerRestaurants.layoutManager = LinearLayoutManager(this) - binding.recyclerRestaurants.adapter = adapter - - // Filter Dialog - filterDialog = FilterDialogFragment() - - binding.filterBar.setOnClickListener { onFilterClicked() } - binding.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) - } - } - } - } - - private fun onFilterClicked() { - // Show the dialog containing filter options - filterDialog.show(supportFragmentManager, FilterDialogFragment.TAG) - } - - private 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 - binding.textCurrentSearch.text = HtmlCompat.fromHtml(filters.getSearchDescription(this), - HtmlCompat.FROM_HTML_MODE_LEGACY) - binding.textCurrentSortBy.text = filters.getOrderDescription(this) - - // Save filters - viewModel.filters = filters - } - - private fun shouldStartSignIn(): Boolean { - return !viewModel.isSigningIn && Firebase.auth.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 const val TAG = "MainActivity" - - private const val RC_SIGN_IN = 9001 - - private const val LIMIT = 50 + setContentView(R.layout.activity_main) + setSupportActionBar(findViewById(R.id.toolbar)) + Navigation.findNavController(this, R.id.nav_host_fragment) + .setGraph(R.navigation.nav_graph_kotlin) } -} +} \ No newline at end of file diff --git a/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainFragment.kt b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainFragment.kt new file mode 100644 index 000000000..f98c379c6 --- /dev/null +++ b/firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainFragment.kt @@ -0,0 +1,281 @@ +package com.google.firebase.example.fireeats.kotlin + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.text.HtmlCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.firebase.ui.auth.AuthUI +import com.firebase.ui.auth.ErrorCodes +import com.firebase.ui.auth.IdpResponse +import com.google.android.material.snackbar.Snackbar +import com.google.firebase.auth.ktx.auth +import com.google.firebase.example.fireeats.R +import com.google.firebase.example.fireeats.databinding.FragmentMainBinding +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 com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase + +class MainFragment : Fragment(), + FilterDialogFragment.FilterListener, + RestaurantAdapter.OnRestaurantSelectedListener { + + lateinit var firestore: FirebaseFirestore + lateinit var query: Query + + private lateinit var binding: FragmentMainBinding + private lateinit var filterDialog: FilterDialogFragment + lateinit var adapter: RestaurantAdapter + + private lateinit var viewModel: MainActivityViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = FragmentMainBinding.inflate(inflater, container, false); + return binding.root; + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // View model + viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java) + + // Enable Firestore logging + FirebaseFirestore.setLoggingEnabled(true) + + // Firestore + firestore = Firebase.firestore + + // Get ${LIMIT} restaurants + query = firestore.collection("restaurants") + .orderBy("avgRating", Query.Direction.DESCENDING) + .limit(LIMIT.toLong()) + + // RecyclerView + adapter = object : RestaurantAdapter(query, this@MainFragment) { + override fun onDataChanged() { + // Show/hide content if the query returns empty. + if (itemCount == 0) { + binding.recyclerRestaurants.visibility = View.GONE + binding.viewEmpty.visibility = View.VISIBLE + } else { + binding.recyclerRestaurants.visibility = View.VISIBLE + binding.viewEmpty.visibility = View.GONE + } + } + + override fun onError(e: FirebaseFirestoreException) { + // Show a snackbar on errors + Snackbar.make(binding.root, + "Error: check logs for info.", Snackbar.LENGTH_LONG).show() + } + } + + binding.recyclerRestaurants.layoutManager = LinearLayoutManager(context) + binding.recyclerRestaurants.adapter = adapter + + // Filter Dialog + filterDialog = FilterDialogFragment() + + binding.filterBar.setOnClickListener { onFilterClicked() } + binding.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, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_main, menu) + return super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add_items -> onAddItemsClicked() + R.id.menu_sign_out -> { + AuthUI.getInstance().signOut(requireContext()) + 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. + requireActivity().finish() + } else if (response.error != null && response.error!!.errorCode == ErrorCodes.NO_NETWORK) { + showSignInErrorDialog(R.string.message_no_network) + } else { + showSignInErrorDialog(R.string.message_unknown) + } + } + } + } + + private fun onFilterClicked() { + // Show the dialog containing filter options + filterDialog.show(childFragmentManager, FilterDialogFragment.TAG) + } + + private fun onClearFilterClicked() { + filterDialog.resetFilters() + + onFilter(Filters.default) + } + + override fun onRestaurantSelected(restaurant: DocumentSnapshot) { + // Go to the details page for the selected restaurant + val action = MainFragmentDirections + .actionMainFragmentToRestaurantDetailFragment(restaurant.id) + + findNavController().navigate(action) + } + + 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 + binding.textCurrentSearch.text = HtmlCompat.fromHtml(filters.getSearchDescription(requireContext()), + HtmlCompat.FROM_HTML_MODE_LEGACY) + binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext()) + + // Save filters + viewModel.filters = filters + } + + private fun shouldStartSignIn(): Boolean { + return !viewModel.isSigningIn && Firebase.auth.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(requireContext()) + 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(requireContext()) + .setTitle(R.string.title_sign_in_error) + .setMessage(message) + .setCancelable(false) + .setPositiveButton(R.string.option_retry) { _, _ -> startSignIn() } + .setNegativeButton(R.string.option_exit) { _, _ -> requireActivity().finish() }.create() + + dialog.show() + } + + companion object { + + private const val TAG = "MainActivity" + + private const val RC_SIGN_IN = 9001 + + private const 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 index ba7cc6399..11b77a404 100644 --- 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 @@ -46,8 +46,8 @@ class RatingDialogFragment : DialogFragment() { override fun onAttach(context: Context) { super.onAttach(context) - if (context is RatingListener) { - ratingListener = context + if (parentFragment is RatingListener) { + ratingListener = parentFragment as RatingListener } } 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/RestaurantDetailFragment.kt similarity index 83% rename from firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RestaurantDetailActivity.kt rename to firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RestaurantDetailFragment.kt index c58bb07d1..8905be2a6 100644 --- 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/RestaurantDetailFragment.kt @@ -3,15 +3,17 @@ package com.google.firebase.example.fireeats.kotlin import android.content.Context import android.os.Bundle import android.util.Log +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.view.inputmethod.InputMethodManager -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide import com.google.android.gms.tasks.Task import com.google.android.material.snackbar.Snackbar import com.google.firebase.example.fireeats.R -import com.google.firebase.example.fireeats.databinding.ActivityRestaurantDetailBinding +import com.google.firebase.example.fireeats.databinding.FragmentRestaurantDetailBinding 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 @@ -27,27 +29,29 @@ import com.google.firebase.firestore.ktx.firestore import com.google.firebase.firestore.ktx.toObject import com.google.firebase.ktx.Firebase -class RestaurantDetailActivity : AppCompatActivity(), - EventListener, +class RestaurantDetailFragment : Fragment(), + EventListener, RatingDialogFragment.RatingListener { private var ratingDialog: RatingDialogFragment? = null - private lateinit var binding: ActivityRestaurantDetailBinding + private lateinit var binding: FragmentRestaurantDetailBinding 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) - binding = ActivityRestaurantDetailBinding.inflate(layoutInflater) - setContentView(binding.root) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = FragmentRestaurantDetailBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) // Get restaurant ID from extras - val restaurantId = intent.extras?.getString(KEY_RESTAURANT_ID) - ?: throw IllegalArgumentException("Must pass extra $KEY_RESTAURANT_ID") + val restaurantId = RestaurantDetailFragmentArgs.fromBundle(requireArguments()).keyRestaurantId // Initialize Firestore firestore = Firebase.firestore @@ -73,7 +77,7 @@ class RestaurantDetailActivity : AppCompatActivity(), } } } - binding.recyclerRatings.layoutManager = LinearLayoutManager(this) + binding.recyclerRatings.layoutManager = LinearLayoutManager(context) binding.recyclerRatings.adapter = ratingAdapter ratingDialog = RatingDialogFragment() @@ -98,11 +102,6 @@ class RestaurantDetailActivity : AppCompatActivity(), 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]). */ @@ -135,29 +134,30 @@ class RestaurantDetailActivity : AppCompatActivity(), } private fun onBackArrowClicked() { - onBackPressed() + requireActivity().onBackPressed() } private fun onAddRatingClicked() { - ratingDialog?.show(supportFragmentManager, RatingDialogFragment.TAG) + ratingDialog?.show(childFragmentManager, RatingDialogFragment.TAG) } override fun onRating(rating: Rating) { // In a transaction, add the new rating and update the aggregate totals addRating(restaurantRef, rating) - .addOnSuccessListener(this) { + .addOnSuccessListener(requireActivity()) { Log.d(TAG, "Rating added") // Hide keyboard and scroll to top hideKeyboard() binding.recyclerRatings.smoothScrollToPosition(0) } - .addOnFailureListener(this) { e -> + .addOnFailureListener(requireActivity()) { 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.make( + requireView().findViewById(android.R.id.content), "Failed to add rating", Snackbar.LENGTH_SHORT).show() } } @@ -193,9 +193,9 @@ class RestaurantDetailActivity : AppCompatActivity(), } private fun hideKeyboard() { - val view = currentFocus + val view = requireActivity().currentFocus if (view != null) { - (getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + (requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) .hideSoftInputFromWindow(view.windowToken, 0) } } diff --git a/firestore/app/src/main/res/layout/activity_main.xml b/firestore/app/src/main/res/layout/activity_main.xml index 5279edeec..571ea7f12 100644 --- a/firestore/app/src/main/res/layout/activity_main.xml +++ b/firestore/app/src/main/res/layout/activity_main.xml @@ -1,11 +1,11 @@ - + xmlns:tools="http://schemas.android.com/tools" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:viewBindingIgnore="true"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@+id/toolbar" /> - + \ No newline at end of file diff --git a/firestore/app/src/main/res/layout/dialog_filters.xml b/firestore/app/src/main/res/layout/dialog_filters.xml index 870596650..c086f8807 100644 --- a/firestore/app/src/main/res/layout/dialog_filters.xml +++ b/firestore/app/src/main/res/layout/dialog_filters.xml @@ -76,7 +76,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firestore/app/src/main/res/layout/activity_restaurant_detail.xml b/firestore/app/src/main/res/layout/fragment_restaurant_detail.xml similarity index 100% rename from firestore/app/src/main/res/layout/activity_restaurant_detail.xml rename to firestore/app/src/main/res/layout/fragment_restaurant_detail.xml diff --git a/firestore/app/src/main/res/navigation/nav_graph_java.xml b/firestore/app/src/main/res/navigation/nav_graph_java.xml new file mode 100644 index 000000000..77c4fec18 --- /dev/null +++ b/firestore/app/src/main/res/navigation/nav_graph_java.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/firestore/app/src/main/res/navigation/nav_graph_kotlin.xml b/firestore/app/src/main/res/navigation/nav_graph_kotlin.xml new file mode 100644 index 000000000..57f2f6a54 --- /dev/null +++ b/firestore/app/src/main/res/navigation/nav_graph_kotlin.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/firestore/build.gradle b/firestore/build.gradle index 909a2ecef..76c2fa965 100644 --- a/firestore/build.gradle +++ b/firestore/build.gradle @@ -2,14 +2,15 @@ buildscript { repositories { - jcenter() google() + jcenter() mavenLocal() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.google.gms:google-services:4.3.5' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31' + classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4' } } @@ -17,8 +18,8 @@ allprojects { repositories { //mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/functions/build.gradle b/functions/build.gradle index 0186fd8a8..e41879df6 100644 --- a/functions/build.gradle +++ b/functions/build.gradle @@ -2,9 +2,9 @@ buildscript { repositories { - jcenter() mavenLocal() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -17,8 +17,8 @@ allprojects { repositories { //mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/messaging/build.gradle b/messaging/build.gradle index de230dc32..47c5f9296 100644 --- a/messaging/build.gradle +++ b/messaging/build.gradle @@ -3,8 +3,8 @@ buildscript { repositories { mavenLocal() - jcenter() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -17,7 +17,7 @@ allprojects { repositories { //mavenLocal() must be listed at the top to facilitate testing mavenLocal() - jcenter() google() + jcenter() } } diff --git a/storage/build.gradle b/storage/build.gradle index fc09e1ccd..1ae960f63 100644 --- a/storage/build.gradle +++ b/storage/build.gradle @@ -2,9 +2,9 @@ buildscript { repositories { - jcenter() mavenLocal() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' @@ -15,9 +15,9 @@ buildscript { allprojects { repositories { - jcenter() mavenLocal() google() + jcenter() } }