diff --git a/README.md b/README.md index d8742b9..f7084a9 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,8 @@ As mistakes in the original repository are fixed, the code will be + + - - - - - + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ShoppingListApplication.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ShoppingListApplication.java index 9038857..62c7265 100755 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ShoppingListApplication.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ShoppingListApplication.java @@ -12,6 +12,8 @@ public void onCreate() { super.onCreate(); /* Initialize Firebase */ Firebase.setAndroidContext(this); + /* Enable disk persistence */ + Firebase.getDefaultConfig().setPersistenceEnabled(true); } } \ No newline at end of file diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/ShoppingList.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/ShoppingList.java index e132b6a..f488b74 100644 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/ShoppingList.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/ShoppingList.java @@ -14,6 +14,9 @@ public class ShoppingList { private String listName; private String owner; private HashMap timestampLastChanged; + private HashMap timestampCreated; + private HashMap timestampLastChangedReverse; + private HashMap usersShopping; /** * Required public constructor @@ -28,15 +31,16 @@ public ShoppingList() { * * @param listName * @param owner - * */ - public ShoppingList(String listName, String owner) { + public ShoppingList(String listName, String owner, HashMap timestampCreated) { this.listName = listName; this.owner = owner; - HashMap timestampLastChangedObj = new HashMap(); - timestampLastChangedObj.put(Constants.FIREBASE_PROPERTY_TIMESTAMP, ServerValue.TIMESTAMP); - this.timestampLastChanged = timestampLastChangedObj; - + this.timestampCreated = timestampCreated; + HashMap timestampNowObject = new HashMap(); + timestampNowObject.put(Constants.FIREBASE_PROPERTY_TIMESTAMP, ServerValue.TIMESTAMP); + this.timestampLastChanged = timestampNowObject; + this.timestampLastChangedReverse = null; + this.usersShopping = new HashMap<>(); } public String getListName() { @@ -51,6 +55,13 @@ public HashMap getTimestampLastChanged() { return timestampLastChanged; } + public HashMap getTimestampCreated() { + return timestampCreated; + } + + public HashMap getTimestampLastChangedReverse() { + return timestampLastChangedReverse; + } @JsonIgnore public long getTimestampLastChangedLong() { @@ -58,5 +69,27 @@ public long getTimestampLastChangedLong() { return (long) timestampLastChanged.get(Constants.FIREBASE_PROPERTY_TIMESTAMP); } + @JsonIgnore + public long getTimestampCreatedLong() { + return (long) timestampLastChanged.get(Constants.FIREBASE_PROPERTY_TIMESTAMP); + } + + @JsonIgnore + public long getTimestampLastChangedReverseLong() { + + return (long) timestampLastChangedReverse.get(Constants.FIREBASE_PROPERTY_TIMESTAMP); + } + + public HashMap getUsersShopping() { + return usersShopping; + } + + public void setTimestampLastChangedToNow() { + HashMap timestampNowObject = new HashMap(); + timestampNowObject.put(Constants.FIREBASE_PROPERTY_TIMESTAMP, ServerValue.TIMESTAMP); + this.timestampLastChanged = timestampNowObject; + } + + } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/ShoppingListItem.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/ShoppingListItem.java new file mode 100644 index 0000000..483c623 --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/ShoppingListItem.java @@ -0,0 +1,47 @@ +package com.udacity.firebase.shoppinglistplusplus.model; + +/** + * Defines the data structure for ShoppingListItem objects. + */ +public class ShoppingListItem { + private String itemName; + private String owner; + private String boughtBy; + private boolean bought; + + /** + * Required public constructor + */ + public ShoppingListItem() { + } + + /** + * Use this constructor to create new ShoppingListItem. + * Takes shopping list item name and list item owner email as params + * + * @param itemName + * @param owner + */ + public ShoppingListItem(String itemName, String owner) { + this.itemName = itemName; + this.owner = owner; + this.boughtBy = null; + this.bought = false; + + } + + public String getItemName() { return itemName; } + + public String getOwner() { + return owner; + } + + public String getBoughtBy() { + return boughtBy; + } + + public boolean isBought() { + return bought; + } + +} diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/User.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/User.java new file mode 100644 index 0000000..7805dde --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/model/User.java @@ -0,0 +1,51 @@ +package com.udacity.firebase.shoppinglistplusplus.model; + +import java.util.HashMap; + +/** + * Defines the data structure for User objects. + */ +public class User { + private String name; + private String email; + private HashMap timestampJoined; + private boolean hasLoggedInWithPassword; + + + /** + * Required public constructor + */ + public User() { + } + + /** + * Use this constructor to create new User. + * Takes user name, email and timestampJoined as params + * + * @param name + * @param email + * @param timestampJoined + */ + public User(String name, String email, HashMap timestampJoined) { + this.name = name; + this.email = email; + this.timestampJoined = timestampJoined; + this.hasLoggedInWithPassword = false; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public HashMap getTimestampJoined() { + return timestampJoined; + } + + public boolean isHasLoggedInWithPassword() { + return hasLoggedInWithPassword; + } +} diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/BaseActivity.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/BaseActivity.java index 972c72a..6a588fe 100755 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/BaseActivity.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/BaseActivity.java @@ -1,17 +1,27 @@ package com.udacity.firebase.shoppinglistplusplus.ui; +import android.content.Intent; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuItem; import android.widget.LinearLayout; +import com.firebase.client.AuthData; +import com.firebase.client.Firebase; import com.google.android.gms.auth.api.Auth; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.ui.login.CreateAccountActivity; +import com.udacity.firebase.shoppinglistplusplus.ui.login.LoginActivity; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; /** * BaseActivity class is used as a base class for all activities in the app @@ -20,8 +30,11 @@ */ public abstract class BaseActivity extends AppCompatActivity implements GoogleApiClient.OnConnectionFailedListener { - + protected String mProvider, mEncodedEmail; + /* Client used to interact with Google APIs. */ protected GoogleApiClient mGoogleApiClient; + protected Firebase.AuthStateListener mAuthListener; + protected Firebase mFirebaseRef; @Override protected void onCreate(Bundle savedInstanceState) { @@ -36,15 +49,50 @@ protected void onCreate(Bundle savedInstanceState) { * Build a GoogleApiClient with access to the Google Sign-In API and the * options specified by gso. */ + + /* Setup the Google API object to allow Google+ logins */ mGoogleApiClient = new GoogleApiClient.Builder(this) .enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */) .addApi(Auth.GOOGLE_SIGN_IN_API, gso) .build(); + + /** + * Getting mProvider and mEncodedEmail from SharedPreferences + */ + final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(BaseActivity.this); + /* Get mEncodedEmail and mProvider from SharedPreferences, use null as default value */ + mEncodedEmail = sp.getString(Constants.KEY_ENCODED_EMAIL, null); + mProvider = sp.getString(Constants.KEY_PROVIDER, null); + + + if (!((this instanceof LoginActivity) || (this instanceof CreateAccountActivity))) { + mFirebaseRef = new Firebase(Constants.FIREBASE_URL); + mAuthListener = new Firebase.AuthStateListener() { + @Override + public void onAuthStateChanged(AuthData authData) { + /* The user has been logged out */ + if (authData == null) { + /* Clear out shared preferences */ + SharedPreferences.Editor spe = sp.edit(); + spe.putString(Constants.KEY_ENCODED_EMAIL, null); + spe.putString(Constants.KEY_PROVIDER, null); + + takeUserToLoginScreenOnUnAuth(); + } + } + }; + mFirebaseRef.addAuthStateListener(mAuthListener); + } } @Override public void onDestroy() { super.onDestroy(); + /* Cleanup the AuthStateListener */ + if (!((this instanceof LoginActivity) || (this instanceof CreateAccountActivity))) { + mFirebaseRef.removeAuthStateListener(mAuthListener); + } + } @Override @@ -67,6 +115,12 @@ public boolean onOptionsItemSelected(MenuItem item) { super.onBackPressed(); return true; } + + if (id == R.id.action_logout) { + logout(); + return true; + } + return super.onOptionsItemSelected(item); } @@ -82,6 +136,38 @@ protected void initializeBackground(LinearLayout linearLayout) { } } + /** + * Logs out the user from their current session and starts LoginActivity. + * Also disconnects the mGoogleApiClient if connected and provider is Google + */ + protected void logout() { + + /* Logout if mProvider is not null */ + if (mProvider != null) { + mFirebaseRef.unauth(); + + if (mProvider.equals(Constants.GOOGLE_PROVIDER)) { + + /* Logout from Google+ */ + Auth.GoogleSignInApi.signOut(mGoogleApiClient).setResultCallback( + new ResultCallback() { + @Override + public void onResult(Status status) { + //nothing + } + }); + } + } + } + + private void takeUserToLoginScreenOnUnAuth() { + /* Move user to LoginActivity, and remove the backstack */ + Intent intent = new Intent(BaseActivity.this, LoginActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } + @Override public void onConnectionFailed(ConnectionResult connectionResult) { } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/MainActivity.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/MainActivity.java index 833ba86..7be903b 100755 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/MainActivity.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/MainActivity.java @@ -1,6 +1,7 @@ package com.udacity.firebase.shoppinglistplusplus.ui; import android.app.DialogFragment; +import android.content.Intent; import android.os.Bundle; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; @@ -8,32 +9,74 @@ import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.widget.Toolbar; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import com.firebase.client.DataSnapshot; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.ValueEventListener; import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.User; import com.udacity.firebase.shoppinglistplusplus.ui.activeLists.AddListDialogFragment; import com.udacity.firebase.shoppinglistplusplus.ui.activeLists.ShoppingListsFragment; import com.udacity.firebase.shoppinglistplusplus.ui.meals.AddMealDialogFragment; import com.udacity.firebase.shoppinglistplusplus.ui.meals.MealsFragment; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; /** * Represents the home screen of the app which * has a {@link ViewPager} with {@link ShoppingListsFragment} and {@link MealsFragment} */ public class MainActivity extends BaseActivity { + private Firebase mUserRef; private static final String LOG_TAG = MainActivity.class.getSimpleName(); + private ValueEventListener mUserRefListener; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + /** + * Create Firebase references + */ + mUserRef = new Firebase(Constants.FIREBASE_URL_USERS).child(mEncodedEmail); /** * Link layout elements from XML and setup the toolbar */ initializeScreen(); + + /** + * Add ValueEventListeners to Firebase references + * to control get data and control behavior and visibility of elements + */ + mUserRefListener = mUserRef.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + User user = snapshot.getValue(User.class); + + /** + * Set the activity title to current user name if user is not null + */ + if (user != null) { + /* Assumes that the first word in the user's name is the user's first name. */ + String firstName = user.getName().split("\\s+")[0]; + String title = firstName + "'s Lists"; + setTitle(title); + } + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(LOG_TAG, + getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + } @@ -57,6 +100,13 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); + /** + * Open SettingsActivity with sort options when Sort icon was clicked + */ + if (id == R.id.action_sort) { + startActivity(new Intent(MainActivity.this, SettingsActivity.class)); + return true; + } return super.onOptionsItemSelected(item); } @@ -64,6 +114,7 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onDestroy() { super.onDestroy(); + mUserRef.removeEventListener(mUserRefListener); } /** @@ -91,7 +142,7 @@ public void initializeScreen() { */ public void showAddListDialog(View view) { /* Create an instance of the dialog fragment and show it */ - DialogFragment dialog = AddListDialogFragment.newInstance(); + DialogFragment dialog = AddListDialogFragment.newInstance(mEncodedEmail); dialog.show(MainActivity.this.getFragmentManager(), "AddListDialogFragment"); } @@ -128,13 +179,13 @@ public Fragment getItem(int position) { */ switch (position) { case 0: - fragment = ShoppingListsFragment.newInstance(); + fragment = ShoppingListsFragment.newInstance(mEncodedEmail); break; case 1: fragment = MealsFragment.newInstance(); break; default: - fragment = ShoppingListsFragment.newInstance(); + fragment = ShoppingListsFragment.newInstance(mEncodedEmail); break; } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/SettingsActivity.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/SettingsActivity.java new file mode 100755 index 0000000..7ad1e06 --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/SettingsActivity.java @@ -0,0 +1,89 @@ +package com.udacity.firebase.shoppinglistplusplus.ui; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; + +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; + +/** + * SettingsActivity represents preference screen and functionality + */ +public class SettingsActivity extends PreferenceActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(R.style.PrefScreenTheme); + + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new SortPreferenceFragment()) + .commit(); + } + + /** + * This fragment shows the preferences for the first header. + */ + public static class SortPreferenceFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + /* Load the preferences from an XML resource */ + addPreferencesFromResource(R.xml.preference_screen); + + /** + * Bind preference summary to value for lists and meals sorting list preferences + */ + bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_name_sort_order_lists))); + } + + /** + * When preference is changed, save it's new value to default shared preferences + * + * @param preference + * @param newValue + */ + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + setPreferenceSummary(preference, newValue); + SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity()); + SharedPreferences.Editor spe = sharedPref.edit(); + spe.putString(Constants.KEY_PREF_SORT_ORDER_LISTS, newValue.toString()).apply(); + return true; + } + + private void bindPreferenceSummaryToValue(Preference preference) { + /* Set the listener to watch for value changes. */ + preference.setOnPreferenceChangeListener(this); + /* Trigger the listener immediately with the preference's current value. */ + setPreferenceSummary(preference, + PreferenceManager + .getDefaultSharedPreferences(preference.getContext()) + .getString(preference.getKey(), "")); + } + + /** + * Sets preference summary to appropriate value + * + * @param preference + * @param value + */ + private void setPreferenceSummary(Preference preference, Object value) { + String stringValue = value.toString(); + + if (preference instanceof ListPreference) { + ListPreference listPreference = (ListPreference) preference; + int prefIndex = listPreference.findIndexOfValue(stringValue); + + if (prefIndex >= 0) { + preference.setSummary(listPreference.getEntries()[prefIndex]); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/ActiveListDetailsActivity.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/ActiveListDetailsActivity.java index d08f1ca..2234433 100755 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/ActiveListDetailsActivity.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/ActiveListDetailsActivity.java @@ -1,43 +1,79 @@ package com.udacity.firebase.shoppinglistplusplus.ui.activeListDetails; +import android.app.Activity; import android.app.DialogFragment; +import android.content.Intent; import android.os.Bundle; +import android.support.v4.content.ContextCompat; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; +import android.widget.Button; import android.widget.ListView; +import android.widget.TextView; +import com.fasterxml.jackson.databind.ObjectMapper; import com.firebase.client.DataSnapshot; import com.firebase.client.Firebase; import com.firebase.client.FirebaseError; import com.firebase.client.ValueEventListener; import com.udacity.firebase.shoppinglistplusplus.R; import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.ShoppingListItem; +import com.udacity.firebase.shoppinglistplusplus.model.User; import com.udacity.firebase.shoppinglistplusplus.ui.BaseActivity; +import com.udacity.firebase.shoppinglistplusplus.ui.sharing.ShareListActivity; import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; /** * Represents the details screen for the selected shopping list */ public class ActiveListDetailsActivity extends BaseActivity { private static final String LOG_TAG = ActiveListDetailsActivity.class.getSimpleName(); - private Firebase mActiveListRef; + private Firebase mCurrentListRef, mCurrentUserRef, mSharedWithRef; + private ActiveListItemAdapter mActiveListItemAdapter; + private Button mButtonShopping; + private TextView mTextViewPeopleShopping; private ListView mListView; + private String mListId; + private User mCurrentUser; + /* Stores whether the current user is shopping */ + private boolean mShopping = false; + /* Stores whether the current user is the owner */ + private boolean mCurrentUserIsOwner = false; private ShoppingList mShoppingList; - + private ValueEventListener mCurrentUserRefListener, mCurrentListRefListener, mSharedWithListener; + private HashMap mSharedWithUsers; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_active_list_details); + /* Get the push ID from the extra passed by ShoppingListFragment */ + Intent intent = this.getIntent(); + mListId = intent.getStringExtra(Constants.KEY_LIST_ID); + if (mListId == null) { + /* No point in continuing without a valid ID. */ + finish(); + return; + } + /** * Create Firebase references */ - mActiveListRef = new Firebase(Constants.FIREBASE_URL_ACTIVE_LIST); + mCurrentListRef = new Firebase(Constants.FIREBASE_URL_USER_LISTS).child(mEncodedEmail).child(mListId); + mCurrentUserRef = new Firebase(Constants.FIREBASE_URL_USERS).child(mEncodedEmail); + mSharedWithRef = new Firebase (Constants.FIREBASE_URL_LISTS_SHARED_WITH).child(mListId); + Firebase listItemsRef = new Firebase(Constants.FIREBASE_URL_SHOPPING_LIST_ITEMS).child(mListId); /** @@ -45,11 +81,48 @@ protected void onCreate(final Bundle savedInstanceState) { */ initializeScreen(); + + /** + * Setup the adapter + */ + mActiveListItemAdapter = new ActiveListItemAdapter(this, ShoppingListItem.class, + R.layout.single_active_list_item, listItemsRef.orderByChild(Constants.FIREBASE_PROPERTY_BOUGHT_BY), + mListId, mEncodedEmail); + /* Create ActiveListItemAdapter and set to listView */ + mListView.setAdapter(mActiveListItemAdapter); + + + /** + * Add ValueEventListeners to Firebase references + * to control get data and control behavior and visibility of elements + */ + + /* Save the most up-to-date version of current user in mCurrentUser */ + mCurrentUserRefListener = mCurrentUserRef.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + User currentUser = dataSnapshot.getValue(User.class); + if (currentUser != null) mCurrentUser = currentUser; + else finish(); + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(LOG_TAG, + getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + + final Activity thisActivity = this; + + /** * Save the most recent version of current shopping list into mShoppingList instance * variable an update the UI to match the current list. */ - mActiveListRef.addValueEventListener(new ValueEventListener() { + mCurrentListRefListener = mCurrentListRef.addValueEventListener(new ValueEventListener() { + @Override public void onDataChange(DataSnapshot snapshot) { @@ -69,12 +142,37 @@ public void onDataChange(DataSnapshot snapshot) { return; } mShoppingList = shoppingList; + /** + * Pass the shopping list to the adapter if it is not null. + * We do this here because mShoppingList is null when first created. + */ + mActiveListItemAdapter.setShoppingList(mShoppingList); + + /* Check if the current user is owner */ + mCurrentUserIsOwner = Utils.checkIfOwner(shoppingList, mEncodedEmail); + /* Calling invalidateOptionsMenu causes onCreateOptionsMenu to be called */ invalidateOptionsMenu(); /* Set title appropriately. */ setTitle(shoppingList.getListName()); + + HashMap usersShopping = mShoppingList.getUsersShopping(); + if (usersShopping != null && usersShopping.size() != 0 && + usersShopping.containsKey(mEncodedEmail)) { + mShopping = true; + mButtonShopping.setText(getString(R.string.button_stop_shopping)); + mButtonShopping.setBackgroundColor(ContextCompat.getColor(ActiveListDetailsActivity.this, R.color.dark_grey)); + } else { + mButtonShopping.setText(getString(R.string.button_start_shopping)); + mButtonShopping.setBackgroundColor(ContextCompat.getColor(ActiveListDetailsActivity.this, R.color.primary_dark)); + mShopping = false; + + } + + setWhosShoppingText(mShoppingList.getUsersShopping()); + } @Override @@ -85,6 +183,23 @@ public void onCancelled(FirebaseError firebaseError) { } }); + mSharedWithListener = mSharedWithRef.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + mSharedWithUsers = new HashMap(); + for (DataSnapshot currentUser : dataSnapshot.getChildren()) { + mSharedWithUsers.put(currentUser.getKey(), currentUser.getValue(User.class)); + } + mActiveListItemAdapter.setSharedWithUsers(mSharedWithUsers); + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(LOG_TAG, + getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); /** * Set up click listeners for interaction. @@ -96,12 +211,72 @@ public void onCancelled(FirebaseError firebaseError) { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { /* Check that the view is not the empty footer item */ - if(view.getId() != R.id.list_view_footer_empty) { - showEditListItemNameDialog(); + if (view.getId() != R.id.list_view_footer_empty) { + ShoppingListItem shoppingListItem = mActiveListItemAdapter.getItem(position); + + if (shoppingListItem != null) { + /* + If the person is the owner and not shopping and the item is not bought, then + they can edit it. + */ + if (shoppingListItem.getOwner().equals(mEncodedEmail) && !mShopping && !shoppingListItem.isBought()) { + String itemName = shoppingListItem.getItemName(); + String itemId = mActiveListItemAdapter.getRef(position).getKey(); + showEditListItemNameDialog(itemName, itemId); + return true; + } + } + } + return false; + } + }); + + /* Perform buy/return action on listView item click event if current user is shopping. */ + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + /* Check that the view is not the empty footer item */ + if (view.getId() != R.id.list_view_footer_empty) { + final ShoppingListItem selectedListItem = mActiveListItemAdapter.getItem(position); + String itemId = mActiveListItemAdapter.getRef(position).getKey(); + + if (selectedListItem != null) { + /* If current user is shopping */ + if (mShopping) { + + /* Create map and fill it in with deep path multi write operations list */ + HashMap updatedItemBoughtData = new HashMap(); + + /* Buy selected item if it is NOT already bought */ + if (!selectedListItem.isBought()) { + updatedItemBoughtData.put(Constants.FIREBASE_PROPERTY_BOUGHT, true); + updatedItemBoughtData.put(Constants.FIREBASE_PROPERTY_BOUGHT_BY, mEncodedEmail); + } else { + /* Return selected item only if it was bought by current user */ + if (selectedListItem.getBoughtBy().equals(mEncodedEmail)) { + updatedItemBoughtData.put(Constants.FIREBASE_PROPERTY_BOUGHT, false); + updatedItemBoughtData.put(Constants.FIREBASE_PROPERTY_BOUGHT_BY, null); + } + } + + /* Do update */ + Firebase firebaseItemLocation = new Firebase(Constants.FIREBASE_URL_SHOPPING_LIST_ITEMS) + .child(mListId).child(itemId); + firebaseItemLocation.updateChildren(updatedItemBoughtData, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + if (firebaseError != null) { + Log.d(LOG_TAG, getString(R.string.log_error_updating_data) + + firebaseError.getMessage()); + } + } + }); + } + } } - return true; } }); + } @Override @@ -118,9 +293,9 @@ public boolean onCreateOptionsMenu(Menu menu) { MenuItem archive = menu.findItem(R.id.action_archive); /* Only the edit and remove options are implemented */ - remove.setVisible(true); - edit.setVisible(true); - share.setVisible(false); + remove.setVisible(mCurrentUserIsOwner); + edit.setVisible(mCurrentUserIsOwner); + share.setVisible(mCurrentUserIsOwner); archive.setVisible(false); return true; @@ -150,6 +325,10 @@ public boolean onOptionsItemSelected(MenuItem item) { * Eventually we'll add this */ if (id == R.id.action_share_list) { + Intent intent = new Intent(ActiveListDetailsActivity.this, ShareListActivity.class); + intent.putExtra(Constants.KEY_LIST_ID, mListId); + /* Starts an active showing the details for the selected list */ + startActivity(intent); return true; } @@ -171,6 +350,10 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onDestroy() { super.onDestroy(); + mActiveListItemAdapter.cleanup(); + mCurrentListRef.removeEventListener(mCurrentListRefListener); + mCurrentUserRef.removeEventListener(mCurrentUserRefListener); + mSharedWithRef.removeEventListener(mSharedWithListener); } /** @@ -178,6 +361,8 @@ public void onDestroy() { */ private void initializeScreen() { mListView = (ListView) findViewById(R.id.list_view_shopping_list_items); + mTextViewPeopleShopping = (TextView) findViewById(R.id.text_view_people_shopping); + mButtonShopping = (Button) findViewById(R.id.button_shopping); Toolbar toolbar = (Toolbar) findViewById(R.id.app_bar); /* Common toolbar setup */ setSupportActionBar(toolbar); @@ -190,6 +375,80 @@ private void initializeScreen() { mListView.addFooterView(footer); } + /** + * Set appropriate text for Start/Stop shopping button and Who's shopping textView + * depending on the current user shopping status + */ + private void setWhosShoppingText(HashMap usersShopping) { + + if (usersShopping != null) { + ArrayList usersWhoAreNotYou = new ArrayList<>(); + /** + * If at least one user is shopping + * Add userName to the list of users shopping if this user is not current user + */ + for (User user : usersShopping.values()) { + if (user != null && !(user.getEmail().equals(mEncodedEmail))) { + usersWhoAreNotYou.add(user.getName()); + } + } + + int numberOfUsersShopping = usersShopping.size(); + String usersShoppingText; + + /** + * If current user is shopping... + * If current user is the only person shopping, set text to "You are shopping" + * If current user and one user are shopping, set text "You and userName are shopping" + * Else set text "You and N others shopping" + */ + if (mShopping) { + switch (numberOfUsersShopping) { + case 1: + usersShoppingText = getString(R.string.text_you_are_shopping); + break; + case 2: + usersShoppingText = String.format( + getString(R.string.text_you_and_other_are_shopping), + usersWhoAreNotYou.get(0)); + break; + default: + usersShoppingText = String.format( + getString(R.string.text_you_and_number_are_shopping), + usersWhoAreNotYou.size()); + } + /** + * If current user is not shopping.. + * If there is only one person shopping, set text to "userName is shopping" + * If there are two users shopping, set text "userName1 and userName2 are shopping" + * Else set text "userName and N others shopping" + */ + } else { + switch (numberOfUsersShopping) { + case 1: + usersShoppingText = String.format( + getString(R.string.text_other_is_shopping), + usersWhoAreNotYou.get(0)); + break; + case 2: + usersShoppingText = String.format( + getString(R.string.text_other_and_other_are_shopping), + usersWhoAreNotYou.get(0), + usersWhoAreNotYou.get(1)); + break; + default: + usersShoppingText = String.format( + getString(R.string.text_other_and_number_are_shopping), + usersWhoAreNotYou.get(0), + usersWhoAreNotYou.size() - 1); + } + } + mTextViewPeopleShopping.setText(usersShoppingText); + } else { + mTextViewPeopleShopping.setText(""); + } + } + /** * Archive current list when user selects "Archive" menu item @@ -210,7 +469,8 @@ public void addMeal(View view) { */ public void removeList() { /* Create an instance of the dialog fragment and show it */ - DialogFragment dialog = RemoveListDialogFragment.newInstance(mShoppingList); + DialogFragment dialog = RemoveListDialogFragment.newInstance(mShoppingList, mListId, + mSharedWithUsers); dialog.show(getFragmentManager(), "RemoveListDialogFragment"); } @@ -219,7 +479,8 @@ public void removeList() { */ public void showAddListItemDialog(View view) { /* Create an instance of the dialog fragment and show it */ - DialogFragment dialog = AddListItemDialogFragment.newInstance(mShoppingList); + DialogFragment dialog = AddListItemDialogFragment.newInstance(mShoppingList, mListId, + mEncodedEmail, mSharedWithUsers); dialog.show(getFragmentManager(), "AddListItemDialogFragment"); } @@ -228,16 +489,22 @@ public void showAddListItemDialog(View view) { */ public void showEditListNameDialog() { /* Create an instance of the dialog fragment and show it */ - DialogFragment dialog = EditListNameDialogFragment.newInstance(mShoppingList); + DialogFragment dialog = EditListNameDialogFragment.newInstance(mShoppingList, mListId, + mEncodedEmail, mSharedWithUsers); dialog.show(this.getFragmentManager(), "EditListNameDialogFragment"); } /** * Show the edit list item name dialog after longClick on the particular item + * + * @param itemName + * @param itemId */ - public void showEditListItemNameDialog() { + public void showEditListItemNameDialog(String itemName, String itemId) { /* Create an instance of the dialog fragment and show it */ - DialogFragment dialog = EditListItemNameDialogFragment.newInstance(mShoppingList); + DialogFragment dialog = EditListItemNameDialogFragment.newInstance(mShoppingList, itemName, + itemId, mListId, mEncodedEmail, mSharedWithUsers); + dialog.show(this.getFragmentManager(), "EditListItemNameDialogFragment"); } @@ -245,6 +512,62 @@ public void showEditListItemNameDialog() { * This method is called when user taps "Start/Stop shopping" button */ public void toggleShopping(View view) { + /** + * Create map and fill it in with deep path multi write operations list + */ + HashMap updatedUserData = new HashMap(); + String propertyToUpdate = Constants.FIREBASE_PROPERTY_USERS_SHOPPING + "/" + mEncodedEmail; + + /** + * If current user is already shopping, remove current user from usersShopping map + */ + if (mShopping) { + + /* Add the value to update at the specified property for all lists */ + Utils.updateMapForAllWithValue(mSharedWithUsers, + mListId, mShoppingList.getOwner(), updatedUserData, + propertyToUpdate, null); + + /* Appends the timestamp changes for all lists */ + Utils.updateMapWithTimestampLastChanged(mSharedWithUsers, + mListId, mShoppingList.getOwner(), updatedUserData); + + /* Do a deep-path update */ + mFirebaseRef.updateChildren(updatedUserData, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + /* Updates the reversed timestamp */ + Utils.updateTimestampReversed(firebaseError, LOG_TAG, mListId, mSharedWithUsers, + mShoppingList.getOwner()); + } + }); + } else { + /** + * If current user is not shopping, create map to represent User model add to usersShopping map + */ + HashMap currentUser = (HashMap) + new ObjectMapper().convertValue(mCurrentUser, Map.class); + + /* Add the value to update at the specified property for all lists */ + Utils.updateMapForAllWithValue(mSharedWithUsers, + mListId, mShoppingList.getOwner(), updatedUserData, propertyToUpdate, currentUser); + + /* Appends the timestamp changes for all lists */ + Utils.updateMapWithTimestampLastChanged(mSharedWithUsers, + mListId, mShoppingList.getOwner(), updatedUserData); + + /* Do a deep-path update */ + mFirebaseRef.updateChildren(updatedUserData, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + /* Updates the reversed timestamp */ + Utils.updateTimestampReversed(firebaseError, LOG_TAG, mListId, mSharedWithUsers, + mShoppingList.getOwner()); + } + }); + + } } + } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/ActiveListItemAdapter.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/ActiveListItemAdapter.java new file mode 100644 index 0000000..b544c07 --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/ActiveListItemAdapter.java @@ -0,0 +1,207 @@ +package com.udacity.firebase.shoppinglistplusplus.ui.activeListDetails; + +import android.app.Activity; +import android.content.DialogInterface; +import android.graphics.Paint; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.View; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.firebase.client.DataSnapshot; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.Query; +import com.firebase.client.ValueEventListener; +import com.firebase.ui.FirebaseListAdapter; +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.ShoppingListItem; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.util.HashMap; + + +/** + * Populates list_view_shopping_list_items inside ActiveListDetailsActivity + */ +public class ActiveListItemAdapter extends FirebaseListAdapter { + private ShoppingList mShoppingList; + private String mListId; + private String mEncodedEmail; + private HashMap mSharedWithUsers; + + /** + * Public constructor that initializes private instance variables when adapter is created + */ + public ActiveListItemAdapter(Activity activity, Class modelClass, int modelLayout, + Query ref, String listId, String encodedEmail) { + super(activity, modelClass, modelLayout, ref); + this.mActivity = activity; + this.mListId = listId; + this.mEncodedEmail = encodedEmail; + } + + /** + * Public method that is used to pass shoppingList object when it is loaded in ValueEventListener + */ + public void setShoppingList(ShoppingList shoppingList) { + this.mShoppingList = shoppingList; + this.notifyDataSetChanged(); + } + + public void setSharedWithUsers(HashMap sharedWithUsers) { + this.mSharedWithUsers = sharedWithUsers; + this.notifyDataSetChanged(); + } + + /** + * Protected method that populates the view attached to the adapter (list_view_friends_autocomplete) + * with items inflated from single_active_list_item.xml + * populateView also handles data changes and updates the listView accordingly + */ + @Override + protected void populateView(View view, final ShoppingListItem item, int position) { + + ImageButton buttonRemoveItem = (ImageButton) view.findViewById(R.id.button_remove_item); + TextView textViewItemName = (TextView) view.findViewById(R.id.text_view_active_list_item_name); + final TextView textViewBoughtByUser = (TextView) view.findViewById(R.id.text_view_bought_by_user); + TextView textViewBoughtBy = (TextView) view.findViewById(R.id.text_view_bought_by); + + String owner = item.getOwner(); + + textViewItemName.setText(item.getItemName()); + + + setItemAppearanceBaseOnBoughtStatus(owner, textViewBoughtByUser, textViewBoughtBy, buttonRemoveItem, + textViewItemName, item); + + + /* Gets the id of the item to remove */ + final String itemToRemoveId = this.getRef(position).getKey(); + + /** + * Set the on click listener for "Remove list item" button + */ + buttonRemoveItem.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(mActivity, R.style.CustomTheme_Dialog) + .setTitle(mActivity.getString(R.string.remove_item_option)) + .setMessage(mActivity.getString(R.string.dialog_message_are_you_sure_remove_item)) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + + removeItem(itemToRemoveId); + /* Dismiss the dialog */ + dialog.dismiss(); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + /* Dismiss the dialog */ + dialog.dismiss(); + } + }) + .setIcon(android.R.drawable.ic_dialog_alert); + + AlertDialog alertDialog = alertDialogBuilder.create(); + alertDialog.show(); + } + }); + } + + private void removeItem(String itemId) { + Firebase firebaseRef = new Firebase(Constants.FIREBASE_URL); + + /* Make a map for the removal */ + HashMap updatedRemoveItemMap = new HashMap(); + + /* Remove the item by passing null */ + updatedRemoveItemMap.put("/" + Constants.FIREBASE_LOCATION_SHOPPING_LIST_ITEMS + "/" + + mListId + "/" + itemId, null); + + /* Add the updated timestamp */ + Utils.updateMapWithTimestampLastChanged(mSharedWithUsers, + mListId, mShoppingList.getOwner(), updatedRemoveItemMap); + + /* Do the update */ + firebaseRef.updateChildren(updatedRemoveItemMap, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + Utils.updateTimestampReversed(firebaseError, "ActListItemAdap", mListId, + mSharedWithUsers, mShoppingList.getOwner()); + } + }); + } + + private void setItemAppearanceBaseOnBoughtStatus(String owner, final TextView textViewBoughtByUser, + TextView textViewBoughtBy, ImageButton buttonRemoveItem, + TextView textViewItemName, ShoppingListItem item) { + /** + * If selected item is bought + * Set "Bought by" text to "You" if current user is owner of the list + * Set "Bought by" text to userName if current user is NOT owner of the list + * Set the remove item button invisible if current user is NOT list or item owner + */ + if (item.isBought() && item.getBoughtBy() != null) { + + textViewBoughtBy.setVisibility(View.VISIBLE); + textViewBoughtByUser.setVisibility(View.VISIBLE); + buttonRemoveItem.setVisibility(View.INVISIBLE); + + /* Add a strike-through */ + textViewItemName.setPaintFlags(textViewItemName.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + + if (item.getBoughtBy().equals(mEncodedEmail)) { + textViewBoughtByUser.setText(mActivity.getString(R.string.text_you)); + } else { + + Firebase boughtByUserRef = new Firebase(Constants.FIREBASE_URL_USERS).child(item.getBoughtBy()); + /* Get the item's owner's name; use a SingleValueEvent listener for memory efficiency */ + boughtByUserRef.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + User user = dataSnapshot.getValue(User.class); + if (user != null) { + textViewBoughtByUser.setText(user.getName()); + } + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(mActivity.getClass().getSimpleName(), + mActivity.getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + } + } else { + /** + * If selected item is NOT bought + * Set "Bought by" text to be empty and invisible + * Set the remove item button visible if current user is owner of the list or selected item + */ + + /* Remove the strike-through */ + textViewItemName.setPaintFlags(textViewItemName.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); + + textViewBoughtBy.setVisibility(View.INVISIBLE); + textViewBoughtByUser.setVisibility(View.INVISIBLE); + textViewBoughtByUser.setText(""); + /** + * If you are the owner of the item or the owner of the list, then the remove icon + * is visible. + */ + if (owner.equals(mEncodedEmail) || (mShoppingList != null && mShoppingList.getOwner().equals(mEncodedEmail))) { + buttonRemoveItem.setVisibility(View.VISIBLE); + } else { + buttonRemoveItem.setVisibility(View.INVISIBLE); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/AddListItemDialogFragment.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/AddListItemDialogFragment.java index 8f0bc9a..601aa36 100644 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/AddListItemDialogFragment.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/AddListItemDialogFragment.java @@ -3,8 +3,18 @@ import android.app.Dialog; import android.os.Bundle; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; import com.udacity.firebase.shoppinglistplusplus.R; import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.ShoppingListItem; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.util.HashMap; +import java.util.Map; /** * Lets user add new list item. @@ -14,10 +24,12 @@ public class AddListItemDialogFragment extends EditListDialogFragment { /** * Public static constructor that creates fragment and passes a bundle with data into it when adapter is created */ - public static AddListItemDialogFragment newInstance(ShoppingList shoppingList) { + public static AddListItemDialogFragment newInstance(ShoppingList shoppingList, String listId, + String encodedEmail, + HashMap sharedWithUsers) { AddListItemDialogFragment addListItemDialogFragment = new AddListItemDialogFragment(); - - Bundle bundle = newInstanceHelper(shoppingList, R.layout.dialog_add_item); + Bundle bundle = EditListDialogFragment.newInstanceHelper(shoppingList, + R.layout.dialog_add_item, listId, encodedEmail, sharedWithUsers); addListItemDialogFragment.setArguments(bundle); return addListItemDialogFragment; @@ -44,6 +56,50 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { */ @Override protected void doListEdit() { + String mItemName = mEditTextForList.getText().toString(); + /** + * Adds list item if the input name is not empty + */ + if (!mItemName.equals("")) { + + Firebase firebaseRef = new Firebase(Constants.FIREBASE_URL); + Firebase itemsRef = new Firebase(Constants.FIREBASE_URL_SHOPPING_LIST_ITEMS).child(mListId); + + /* Make a map for the item you are adding */ + HashMap updatedItemToAddMap = new HashMap(); + + /* Save push() to maintain same random Id */ + Firebase newRef = itemsRef.push(); + String itemId = newRef.getKey(); + + /* Make a POJO for the item and immediately turn it into a HashMap */ + ShoppingListItem itemToAddObject = new ShoppingListItem(mItemName, mEncodedEmail); + HashMap itemToAdd = + (HashMap) new ObjectMapper().convertValue(itemToAddObject, Map.class); + + + /* Add the item to the update map*/ + updatedItemToAddMap.put("/" + Constants.FIREBASE_LOCATION_SHOPPING_LIST_ITEMS + "/" + + mListId + "/" + itemId, itemToAdd); + + /* Update affected lists timestamps */ + Utils.updateMapWithTimestampLastChanged(mSharedWith, + mListId, mOwner, updatedItemToAddMap); + + /* Do the update */ + firebaseRef.updateChildren(updatedItemToAddMap, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + /* Now that we have the timestamp, update the reversed timestamp */ + Utils.updateTimestampReversed(firebaseError, "AddListItem", mListId, + mSharedWith, mOwner); + } + }); + /** + * Close the dialog fragment when done + */ + AddListItemDialogFragment.this.getDialog().cancel(); + } } } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListDialogFragment.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListDialogFragment.java index c68b105..c10fe3c 100644 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListDialogFragment.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListDialogFragment.java @@ -15,26 +15,40 @@ import com.udacity.firebase.shoppinglistplusplus.R; import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.User; import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import java.util.HashMap; + /** * Base class for {@link DialogFragment}s involved with editing a shopping list. */ public abstract class EditListDialogFragment extends DialogFragment { + String mListId, mOwner, mEncodedEmail; EditText mEditTextForList; int mResource; + HashMap mSharedWith; /** * Helper method that creates a basic bundle of all of the information needed to change * values in a shopping list. * - * @param shoppingList - * @param resource - * @return + * @param shoppingList The shopping list that the dialog is editing + * @param resource The xml layout file associated with the dialog + * @param listId The id of the shopping list the dialog is editing + * @param encodedEmail The encoded email of the current user + * @param sharedWithUsers The HashMap containing all users that the current shopping list + * is shared with + * @return The bundle containing all the arguments. */ - protected static Bundle newInstanceHelper(ShoppingList shoppingList, int resource) { + protected static Bundle newInstanceHelper(ShoppingList shoppingList, int resource, String listId, + String encodedEmail, HashMap sharedWithUsers) { Bundle bundle = new Bundle(); + bundle.putSerializable(Constants.KEY_SHARED_WITH_USERS, sharedWithUsers); + bundle.putString(Constants.KEY_LIST_ID, listId); bundle.putInt(Constants.KEY_LAYOUT_RESOURCE, resource); + bundle.putString(Constants.KEY_LIST_OWNER, shoppingList.getOwner()); + bundle.putString(Constants.KEY_ENCODED_EMAIL, encodedEmail); return bundle; } @@ -44,7 +58,11 @@ protected static Bundle newInstanceHelper(ShoppingList shoppingList, int resourc @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mSharedWith = (HashMap) getArguments().getSerializable(Constants.KEY_SHARED_WITH_USERS); + mListId = getArguments().getString(Constants.KEY_LIST_ID); mResource = getArguments().getInt(Constants.KEY_LAYOUT_RESOURCE); + mOwner = getArguments().getString(Constants.KEY_LIST_OWNER); + mEncodedEmail = getArguments().getString(Constants.KEY_ENCODED_EMAIL); } /** diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListItemNameDialogFragment.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListItemNameDialogFragment.java index 150a7db..8008874 100644 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListItemNameDialogFragment.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListItemNameDialogFragment.java @@ -3,21 +3,34 @@ import android.app.Dialog; import android.os.Bundle; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; import com.udacity.firebase.shoppinglistplusplus.R; import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.util.HashMap; /** * Lets user edit list item name for all copies of the current list */ public class EditListItemNameDialogFragment extends EditListDialogFragment { + String mItemName, mItemId; /** * Public static constructor that creates fragment and passes a bundle with data into it when adapter is created */ - public static EditListItemNameDialogFragment newInstance(ShoppingList shoppingList) { + public static EditListItemNameDialogFragment newInstance(ShoppingList shoppingList, String itemName, + String itemId, String listId, String encodedEmail, + HashMap sharedWithUsers) { EditListItemNameDialogFragment editListItemNameDialogFragment = new EditListItemNameDialogFragment(); - Bundle bundle = EditListDialogFragment.newInstanceHelper(shoppingList, R.layout.dialog_edit_item); + Bundle bundle = EditListDialogFragment.newInstanceHelper(shoppingList, R.layout.dialog_edit_item, + listId, encodedEmail, sharedWithUsers); + bundle.putString(Constants.KEY_LIST_ITEM_NAME, itemName); + bundle.putString(Constants.KEY_LIST_ITEM_ID, itemId); editListItemNameDialogFragment.setArguments(bundle); return editListItemNameDialogFragment; @@ -29,6 +42,8 @@ public static EditListItemNameDialogFragment newInstance(ShoppingList shoppingLi @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mItemName = getArguments().getString(Constants.KEY_LIST_ITEM_NAME); + mItemId = getArguments().getString(Constants.KEY_LIST_ITEM_ID); } @@ -39,6 +54,12 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { * superclass method that creates the dialog */ Dialog dialog = super.createDialogHelper(R.string.positive_button_edit_item); + /** + * {@link EditListDialogFragment#helpSetDefaultValueEditText(String)} is a superclass + * method that sets the default text of the TextView + */ + super.helpSetDefaultValueEditText(mItemName); + return dialog; } @@ -46,5 +67,37 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { * Change selected list item name to the editText input if it is not empty */ protected void doListEdit() { + String nameInput = mEditTextForList.getText().toString(); + + /** + * Set input text to be the current list item name if it is not empty and is not the + * previous name. + */ + if (!nameInput.equals("") && !nameInput.equals(mItemName)) { + Firebase firebaseRef = new Firebase(Constants.FIREBASE_URL); + + /* Make a map for the item you are changing the name of */ + HashMap updatedDataItemToEditMap = new HashMap(); + + /* Add the new name to the update map*/ + updatedDataItemToEditMap.put("/" + Constants.FIREBASE_LOCATION_SHOPPING_LIST_ITEMS + "/" + + mListId + "/" + mItemId + "/" + Constants.FIREBASE_PROPERTY_ITEM_NAME, + nameInput); + + /* Update affected lists timestamps */ + Utils.updateMapWithTimestampLastChanged(mSharedWith, mListId, mOwner, updatedDataItemToEditMap); + + /* Do the update */ + firebaseRef.updateChildren(updatedDataItemToEditMap, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + /* Now that we have the timestamp, update the reversed timestamp */ + Utils.updateTimestampReversed(firebaseError, "EditListItem", mListId, + mSharedWith, mOwner); + } + }); + + + } } } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListNameDialogFragment.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListNameDialogFragment.java index 7326e46..3de26d2 100644 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListNameDialogFragment.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/EditListNameDialogFragment.java @@ -3,23 +3,33 @@ import android.app.Dialog; import android.os.Bundle; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; import com.udacity.firebase.shoppinglistplusplus.R; import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.util.HashMap; /** * Lets user edit the list name for all copies of the current list */ public class EditListNameDialogFragment extends EditListDialogFragment { private static final String LOG_TAG = ActiveListDetailsActivity.class.getSimpleName(); + String mListName; /** * Public static constructor that creates fragment and passes a bundle with data into it when adapter is created */ - public static EditListNameDialogFragment newInstance(ShoppingList shoppingList) { + public static EditListNameDialogFragment newInstance(ShoppingList shoppingList, String listId, + String encodedEmail, + HashMap sharedWithUsers) { EditListNameDialogFragment editListNameDialogFragment = new EditListNameDialogFragment(); - Bundle bundle = EditListDialogFragment.newInstanceHelper(shoppingList, R.layout.dialog_edit_list); - // TODO add any values you need here from the shopping list to make this change. - // Once you put a value in the bundle, it available to you in onCreate + Bundle bundle = EditListDialogFragment.newInstanceHelper(shoppingList, + R.layout.dialog_edit_list, listId, encodedEmail, sharedWithUsers); + bundle.putString(Constants.KEY_LIST_NAME, shoppingList.getListName()); editListNameDialogFragment.setArguments(bundle); return editListNameDialogFragment; } @@ -30,9 +40,7 @@ public static EditListNameDialogFragment newInstance(ShoppingList shoppingList) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // TODO Extract any arguments you put in the bundle when the newInstance method - // created the dialog. You can store these in an instance variable so that they - // are available to you. + mListName = getArguments().getString(Constants.KEY_LIST_NAME); } @@ -43,10 +51,11 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { * superclass method that creates the dialog **/ Dialog dialog = super.createDialogHelper(R.string.positive_button_edit_item); - // TODO You can use the helper method in the superclass I made (EditListDialogFragment) - // called helpSetDefaultValueEditText. This will allow you to set what text the - // user sees when the dialog opens. - + /** + * {@link EditListDialogFragment#helpSetDefaultValueEditText(String)} is a superclass + * method that sets the default text of the TextView + */ + helpSetDefaultValueEditText(mListName); return dialog; } @@ -54,10 +63,39 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { * Changes the list name in all copies of the current list */ protected void doListEdit() { - // TODO Do the actual edit operation here. - // Remember, you need to update the timestampLastChanged for - // the shopping list. + final String inputListName = mEditTextForList.getText().toString(); + /** + * Check that the user inputted list name is not empty, has changed the original name + * and that the dialog was properly initialized with the current name and id of the list. + */ + if (!inputListName.equals("") && mListName != null && + mListId != null && !inputListName.equals(mListName)) { + + Firebase firebaseRef = new Firebase(Constants.FIREBASE_URL); + + /** + * Create map and fill it in with deep path multi write operations list + */ + HashMap updatedListData = new HashMap(); + + /* Add the value to update at the specified property for all lists */ + Utils.updateMapForAllWithValue(mSharedWith, mListId, mOwner, updatedListData, + Constants.FIREBASE_PROPERTY_LIST_NAME, inputListName); + + /* Update affected lists timestamps */ + Utils.updateMapWithTimestampLastChanged(mSharedWith, mListId, mOwner, updatedListData); + + /* Do a deep-path update */ + firebaseRef.updateChildren(updatedListData, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + /* Now that we have the timestamp, update the reversed timestamp */ + Utils.updateTimestampReversed(firebaseError, LOG_TAG, mListId, + mSharedWith, mOwner); + } + }); + } } } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/RemoveListDialogFragment.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/RemoveListDialogFragment.java index 2d42e29..48af5c4 100644 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/RemoveListDialogFragment.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeListDetails/RemoveListDialogFragment.java @@ -5,22 +5,38 @@ import android.content.DialogInterface; import android.os.Bundle; import android.support.v7.app.AlertDialog; +import android.util.Log; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; import com.udacity.firebase.shoppinglistplusplus.R; import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.util.HashMap; /** * Lets the user remove active shopping list */ public class RemoveListDialogFragment extends DialogFragment { + String mListId; + String mListOwner; + HashMap mSharedWith; + final static String LOG_TAG = RemoveListDialogFragment.class.getSimpleName(); /** * Public static constructor that creates fragment and passes a bundle with data into it when adapter is created */ - public static RemoveListDialogFragment newInstance(ShoppingList shoppingList) { + public static RemoveListDialogFragment newInstance(ShoppingList shoppingList, String listId, + HashMap sharedWithUsers) { RemoveListDialogFragment removeListDialogFragment = new RemoveListDialogFragment(); Bundle bundle = new Bundle(); + bundle.putString(Constants.KEY_LIST_ID, listId); + bundle.putString(Constants.KEY_LIST_OWNER, shoppingList.getOwner()); + bundle.putSerializable(Constants.KEY_SHARED_WITH_USERS, sharedWithUsers); removeListDialogFragment.setArguments(bundle); return removeListDialogFragment; } @@ -31,6 +47,9 @@ public static RemoveListDialogFragment newInstance(ShoppingList shoppingList) { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mListId = getArguments().getString(Constants.KEY_LIST_ID); + mListOwner = getArguments().getString(Constants.KEY_LIST_OWNER); + mSharedWith = (HashMap) getArguments().getSerializable(Constants.KEY_SHARED_WITH_USERS); } @Override @@ -57,7 +76,32 @@ public void onClick(DialogInterface dialog, int which) { } private void removeList() { + Firebase firebaseRef = new Firebase(Constants.FIREBASE_URL); + + /** + * Create map and fill it in with deep path multi write operations list + */ + HashMap removeListData = new HashMap(); + + /* Remove the ShoppingLists from both user lists and active lists */ + Utils.updateMapForAllWithValue(mSharedWith, mListId, mListOwner, removeListData, "", null); + + /* Remove the associated list items */ + removeListData.put("/" + Constants.FIREBASE_LOCATION_SHOPPING_LIST_ITEMS + "/" + mListId, + null); + + removeListData.put("/" + Constants.FIREBASE_LOCATION_OWNER_MAPPINGS + "/" + mListId, + null); + /* Do a deep-path update */ + firebaseRef.updateChildren(removeListData, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + if (firebaseError != null) { + Log.e(LOG_TAG, getString(R.string.log_error_updating_data) + firebaseError.getMessage()); + } + } + }); } } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/ActiveListAdapter.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/ActiveListAdapter.java new file mode 100644 index 0000000..64da03c --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/ActiveListAdapter.java @@ -0,0 +1,107 @@ +package com.udacity.firebase.shoppinglistplusplus.ui.activeLists; + +import android.app.Activity; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import com.firebase.client.DataSnapshot; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.Query; +import com.firebase.client.ValueEventListener; +import com.firebase.ui.FirebaseListAdapter; +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; + +/** + * Populates the list_view_active_lists inside ShoppingListsFragment + */ +public class ActiveListAdapter extends FirebaseListAdapter { + private String mEncodedEmail; + + /** + * Public constructor that initializes private instance variables when adapter is created + */ + public ActiveListAdapter(Activity activity, Class modelClass, int modelLayout, + Query ref, String encodedEmail) { + super(activity, modelClass, modelLayout, ref); + this.mEncodedEmail = encodedEmail; + this.mActivity = activity; + } + + /** + * Protected method that populates the view attached to the adapter (list_view_active_lists) + * with items inflated from single_active_list.xml + * populateView also handles data changes and updates the listView accordingly + */ + @Override + protected void populateView(View view, ShoppingList list) { + + /** + * Grab the needed Textivews and strings + */ + TextView textViewListName = (TextView) view.findViewById(R.id.text_view_list_name); + final TextView textViewCreatedByUser = (TextView) view.findViewById(R.id.text_view_created_by_user); + final TextView textViewUsersShopping = (TextView) view.findViewById(R.id.text_view_people_shopping_count); + + String ownerEmail = list.getOwner(); + + /* Set the list name and owner */ + textViewListName.setText(list.getListName()); + + /** + * Show "1 person is shopping" if one person is shopping + * Show "N people shopping" if two or more users are shopping + * Show nothing if nobody is shopping + */ + if (list.getUsersShopping() != null) { + int usersShopping = list.getUsersShopping().size(); + if (usersShopping == 1) { + textViewUsersShopping.setText(String.format( + mActivity.getResources().getString(R.string.person_shopping), + usersShopping)); + } else { + textViewUsersShopping.setText(String.format( + mActivity.getResources().getString(R.string.people_shopping), + usersShopping)); + } + } else { + /* otherwise show nothing */ + textViewUsersShopping.setText(""); + } + + /** + * Set "Created by" text to "You" if current user is owner of the list + * Set "Created by" text to userName if current user is NOT owner of the list + */ + if (ownerEmail != null) { + if (ownerEmail.equals(mEncodedEmail)) { + textViewCreatedByUser.setText(mActivity.getResources().getString(R.string.text_you)); + } else { + Firebase userRef = new Firebase(Constants.FIREBASE_URL_USERS).child(ownerEmail); + /* Get the user's name */ + userRef.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + User user = dataSnapshot.getValue(User.class); + + if (user != null) { + textViewCreatedByUser.setText(user.getName()); + } + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(mActivity.getClass().getSimpleName(), + mActivity.getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + } + } + + } +} diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/AddListDialogFragment.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/AddListDialogFragment.java index eb5a2b3..756eab3 100644 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/AddListDialogFragment.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/AddListDialogFragment.java @@ -13,24 +13,33 @@ import android.widget.EditText; import android.widget.TextView; +import com.fasterxml.jackson.databind.ObjectMapper; import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.ServerValue; import com.udacity.firebase.shoppinglistplusplus.R; import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.util.HashMap; +import java.util.Map; /** * Adds a new shopping list */ public class AddListDialogFragment extends DialogFragment { + String mEncodedEmail; EditText mEditTextListName; /** * Public static constructor that creates fragment and * passes a bundle with data into it when adapter is created */ - public static AddListDialogFragment newInstance() { + public static AddListDialogFragment newInstance(String encodedEmail) { AddListDialogFragment addListDialogFragment = new AddListDialogFragment(); Bundle bundle = new Bundle(); + bundle.putString(Constants.KEY_ENCODED_EMAIL, encodedEmail); addListDialogFragment.setArguments(bundle); return addListDialogFragment; } @@ -41,6 +50,7 @@ public static AddListDialogFragment newInstance() { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mEncodedEmail = getArguments().getString(Constants.KEY_ENCODED_EMAIL); } /** @@ -92,18 +102,61 @@ public void onClick(DialogInterface dialog, int id) { * Add new active list */ public void addShoppingList() { - // Get the reference to the root node in Firebase - Firebase ref = new Firebase(Constants.FIREBASE_URL); - // Get the string that the user entered into the EditText and make an object with it - // We'll use "Anonymous Owner" for the owner because we don't have user accounts yet String userEnteredName = mEditTextListName.getText().toString(); - String owner = "Anonymous Owner"; - ShoppingList currentList = new ShoppingList(userEnteredName, owner); - // Go to the "activeList" child node of the root node. - // This will create the node for you if it doesn't already exist. - // Then using the setValue menu it will serialize the ShoppingList POJO - ref.child(Constants.FIREBASE_LOCATION_ACTIVE_LIST).setValue(currentList); + /** + * If EditText input is not empty + */ + if (!userEnteredName.equals("")) { + + /** + * Create Firebase references + */ + Firebase userListsRef = new Firebase(Constants.FIREBASE_URL_USER_LISTS). + child(mEncodedEmail); + final Firebase firebaseRef = new Firebase(Constants.FIREBASE_URL); + + Firebase newListRef = userListsRef.push(); + + /* Save listsRef.push() to maintain same random Id */ + final String listId = newListRef.getKey(); + + /* HashMap for data to update */ + HashMap updateShoppingListData = new HashMap<>(); + + /** + * Set raw version of date to the ServerValue.TIMESTAMP value and save into + * timestampCreatedMap + */ + HashMap timestampCreated = new HashMap<>(); + timestampCreated.put(Constants.FIREBASE_PROPERTY_TIMESTAMP, ServerValue.TIMESTAMP); + + /* Build the shopping list */ + ShoppingList newShoppingList = new ShoppingList(userEnteredName, mEncodedEmail, + timestampCreated); + + HashMap shoppingListMap = (HashMap) + new ObjectMapper().convertValue(newShoppingList, Map.class); + + Utils.updateMapForAllWithValue(null, listId, mEncodedEmail, + updateShoppingListData, "", shoppingListMap); + + updateShoppingListData.put("/" + Constants.FIREBASE_LOCATION_OWNER_MAPPINGS + "/" + listId, + mEncodedEmail); + + /* Do the update */ + firebaseRef.updateChildren(updateShoppingListData, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + /* Now that we have the timestamp, update the reversed timestamp */ + Utils.updateTimestampReversed(firebaseError, "AddList", listId, + null, mEncodedEmail); + } + }); + + /* Close the dialog fragment */ + AddListDialogFragment.this.getDialog().cancel(); + } } } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/ShoppingListsFragment.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/ShoppingListsFragment.java index 346316c..4e0678b 100755 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/ShoppingListsFragment.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/activeLists/ShoppingListsFragment.java @@ -2,26 +2,22 @@ import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; -import android.widget.TextView; -import com.firebase.client.DataSnapshot; import com.firebase.client.Firebase; -import com.firebase.client.FirebaseError; -import com.firebase.client.ValueEventListener; +import com.firebase.client.Query; import com.udacity.firebase.shoppinglistplusplus.R; import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; import com.udacity.firebase.shoppinglistplusplus.ui.activeListDetails.ActiveListDetailsActivity; import com.udacity.firebase.shoppinglistplusplus.utils.Constants; -import com.udacity.firebase.shoppinglistplusplus.utils.Utils; - -import java.util.Date; /** @@ -30,9 +26,9 @@ * create an instance of this fragment. */ public class ShoppingListsFragment extends Fragment { + private String mEncodedEmail; + private ActiveListAdapter mActiveListAdapter; private ListView mListView; - private TextView mTextViewListName, mTextViewListOwner; - private TextView mTextViewEditTime; public ShoppingListsFragment() { /* Required empty public constructor */ @@ -42,18 +38,13 @@ public ShoppingListsFragment() { * Create fragment and pass bundle with data as it's arguments * Right now there are not arguments...but eventually there will be. */ - public static ShoppingListsFragment newInstance() { + public static ShoppingListsFragment newInstance(String encodedEmail) { ShoppingListsFragment fragment = new ShoppingListsFragment(); Bundle args = new Bundle(); + args.putString(Constants.KEY_ENCODED_EMAIL, encodedEmail); fragment.setArguments(args); return fragment; } - - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - } /** * Initialize instance variables with data from bundle @@ -62,12 +53,14 @@ public void onActivityCreated(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { + mEncodedEmail = getArguments().getString(Constants.KEY_ENCODED_EMAIL); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + /** * Initialize UI elements */ @@ -75,80 +68,82 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, initializeScreen(rootView); /** - * Create Firebase references - */ - Firebase refListName = new Firebase(Constants.FIREBASE_URL).child(Constants.FIREBASE_LOCATION_ACTIVE_LIST); - - /** - * Add ValueEventListeners to Firebase references - * to control get data and control behavior and visibility of elements + * Set interactive bits, such as click events and adapters */ - refListName.addValueEventListener(new ValueEventListener() { + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override - public void onDataChange(DataSnapshot dataSnapshot) { - // You can use getValue to deserialize the data at dataSnapshot - // into a ShoppingList. - ShoppingList shoppingList = dataSnapshot.getValue(ShoppingList.class); - - // If there was no data at the location we added the listener, then - // shoppingList will be null. - if (shoppingList != null) { - // If there was data, take the TextViews and set the appropriate values. - mTextViewListName.setText(shoppingList.getListName()); - mTextViewListOwner.setText(shoppingList.getOwner()); - if (shoppingList.getTimestampLastChanged() != null) { - mTextViewEditTime.setText( - Utils.SIMPLE_DATE_FORMAT.format( - new Date(shoppingList.getTimestampLastChangedLong()))); - } else { - mTextViewEditTime.setText(""); - } - + public void onItemClick(AdapterView parent, View view, int position, long id) { + ShoppingList selectedList = mActiveListAdapter.getItem(position); + if (selectedList != null) { + Intent intent = new Intent(getActivity(), ActiveListDetailsActivity.class); + /* Get the list ID using the adapter's get ref method to get the Firebase + * ref and then grab the key. + */ + String listId = mActiveListAdapter.getRef(position).getKey(); + intent.putExtra(Constants.KEY_LIST_ID, listId); + /* Starts an active showing the details for the selected list */ + startActivity(intent); } } - - @Override - public void onCancelled(FirebaseError firebaseError) { - - } }); + + return rootView; + } + /** + * Updates the order of mListView onResume to handle sortOrderChanges properly + */ + @Override + public void onResume() { + super.onResume(); + final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity()); + String sortOrder = sharedPref.getString(Constants.KEY_PREF_SORT_ORDER_LISTS, Constants.ORDER_BY_KEY); + + Query orderedActiveUserListsRef; + Firebase activeListsRef = new Firebase(Constants.FIREBASE_URL_USER_LISTS) + .child(mEncodedEmail); /** - * Set interactive bits, such as click events and adapters + * Sort active lists by "date created" + * if it's been selected in the SettingsActivity */ - mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { + if (sortOrder.equals(Constants.ORDER_BY_KEY)) { + orderedActiveUserListsRef = activeListsRef.orderByKey(); + } else { - } - }); + /** + * Sort active by lists by name or datelastChanged. Otherwise + * depending on what's been selected in SettingsActivity + */ - mTextViewListName.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - /* Starts an active showing the details for the selected list */ - Intent intent = new Intent(getActivity(), ActiveListDetailsActivity.class); - startActivity(intent); - } - }); + orderedActiveUserListsRef = activeListsRef.orderByChild(sortOrder); + } - return rootView; + /** + * Create the adapter with selected sort order + */ + mActiveListAdapter = new ActiveListAdapter(getActivity(), ShoppingList.class, + R.layout.single_active_list, orderedActiveUserListsRef, + mEncodedEmail); + + /** + * Set the adapter to the mListView + */ + mListView.setAdapter(mActiveListAdapter); } + /** + * Cleanup the adapter when activity is paused. + */ @Override - public void onDestroy() { - super.onDestroy(); + public void onPause() { + super.onPause(); + mActiveListAdapter.cleanup(); } - /** - * Link layout elements from XML + * Link list view from XML */ private void initializeScreen(View rootView) { mListView = (ListView) rootView.findViewById(R.id.list_view_active_lists); - // Get the TextViews in the single_active_list layout for list name, edit time and owner - mTextViewListName = (TextView) rootView.findViewById(R.id.text_view_list_name); - mTextViewListOwner = (TextView) rootView.findViewById(R.id.text_view_created_by_user); - mTextViewEditTime = (TextView) rootView.findViewById(R.id.text_view_edit_time); } } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/login/CreateAccountActivity.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/login/CreateAccountActivity.java new file mode 100644 index 0000000..40343f4 --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/login/CreateAccountActivity.java @@ -0,0 +1,275 @@ +package com.udacity.firebase.shoppinglistplusplus.ui.login; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.Menu; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Toast; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebase.client.AuthData; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.ServerValue; +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.ui.BaseActivity; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents Sign up screen and functionality of the app + */ +public class CreateAccountActivity extends BaseActivity { + private static final String LOG_TAG = CreateAccountActivity.class.getSimpleName(); + private ProgressDialog mAuthProgressDialog; + private Firebase mFirebaseRef; + private EditText mEditTextUsernameCreate, mEditTextEmailCreate; + private String mUserName, mUserEmail, mPassword; + private SecureRandom mRandom = new SecureRandom(); + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_create_account); + + /** + * Create Firebase references + */ + mFirebaseRef = new Firebase(Constants.FIREBASE_URL); + + /** + * Link layout elements from XML and setup the progress dialog + */ + initializeScreen(); + } + + /** + * Override onCreateOptionsMenu to inflate nothing + * + * @param menu The menu with which nothing will happen + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return true; + } + + + /** + * Link layout elements from XML and setup the progress dialog + */ + public void initializeScreen() { + mEditTextUsernameCreate = (EditText) findViewById(R.id.edit_text_username_create); + mEditTextEmailCreate = (EditText) findViewById(R.id.edit_text_email_create); + LinearLayout linearLayoutCreateAccountActivity = (LinearLayout) findViewById(R.id.linear_layout_create_account_activity); + initializeBackground(linearLayoutCreateAccountActivity); + + /* Setup the progress dialog that is displayed later when authenticating with Firebase */ + mAuthProgressDialog = new ProgressDialog(this); + mAuthProgressDialog.setTitle(getResources().getString(R.string.progress_dialog_loading)); + mAuthProgressDialog.setMessage(getResources().getString(R.string.progress_dialog_check_inbox)); + mAuthProgressDialog.setCancelable(false); + } + + /** + * Open LoginActivity when user taps on "Sign in" textView + */ + public void onSignInPressed(View view) { + Intent intent = new Intent(CreateAccountActivity.this, LoginActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } + + /** + * Create new account using Firebase email/password provider + */ + public void onCreateAccountPressed(View view) { + mUserName = mEditTextUsernameCreate.getText().toString(); + mUserEmail = mEditTextEmailCreate.getText().toString().toLowerCase(); + mPassword = new BigInteger(130, mRandom).toString(32); + + /** + * Check that email and user name are okay + */ + boolean validEmail = isEmailValid(mUserEmail); + boolean validUserName = isUserNameValid(mUserName); + if (!validEmail || !validUserName) return; + + /** + * If everything was valid show the progress dialog to indicate that + * account creation has started + */ + mAuthProgressDialog.show(); + + /** + * Create new user with specified email and password + */ + mFirebaseRef.createUser(mUserEmail, mPassword, new Firebase.ValueResultHandler>() { + @Override + public void onSuccess(final Map result) { + /** + * If user was successfully created, run resetPassword() to send temporary 24h + * password to the user's email and make sure that user owns specified email + */ + mFirebaseRef.resetPassword(mUserEmail, new Firebase.ResultHandler() { + @Override + public void onSuccess() { + + mFirebaseRef.authWithPassword(mUserEmail, mPassword, new Firebase.AuthResultHandler() { + @Override + public void onAuthenticated(AuthData authData) { + mAuthProgressDialog.dismiss(); + Log.i(LOG_TAG, getString(R.string.log_message_auth_successful)); + + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(CreateAccountActivity.this); + SharedPreferences.Editor spe = sp.edit(); + + /** + * Save name and email to sharedPreferences to create User database record + * when the registered user will sign in for the first time + */ + spe.putString(Constants.KEY_SIGNUP_EMAIL, mUserEmail).apply(); + + /** + * Encode user email replacing "." with "," + * to be able to use it as a Firebase db key + */ + createUserInFirebaseHelper((String) result.get("uid")); + + /** + * Password reset email sent, open app chooser to pick app + * for handling inbox email intent + */ + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_APP_EMAIL); + try { + startActivity(intent); + finish(); + } catch (android.content.ActivityNotFoundException ex) { + /* User does not have any app to handle email */ + } + } + + @Override + public void onAuthenticationError(FirebaseError firebaseError) { + Log.e(LOG_TAG, firebaseError.getMessage()); + } + }); + + } + + @Override + public void onError(FirebaseError firebaseError) { + /* Error occurred, log the error and dismiss the progress dialog */ + Log.d(LOG_TAG, getString(R.string.log_error_occurred) + + firebaseError); + mAuthProgressDialog.dismiss(); + } + }); + + + } + + @Override + public void onError(FirebaseError firebaseError) { + /* Error occurred, log the error and dismiss the progress dialog */ + Log.d(LOG_TAG, getString(R.string.log_error_occurred) + + firebaseError); + mAuthProgressDialog.dismiss(); + /* Display the appropriate error message */ + if (firebaseError.getCode() == FirebaseError.EMAIL_TAKEN) { + mEditTextEmailCreate.setError(getString(R.string.error_email_taken)); + } else { + showErrorToast(firebaseError.getMessage()); + } + + } + }); + + + } + + /** + * Creates a new user in Firebase from the Java POJO + */ + private void createUserInFirebaseHelper(final String authUserId) { + final String encodedEmail = Utils.encodeEmail(mUserEmail); + + /** + * Create the user and uid mapping + */ + HashMap userAndUidMapping = new HashMap(); + + /* Set raw version of date to the ServerValue.TIMESTAMP value and save into dateCreatedMap */ + HashMap timestampJoined = new HashMap<>(); + timestampJoined.put(Constants.FIREBASE_PROPERTY_TIMESTAMP, ServerValue.TIMESTAMP); + + /* Create a HashMap version of the user to add */ + User newUser = new User(mUserName, encodedEmail, timestampJoined); + HashMap newUserMap = (HashMap) + new ObjectMapper().convertValue(newUser, Map.class); + + /* Add the user and UID to the update map */ + userAndUidMapping.put("/" + Constants.FIREBASE_LOCATION_USERS + "/" + encodedEmail, + newUserMap); + userAndUidMapping.put("/" + Constants.FIREBASE_LOCATION_UID_MAPPINGS + "/" + + authUserId, encodedEmail); + + /* Try to update the database; if there is already a user, this will fail */ + mFirebaseRef.updateChildren(userAndUidMapping, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + if (firebaseError != null) { + /* Try just making a uid mapping */ + mFirebaseRef.child(Constants.FIREBASE_LOCATION_UID_MAPPINGS) + .child(authUserId).setValue(encodedEmail); + } + /** + * The value has been set or it failed; either way, log out the user since + * they were only logged in with a temp password + **/ + mFirebaseRef.unauth(); + } + }); + } + + private boolean isEmailValid(String email) { + boolean isGoodEmail = + (email != null && android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()); + if (!isGoodEmail) { + mEditTextEmailCreate.setError(String.format(getString(R.string.error_invalid_email_not_valid), + email)); + return false; + } + return isGoodEmail; + } + + private boolean isUserNameValid(String userName) { + if (userName.equals("")) { + mEditTextUsernameCreate.setError(getResources().getString(R.string.error_cannot_be_empty)); + return false; + } + return true; + } + + + /** + * Show error toast to users + */ + private void showErrorToast(String message) { + Toast.makeText(CreateAccountActivity.this, message, Toast.LENGTH_LONG).show(); + } +} diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/login/LoginActivity.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/login/LoginActivity.java new file mode 100644 index 0000000..84df4f8 --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/login/LoginActivity.java @@ -0,0 +1,589 @@ +package com.udacity.firebase.shoppinglistplusplus.ui.login; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebase.client.AuthData; +import com.firebase.client.DataSnapshot; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.ServerValue; +import com.firebase.client.ValueEventListener; +import com.google.android.gms.auth.GoogleAuthException; +import com.google.android.gms.auth.GoogleAuthUtil; +import com.google.android.gms.auth.UserRecoverableAuthException; +import com.google.android.gms.auth.api.Auth; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInResult; +import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.Scopes; +import com.google.android.gms.common.SignInButton; +import com.google.android.gms.common.api.Scope; +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.ui.BaseActivity; +import com.udacity.firebase.shoppinglistplusplus.ui.MainActivity; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents Sign in screen and functionality of the app + */ +public class LoginActivity extends BaseActivity { + + private static final String LOG_TAG = LoginActivity.class.getSimpleName(); + /* A dialog that is presented until the Firebase authentication finished. */ + private ProgressDialog mAuthProgressDialog; + /* References to the Firebase */ + private Firebase mFirebaseRef; + /* Listener for Firebase session changes */ + private Firebase.AuthStateListener mAuthStateListener; + private EditText mEditTextEmailInput, mEditTextPasswordInput; + + private SharedPreferences mSharedPref; + private SharedPreferences.Editor mSharedPrefEditor; + + /** + * Variables related to Google Login + */ + /* A flag indicating that a PendingIntent is in progress and prevents us from starting further intents. */ + private boolean mGoogleIntentInProgress; + /* Request code used to invoke sign in user interactions for Google+ */ + public static final int RC_GOOGLE_LOGIN = 1; + /* A Google account object that is populated if the user signs in with Google */ + GoogleSignInAccount mGoogleAccount; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + mSharedPref = PreferenceManager.getDefaultSharedPreferences(this); + mSharedPrefEditor = mSharedPref.edit(); + + /** + * Create Firebase references + */ + mFirebaseRef = new Firebase(Constants.FIREBASE_URL); + + /** + * Link layout elements from XML and setup progress dialog + */ + initializeScreen(); + + /** + * Call signInPassword() when user taps "Done" keyboard action + */ + mEditTextPasswordInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { + + if (actionId == EditorInfo.IME_ACTION_DONE || keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + signInPassword(); + } + return true; + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + + /** + * This is the authentication listener that maintains the current user session + * and signs in automatically on application launch + */ + mAuthStateListener = new Firebase.AuthStateListener() { + @Override + public void onAuthStateChanged(AuthData authData) { + mAuthProgressDialog.dismiss(); + + /** + * If there is a valid session to be restored, start MainActivity. + * No need to pass data via SharedPreferences because app + * already holds userName/provider data from the latest session + */ + if (authData != null) { + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } + } + }; + /* Add auth listener to Firebase ref */ + mFirebaseRef.addAuthStateListener(mAuthStateListener); + + /** + * Get the newly registered user email if present, use null as default value + */ + String signupEmail = mSharedPref.getString(Constants.KEY_SIGNUP_EMAIL, null); + + /** + * Fill in the email editText and remove value from SharedPreferences if email is present + */ + if (signupEmail != null) { + mEditTextEmailInput.setText(signupEmail); + + /** + * Clear signupEmail sharedPreferences to make sure that they are used just once + */ + mSharedPrefEditor.putString(Constants.KEY_SIGNUP_EMAIL, null).apply(); + } + } + + /** + * Cleans up listeners tied to the user's authentication state + */ + @Override + public void onPause() { + super.onPause(); + mFirebaseRef.removeAuthStateListener(mAuthStateListener); + } + + /** + * Override onCreateOptionsMenu to inflate nothing + * + * @param menu The menu with which nothing will happen + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return true; + } + + + /** + * Sign in with Password provider when user clicks sign in button + */ + public void onSignInPressed(View view) { + signInPassword(); + } + + /** + * Open CreateAccountActivity when user taps on "Sign up" TextView + */ + public void onSignUpPressed(View view) { + Intent intent = new Intent(LoginActivity.this, CreateAccountActivity.class); + startActivity(intent); + } + + /** + * Link layout elements from XML and setup the progress dialog + */ + public void initializeScreen() { + mEditTextEmailInput = (EditText) findViewById(R.id.edit_text_email); + mEditTextPasswordInput = (EditText) findViewById(R.id.edit_text_password); + LinearLayout linearLayoutLoginActivity = (LinearLayout) findViewById(R.id.linear_layout_login_activity); + initializeBackground(linearLayoutLoginActivity); + /* Setup the progress dialog that is displayed later when authenticating with Firebase */ + mAuthProgressDialog = new ProgressDialog(this); + mAuthProgressDialog.setTitle(getString(R.string.progress_dialog_loading)); + mAuthProgressDialog.setMessage(getString(R.string.progress_dialog_authenticating_with_firebase)); + mAuthProgressDialog.setCancelable(false); + /* Setup Google Sign In */ + setupGoogleSignIn(); + } + + /** + * Sign in with Password provider (used when user taps "Done" action on keyboard) + */ + public void signInPassword() { + String email = mEditTextEmailInput.getText().toString(); + String password = mEditTextPasswordInput.getText().toString(); + + /** + * If email and password are not empty show progress dialog and try to authenticate + */ + if (email.equals("")) { + mEditTextEmailInput.setError(getString(R.string.error_cannot_be_empty)); + return; + } + + if (password.equals("")) { + mEditTextPasswordInput.setError(getString(R.string.error_cannot_be_empty)); + return; + } + mAuthProgressDialog.show(); + mFirebaseRef.authWithPassword(email, password, new MyAuthResultHandler(Constants.PASSWORD_PROVIDER)); + } + + /** + * Handle user authentication that was initiated with mFirebaseRef.authWithPassword + * or mFirebaseRef.authWithOAuthToken + */ + private class MyAuthResultHandler implements Firebase.AuthResultHandler { + + private final String provider; + + public MyAuthResultHandler(String provider) { + this.provider = provider; + } + + /** + * On successful authentication call setAuthenticatedUser if it was not already + * called in + */ + @Override + public void onAuthenticated(AuthData authData) { + mAuthProgressDialog.dismiss(); + Log.i(LOG_TAG, provider + " " + getString(R.string.log_message_auth_successful)); + + if (authData != null) { + /** + * If user has logged in with Google provider + */ + if (authData.getProvider().equals(Constants.PASSWORD_PROVIDER)) { + setAuthenticatedUserPasswordProvider(authData); + } else + /** + * If user has logged in with Password provider + */ + if (authData.getProvider().equals(Constants.GOOGLE_PROVIDER)) { + setAuthenticatedUserGoogle(authData); + } else { + Log.e(LOG_TAG, getString(R.string.log_error_invalid_provider) + authData.getProvider()); + } + + /* Save provider name and encodedEmail for later use and start MainActivity */ + mSharedPrefEditor.putString(Constants.KEY_PROVIDER, authData.getProvider()).apply(); + mSharedPrefEditor.putString(Constants.KEY_ENCODED_EMAIL, mEncodedEmail).apply(); + + /* Go to main activity */ + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } + } + + @Override + public void onAuthenticationError(FirebaseError firebaseError) { + mAuthProgressDialog.dismiss(); + + /** + * Use utility method to check the network connection state + * Show "No network connection" if there is no connection + * Show Firebase specific error message otherwise + */ + switch (firebaseError.getCode()) { + case FirebaseError.INVALID_EMAIL: + case FirebaseError.USER_DOES_NOT_EXIST: + mEditTextEmailInput.setError(getString(R.string.error_message_email_issue)); + break; + case FirebaseError.INVALID_PASSWORD: + mEditTextPasswordInput.setError(firebaseError.getMessage()); + break; + case FirebaseError.NETWORK_ERROR: + showErrorToast(getString(R.string.error_message_failed_sign_in_no_network)); + break; + default: + showErrorToast(firebaseError.toString()); + } + } + } + + /** + * Helper method that makes sure a user is created if the user + * logs in with Firebase's email/password provider. + * + * @param authData AuthData object returned from onAuthenticated + */ + private void setAuthenticatedUserPasswordProvider(AuthData authData) { + final String unprocessedEmail = authData.getProviderData().get(Constants.FIREBASE_PROPERTY_EMAIL).toString().toLowerCase(); + /** + * Encode user email replacing "." with "," + * to be able to use it as a Firebase db key + */ + mEncodedEmail = Utils.encodeEmail(unprocessedEmail); + + final Firebase userRef = new Firebase(Constants.FIREBASE_URL_USERS).child(mEncodedEmail); + + /** + * Check if current user has logged in at least once + */ + userRef.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + User user = dataSnapshot.getValue(User.class); + + if (user != null) { + + /** + * If recently registered user has hasLoggedInWithPassword = "false" + * (never logged in using password provider) + */ + if (!user.isHasLoggedInWithPassword()) { + + /** + * Change password if user that just signed in signed up recently + * to make sure that user will be able to use temporary password + * from the email more than 24 hours + */ + mFirebaseRef.changePassword(unprocessedEmail, mEditTextPasswordInput.getText().toString(), mEditTextPasswordInput.getText().toString(), new Firebase.ResultHandler() { + @Override + public void onSuccess() { + userRef.child(Constants.FIREBASE_PROPERTY_USER_HAS_LOGGED_IN_WITH_PASSWORD).setValue(true); + /* The password was changed */ + Log.d(LOG_TAG, getString(R.string.log_message_password_changed_successfully) + mEditTextPasswordInput.getText().toString()); + } + + @Override + public void onError(FirebaseError firebaseError) { + Log.d(LOG_TAG, getString(R.string.log_error_failed_to_change_password) + firebaseError); + } + }); + } + } + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(LOG_TAG, + getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + + } + + /** + * Helper method that makes sure a user is created if the user + * logs in with Firebase's Google login provider. + * + * @param authData AuthData object returned from onAuthenticated + */ + private void setAuthenticatedUserGoogle(final AuthData authData) { + /** + * If google api client is connected, get the lowerCase user email + * and save in sharedPreferences + */ + String unprocessedEmail; + if (mGoogleApiClient.isConnected()) { + unprocessedEmail = mGoogleAccount.getEmail().toLowerCase(); + mSharedPrefEditor.putString(Constants.KEY_GOOGLE_EMAIL, unprocessedEmail).apply(); + } else { + + /** + * Otherwise get email from sharedPreferences, use null as default value + * (this mean that user resumes his session) + */ + unprocessedEmail = mSharedPref.getString(Constants.KEY_GOOGLE_EMAIL, null); + } + + /** + * Encode user email replacing "." with "," to be able to use it + * as a Firebase db key + */ + mEncodedEmail = Utils.encodeEmail(unprocessedEmail); + + /* Get username from authData */ + final String userName = (String) authData.getProviderData().get(Constants.PROVIDER_DATA_DISPLAY_NAME); + + /* Make a user */ + final Firebase userLocation = new Firebase(Constants.FIREBASE_URL_USERS).child(mEncodedEmail); + + HashMap userAndUidMapping = new HashMap(); + + HashMap timestampJoined = new HashMap<>(); + timestampJoined.put(Constants.FIREBASE_PROPERTY_TIMESTAMP, ServerValue.TIMESTAMP); + + /* Create a HashMap version of the user to add */ + User newUser = new User(userName, mEncodedEmail, timestampJoined); + HashMap newUserMap = (HashMap) + new ObjectMapper().convertValue(newUser, Map.class); + + /* Add the user and UID to the update map */ + userAndUidMapping.put("/" + Constants.FIREBASE_LOCATION_USERS + "/" + mEncodedEmail, + newUserMap); + userAndUidMapping.put("/" + Constants.FIREBASE_LOCATION_UID_MAPPINGS + "/" + + authData.getUid(), mEncodedEmail); + + /* Update the database; it will fail if a user already exists */ + mFirebaseRef.updateChildren(userAndUidMapping, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + if (firebaseError != null) { + /* Try just making a uid mapping */ + mFirebaseRef.child(Constants.FIREBASE_LOCATION_UID_MAPPINGS) + .child(authData.getUid()).setValue(mEncodedEmail); + } + } + }); + } + + /** + * Show error toast to users + */ + private void showErrorToast(String message) { + Toast.makeText(LoginActivity.this, message, Toast.LENGTH_LONG).show(); + } + + + /** + * Signs you into ShoppingList++ using the Google Login Provider + * + * @param token A Google OAuth access token returned from Google + */ + private void loginWithGoogle(String token) { + mFirebaseRef.authWithOAuthToken(Constants.GOOGLE_PROVIDER, token, new MyAuthResultHandler(Constants.GOOGLE_PROVIDER)); + } + + /** + * GOOGLE SIGN IN CODE + *

+ * This code is mostly boiler plate from + * https://developers.google.com/identity/sign-in/android/start-integrating + * and + * https://github.com/googlesamples/google-services/blob/master/android/signin/app/src/main/java/com/google/samples/quickstart/signin/SignInActivity.java + *

+ * The big picture steps are: + * 1. User clicks the sign in with Google button + * 2. An intent is started for sign in. + * - If the connection fails it is caught in the onConnectionFailed callback + * - If it finishes, onActivityResult is called with the correct request code. + * 3. If the sign in was successful, set the mGoogleAccount to the current account and + * then call get GoogleOAuthTokenAndLogin + * 4. getGoogleOAuthTokenAndLogin launches an AsyncTask to get an OAuth2 token from Google. + * 5. Once this token is retrieved it is available to you in the onPostExecute method of + * the AsyncTask. **This is the token required by Firebase** + */ + + + /* Sets up the Google Sign In Button : https://developers.google.com/android/reference/com/google/android/gms/common/SignInButton */ + private void setupGoogleSignIn() { + SignInButton signInButton = (SignInButton) findViewById(R.id.login_with_google); + signInButton.setSize(SignInButton.SIZE_WIDE); + signInButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onSignInGooglePressed(v); + } + }); + } + + /** + * Sign in with Google plus when user clicks "Sign in with Google" textView (button) + */ + public void onSignInGooglePressed(View view) { + Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient); + startActivityForResult(signInIntent, RC_GOOGLE_LOGIN); + mAuthProgressDialog.show(); + + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + /** + * An unresolvable error has occurred and Google APIs (including Sign-In) will not + * be available. + */ + mAuthProgressDialog.dismiss(); + showErrorToast(result.toString()); + } + + + /** + * This callback is triggered when any startActivityForResult finishes. The requestCode maps to + * the value passed into startActivityForResult. + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + /* Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...); */ + if (requestCode == RC_GOOGLE_LOGIN) { + GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data); + handleSignInResult(result); + } + + } + + private void handleSignInResult(GoogleSignInResult result) { + Log.d(LOG_TAG, "handleSignInResult:" + result.isSuccess()); + if (result.isSuccess()) { + /* Signed in successfully, get the OAuth token */ + mGoogleAccount = result.getSignInAccount(); + getGoogleOAuthTokenAndLogin(); + + + } else { + if (result.getStatus().getStatusCode() == GoogleSignInStatusCodes.SIGN_IN_CANCELLED) { + showErrorToast("The sign in was cancelled. Make sure you're connected to the internet and try again."); + } else { + showErrorToast("Error handling the sign in: " + result.getStatus().getStatusMessage()); + } + mAuthProgressDialog.dismiss(); + } + } + + /** + * Gets the GoogleAuthToken and logs in. + */ + private void getGoogleOAuthTokenAndLogin() { + /* Get OAuth token in Background */ + AsyncTask task = new AsyncTask() { + String mErrorMessage = null; + + @Override + protected String doInBackground(Void... params) { + String token = null; + + try { + String scope = String.format(getString(R.string.oauth2_format), new Scope(Scopes.PROFILE)) + " email"; + + token = GoogleAuthUtil.getToken(LoginActivity.this, mGoogleAccount.getEmail(), scope); + } catch (IOException transientEx) { + /* Network or server error */ + Log.e(LOG_TAG, getString(R.string.google_error_auth_with_google) + transientEx); + mErrorMessage = getString(R.string.google_error_network_error) + transientEx.getMessage(); + } catch (UserRecoverableAuthException e) { + Log.w(LOG_TAG, getString(R.string.google_error_recoverable_oauth_error) + e.toString()); + + /* We probably need to ask for permissions, so start the intent if there is none pending */ + if (!mGoogleIntentInProgress) { + mGoogleIntentInProgress = true; + Intent recover = e.getIntent(); + startActivityForResult(recover, RC_GOOGLE_LOGIN); + } + } catch (GoogleAuthException authEx) { + /* The call is not ever expected to succeed assuming you have already verified that + * Google Play services is installed. */ + Log.e(LOG_TAG, " " + authEx.getMessage(), authEx); + mErrorMessage = getString(R.string.google_error_auth_with_google) + authEx.getMessage(); + } + return token; + } + + @Override + protected void onPostExecute(String token) { + mAuthProgressDialog.dismiss(); + if (token != null) { + /* Successfully got OAuth token, now login with Google */ + loginWithGoogle(token); + } else if (mErrorMessage != null) { + showErrorToast(mErrorMessage); + } + } + }; + + task.execute(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/AddFriendActivity.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/AddFriendActivity.java new file mode 100755 index 0000000..e27c631 --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/AddFriendActivity.java @@ -0,0 +1,102 @@ +package com.udacity.firebase.shoppinglistplusplus.ui.sharing; + +import android.os.Bundle; +import android.support.v7.widget.Toolbar; +import android.text.Editable; +import android.text.TextWatcher; +import android.widget.EditText; +import android.widget.ListView; + +import com.firebase.client.Firebase; +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.ui.BaseActivity; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; + +/** + * Represents the Add Friend screen and functionality + */ +public class AddFriendActivity extends BaseActivity { + private EditText mEditTextAddFriendEmail; + private AutocompleteFriendAdapter mFriendsAutocompleteAdapter; + private String mInput; + private ListView mListViewAutocomplete; + private Firebase mUsersRef; + + + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_add_friend); + /** + * Create Firebase references + */ + mUsersRef = new Firebase(Constants.FIREBASE_URL_USERS); + + /** + * Link layout elements from XML and setup the toolbar + */ + initializeScreen(); + + /** + * Set interactive bits, such as click events/adapters + */ + + mEditTextAddFriendEmail.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + /* Get the input after every textChanged event and transform it to lowercase */ + mInput = mEditTextAddFriendEmail.getText().toString().toLowerCase(); + + /* Clean up the old adapter */ + if (mFriendsAutocompleteAdapter != null) mFriendsAutocompleteAdapter.cleanup(); + /* Nullify the adapter data if the input length is less than 2 characters */ + if (mInput.equals("") || mInput.length() < 2) { + mListViewAutocomplete.setAdapter(null); + + /* Define and set the adapter otherwise. */ + } else { + mFriendsAutocompleteAdapter = new AutocompleteFriendAdapter(AddFriendActivity.this, User.class, + R.layout.single_autocomplete_item, mUsersRef.orderByChild(Constants.FIREBASE_PROPERTY_EMAIL) + .startAt(mInput).endAt(mInput + "~").limitToFirst(5), mEncodedEmail); + + mListViewAutocomplete.setAdapter(mFriendsAutocompleteAdapter); + } + + } + }); + + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mFriendsAutocompleteAdapter != null) { + mFriendsAutocompleteAdapter.cleanup(); + } + } + + /** + * Link layout elements from XML and setup the toolbar + */ + public void initializeScreen() { + mListViewAutocomplete = (ListView) findViewById(R.id.list_view_friends_autocomplete); + mEditTextAddFriendEmail = (EditText) findViewById(R.id.edit_text_add_friend_email); + Toolbar toolbar = (Toolbar) findViewById(R.id.app_bar); + setSupportActionBar(toolbar); + /* Add back button to the action bar */ + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + } +} diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/AutocompleteFriendAdapter.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/AutocompleteFriendAdapter.java new file mode 100644 index 0000000..3f4afb4 --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/AutocompleteFriendAdapter.java @@ -0,0 +1,119 @@ +package com.udacity.firebase.shoppinglistplusplus.ui.sharing; + +import android.app.Activity; +import android.util.Log; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.firebase.client.DataSnapshot; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.Query; +import com.firebase.client.ValueEventListener; +import com.firebase.ui.FirebaseListAdapter; +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +/** + * Populates the list_view_friends_autocomplete inside AddFriendActivity + */ +public class AutocompleteFriendAdapter extends FirebaseListAdapter { + private String mEncodedEmail; + + /** + * Public constructor that initializes private instance variables when adapter is created + */ + public AutocompleteFriendAdapter(Activity activity, Class modelClass, int modelLayout, + Query ref, String encodedEmail) { + super(activity, modelClass, modelLayout, ref); + this.mActivity = activity; + this.mEncodedEmail = encodedEmail; + } + + /** + * Protected method that populates the view attached to the adapter (list_view_friends_autocomplete) + * with items inflated from single_autocomplete_item.xml + * populateView also handles data changes and updates the listView accordingly + */ + @Override + protected void populateView(View view, final User user) { + /* Get friends email textview and set it's text to user.email() */ + TextView textViewFriendEmail = (TextView) view.findViewById(R.id.text_view_autocomplete_item); + textViewFriendEmail.setText(Utils.decodeEmail(user.getEmail())); + + /** + * Set the onClickListener to a single list item + * If selected email is not friend already and if it is not the + * current user's email, we add selected user to current user's friends + */ + textViewFriendEmail.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + /** + * If selected user is not current user proceed + */ + if (isNotCurrentUser(user)) { + + Firebase currentUserFriendsRef = new Firebase(Constants.FIREBASE_URL_USER_FRIENDS).child(mEncodedEmail); + final Firebase friendRef = currentUserFriendsRef.child(user.getEmail()); + + /** + * Add listener for single value event to perform a one time operation + */ + friendRef.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + + /** + * Add selected user to current user's friends if not in friends yet + */ + if (isNotAlreadyAdded(dataSnapshot, user)) { + friendRef.setValue(user); + mActivity.finish(); + } + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(mActivity.getClass().getSimpleName(), + mActivity.getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + + } + } + }); + + } + + private boolean isNotCurrentUser(User user) { + if (user.getEmail().equals(mEncodedEmail)) { + /* Toast appropriate error message if the user is trying to add themselves */ + Toast.makeText(mActivity, + mActivity.getResources().getString(R.string.toast_you_cant_add_yourself), + Toast.LENGTH_LONG).show(); + return false; + } + return true; + } + + private boolean isNotAlreadyAdded(DataSnapshot dataSnapshot, User user) { + if (dataSnapshot.getValue(User.class) != null) { + /* Toast appropriate error message if the user is already a friend of the user */ + String friendError = String.format(mActivity.getResources(). + getString(R.string.toast_is_already_your_friend), + user.getName()); + + Toast.makeText(mActivity, + friendError, + Toast.LENGTH_LONG).show(); + return false; + } + return true; + } +} diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/FriendAdapter.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/FriendAdapter.java new file mode 100644 index 0000000..4cb286d --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/FriendAdapter.java @@ -0,0 +1,223 @@ +package com.udacity.firebase.shoppinglistplusplus.ui.sharing; + +import android.app.Activity; +import android.util.Log; +import android.view.View; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebase.client.DataSnapshot; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.Query; +import com.firebase.client.ValueEventListener; +import com.firebase.ui.FirebaseListAdapter; +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; +import com.udacity.firebase.shoppinglistplusplus.utils.Utils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Populates the list_view_friends_share inside ShareListActivity + */ +public class FriendAdapter extends FirebaseListAdapter { + private ShoppingList mShoppingList; + private static final String LOG_TAG = FriendAdapter.class.getSimpleName(); + private String mListId; + private Firebase mFirebaseRef; + private HashMap mSharedUsersList; + private HashMap mLocationListenerMap; + + + /** + * Public constructor that initializes private instance variables when adapter is created + */ + public FriendAdapter(Activity activity, Class modelClass, int modelLayout, + Query ref, String listId) { + super(activity, modelClass, modelLayout, ref); + this.mActivity = activity; + this.mListId = listId; + mFirebaseRef = new Firebase(Constants.FIREBASE_URL); + mLocationListenerMap = new HashMap(); + } + + /** + * Protected method that populates the view attached to the adapter (list_view_friends_autocomplete) + * with items inflated from single_user_item.xml + * populateView also handles data changes and updates the listView accordingly + */ + @Override + protected void populateView(View view, final User friend) { + ((TextView) view.findViewById(R.id.user_name)).setText(friend.getName()); + final ImageButton buttonToggleShare = (ImageButton) view.findViewById(R.id.button_toggle_share); + + final Firebase sharedFriendInShoppingListRef = new Firebase(Constants.FIREBASE_URL_LISTS_SHARED_WITH) + .child(mListId).child(friend.getEmail()); + + /** + * Gets the value of the friend from the ShoppingList's sharedWith list of users + * and then allows the friend to be toggled as shared with or not. + * + * The friend in the snapshot (sharedFriendInShoppingList) will either be a User object + * (if they are in the the sharedWith list) or null (if they are not in the sharedWith + * list) + */ + + ValueEventListener listener = sharedFriendInShoppingListRef.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + final User sharedFriendInShoppingList = snapshot.getValue(User.class); + + /** + * If list is already being shared with this friend, set the buttonToggleShare + * to remove selected friend from sharedWith onClick and change the + * buttonToggleShare image to green + */ + if (sharedFriendInShoppingList != null) { + buttonToggleShare.setImageResource(R.drawable.ic_shared_check); + buttonToggleShare.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + /** + * Create map and fill it in with deep path multi write operations list. + * Use false to mark that you are removing this friend + */ + HashMap updatedUserData = updateFriendInSharedWith(false, friend); + + /* Do a deep-path update */ + mFirebaseRef.updateChildren(updatedUserData, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + Utils.updateTimestampReversed(firebaseError, LOG_TAG, mListId, + mSharedUsersList, mShoppingList.getOwner()); + } + }); + } + }); + } else { + + /** + * Set the buttonToggleShare onClickListener to add selected friend to sharedWith + * and change the buttonToggleShare image to grey otherwise + */ + buttonToggleShare.setImageResource(R.drawable.icon_add_friend); + buttonToggleShare.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + /** + * Create map and fill it in with deep path multi write operations list + */ + HashMap updatedUserData = updateFriendInSharedWith(true, friend); + + /* Do a deep-path update */ + mFirebaseRef.updateChildren(updatedUserData, new Firebase.CompletionListener() { + @Override + public void onComplete(FirebaseError firebaseError, Firebase firebase) { + Utils.updateTimestampReversed(firebaseError, LOG_TAG, mListId, + mSharedUsersList, mShoppingList.getOwner()); + } + }); + } + }); + } + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(LOG_TAG, + mActivity.getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + /* Add the listener to the HashMap so that it can be removed on cleanup */ + mLocationListenerMap.put(sharedFriendInShoppingListRef, listener); + + } + + /** + * Public method that is used to pass ShoppingList object when it is loaded in ValueEventListener + */ + public void setShoppingList(ShoppingList shoppingList) { + this.mShoppingList = shoppingList; + this.notifyDataSetChanged(); + } + + /** + * Public method that is used to pass SharedUsers when they are loaded in ValueEventListener + */ + public void setSharedWithUsers(HashMap sharedUsersList) { + this.mSharedUsersList = sharedUsersList; + this.notifyDataSetChanged(); + } + + /** + * This method does the tricky job of adding or removing a friend from the sharedWith list. + * @param addFriend This is true if the friend is being added, false is the friend is being removed. + * @param friendToAddOrRemove This is the friend to either add or remove + * @return + */ + private HashMap updateFriendInSharedWith(Boolean addFriend, User friendToAddOrRemove) { + HashMap updatedUserData = new HashMap(); + + /* The newSharedWith lists contains all users who need their last time changed updated */ + HashMap newSharedWith = new HashMap(mSharedUsersList); + + if (addFriend) { + /** + * Changes the timestamp changed to now; Because of ancestry issues, we cannot + * have one updateChildren call that both creates data and then updates that same data + * because updateChildren has no way of knowing what was the intended update + */ + mShoppingList.setTimestampLastChangedToNow(); + /* Make it a HashMap of the shopping list and user */ + final HashMap shoppingListForFirebase = (HashMap) + new ObjectMapper().convertValue(mShoppingList, Map.class); + + final HashMap friendForFirebase = (HashMap) + new ObjectMapper().convertValue(friendToAddOrRemove, Map.class); + + /* Add the friend to the shared list */ + updatedUserData.put("/" + Constants.FIREBASE_LOCATION_LISTS_SHARED_WITH + "/" + mListId + + "/" + friendToAddOrRemove.getEmail(), friendForFirebase); + + /* Add that shopping list hashmap to the new user's active lists */ + updatedUserData.put("/" + Constants.FIREBASE_LOCATION_USER_LISTS + "/" + friendToAddOrRemove.getEmail() + + "/" + mListId, shoppingListForFirebase); + + } else { + /* Remove the friend from the shared list */ + updatedUserData.put("/" + Constants.FIREBASE_LOCATION_LISTS_SHARED_WITH + "/" + mListId + + "/" + friendToAddOrRemove.getEmail(), null); + + /* Remove the list from the shared friend */ + updatedUserData.put("/" + Constants.FIREBASE_LOCATION_USER_LISTS + "/" + friendToAddOrRemove.getEmail() + + "/" + mListId, null); + + newSharedWith.remove(friendToAddOrRemove.getEmail()); + + } + + Utils.updateMapWithTimestampLastChanged(newSharedWith, + mListId, mShoppingList.getOwner(), updatedUserData); + + return updatedUserData; + } + + @Override + public void cleanup() { + super.cleanup(); + /* Clean up the event listeners */ + for (HashMap.Entry listenerToClean : mLocationListenerMap.entrySet()) + { + listenerToClean.getKey().removeEventListener(listenerToClean.getValue()); + } + + } +} diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/ShareListActivity.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/ShareListActivity.java new file mode 100755 index 0000000..aa2ee73 --- /dev/null +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/ui/sharing/ShareListActivity.java @@ -0,0 +1,156 @@ +package com.udacity.firebase.shoppinglistplusplus.ui.sharing; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.View; +import android.widget.ListView; + +import com.firebase.client.DataSnapshot; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.ValueEventListener; +import com.udacity.firebase.shoppinglistplusplus.R; +import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.User; +import com.udacity.firebase.shoppinglistplusplus.ui.BaseActivity; +import com.udacity.firebase.shoppinglistplusplus.utils.Constants; + +import java.util.HashMap; + +/** + * Allows for you to check and un-check friends that you share the current list with + */ +public class ShareListActivity extends BaseActivity { + private static final String LOG_TAG = ShareListActivity.class.getSimpleName(); + private FriendAdapter mFriendAdapter; + private ListView mListView; + private ShoppingList mShoppingList; + private String mListId; + private Firebase mActiveListRef, mSharedWithRef; + private ValueEventListener mActiveListRefListener, mSharedWithListener; + private HashMap mSharedWithUsers; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_share_list); + + /* Get the push ID from the extra passed by ActiveListDetailsActivity */ + Intent intent = this.getIntent(); + mListId = intent.getStringExtra(Constants.KEY_LIST_ID); + if (mListId == null) { + /* No point in continuing without a valid ID. */ + finish(); + return; + } + + /** + * Link layout elements from XML and setup the toolbar + */ + initializeScreen(); + + /** + * Create Firebase references + */ + Firebase currentUserFriendsRef = new Firebase(Constants.FIREBASE_URL_USER_FRIENDS).child(mEncodedEmail); + mActiveListRef = new Firebase(Constants.FIREBASE_URL_USER_LISTS).child(mEncodedEmail).child(mListId); + mSharedWithRef = new Firebase (Constants.FIREBASE_URL_LISTS_SHARED_WITH).child(mListId); + + /** + * Add ValueEventListeners to Firebase references + * to control get data and control behavior and visibility of elements + */ + + mActiveListRefListener = mActiveListRef.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + ShoppingList shoppingList = dataSnapshot.getValue(ShoppingList.class); + + /** + * Saving the most recent version of current shopping list into mShoppingList + * and pass it to setShoppingList() if present + * finish() the activity otherwise + */ + if (shoppingList != null) { + mShoppingList = shoppingList; + mFriendAdapter.setShoppingList(mShoppingList); + } else { + finish(); + } + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(LOG_TAG, + getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + + + mSharedWithListener = mSharedWithRef.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + mSharedWithUsers = new HashMap(); + for (DataSnapshot currentUser : dataSnapshot.getChildren()) { + mSharedWithUsers.put(currentUser.getKey(), currentUser.getValue(User.class)); + } + mFriendAdapter.setSharedWithUsers(mSharedWithUsers); + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.e(LOG_TAG, + getString(R.string.log_error_the_read_failed) + + firebaseError.getMessage()); + } + }); + + + /** + * Set interactive bits, such as click events/adapters + */ + mFriendAdapter = new FriendAdapter(ShareListActivity.this, User.class, + R.layout.single_user_item, currentUserFriendsRef, mListId); + + /* Set adapter for the listView */ + mListView.setAdapter(mFriendAdapter); + } + + /** + * Cleanup the adapter when activity is destroyed + */ + @Override + public void onDestroy() { + super.onDestroy(); + /* Set adapter for the listView */ + mFriendAdapter.cleanup(); + mActiveListRef.removeEventListener(mActiveListRefListener); + mSharedWithRef.removeEventListener(mSharedWithListener); + } + + /** + * Link layout elements from XML and setup the toolbar + */ + public void initializeScreen() { + mListView = (ListView) findViewById(R.id.list_view_friends_share); + Toolbar toolbar = (Toolbar) findViewById(R.id.app_bar); + setSupportActionBar(toolbar); + /* Add back button to the action bar */ + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + } + + /** + * Launch AddFriendActivity to find and add user to current user's friends list + * when the button AddFriend is pressed + */ + public void onAddFriendPressed(View view) { + Intent intent = new Intent(ShareListActivity.this, AddFriendActivity.class); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/utils/Constants.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/utils/Constants.java index 65d1d94..90221ac 100755 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/utils/Constants.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/utils/Constants.java @@ -9,28 +9,74 @@ public final class Constants { /** * Constants related to locations in Firebase, such as the name of the node - * where active lists are stored (ie "activeLists") + * where user lists are stored (ie "userLists") */ - public static final String FIREBASE_LOCATION_ACTIVE_LIST = "activeList"; + public static final String FIREBASE_LOCATION_SHOPPING_LIST_ITEMS = "shoppingListItems"; + public static final String FIREBASE_LOCATION_USERS = "users"; + public static final String FIREBASE_LOCATION_USER_LISTS = "userLists"; + public static final String FIREBASE_LOCATION_USER_FRIENDS = "userFriends"; + public static final String FIREBASE_LOCATION_LISTS_SHARED_WITH = "sharedWith"; + public static final String FIREBASE_LOCATION_UID_MAPPINGS = "uidMappings"; + public static final String FIREBASE_LOCATION_OWNER_MAPPINGS = "ownerMappings"; + + /** * Constants for Firebase object properties */ + public static final String FIREBASE_PROPERTY_BOUGHT = "bought"; + public static final String FIREBASE_PROPERTY_BOUGHT_BY = "boughtBy"; public static final String FIREBASE_PROPERTY_LIST_NAME = "listName"; + public static final String FIREBASE_PROPERTY_TIMESTAMP_LAST_CHANGED = "timestampLastChanged"; public static final String FIREBASE_PROPERTY_TIMESTAMP = "timestamp"; + public static final String FIREBASE_PROPERTY_ITEM_NAME = "itemName"; + public static final String FIREBASE_PROPERTY_EMAIL = "email"; + public static final String FIREBASE_PROPERTY_USERS_SHOPPING = "usersShopping"; + public static final String FIREBASE_PROPERTY_USER_HAS_LOGGED_IN_WITH_PASSWORD = "hasLoggedInWithPassword"; + public static final String FIREBASE_PROPERTY_TIMESTAMP_LAST_CHANGED_REVERSE = "timestampLastChangedReverse"; /** * Constants for Firebase URL */ public static final String FIREBASE_URL = BuildConfig.UNIQUE_FIREBASE_ROOT_URL; - public static final String FIREBASE_URL_ACTIVE_LIST = FIREBASE_URL + "/" + FIREBASE_LOCATION_ACTIVE_LIST; - + public static final String FIREBASE_URL_SHOPPING_LIST_ITEMS = FIREBASE_URL + "/" + FIREBASE_LOCATION_SHOPPING_LIST_ITEMS; + public static final String FIREBASE_URL_USERS = FIREBASE_URL + "/" + FIREBASE_LOCATION_USERS; + public static final String FIREBASE_URL_USER_LISTS = FIREBASE_URL + "/" + FIREBASE_LOCATION_USER_LISTS; + public static final String FIREBASE_URL_USER_FRIENDS = FIREBASE_URL + "/" + FIREBASE_LOCATION_USER_FRIENDS; + public static final String FIREBASE_URL_LISTS_SHARED_WITH = FIREBASE_URL + "/" + FIREBASE_LOCATION_LISTS_SHARED_WITH; /** * Constants for bundles, extras and shared preferences keys */ + public static final String KEY_LIST_NAME = "LIST_NAME"; public static final String KEY_LAYOUT_RESOURCE = "LAYOUT_RESOURCE"; + public static final String KEY_LIST_ID = "LIST_ID"; + public static final String KEY_SIGNUP_EMAIL = "SIGNUP_EMAIL"; + public static final String KEY_LIST_ITEM_NAME = "ITEM_NAME"; + public static final String KEY_LIST_ITEM_ID = "LIST_ITEM_ID"; + public static final String KEY_PROVIDER = "PROVIDER"; + public static final String KEY_ENCODED_EMAIL = "ENCODED_EMAIL"; + public static final String KEY_LIST_OWNER = "LIST_OWNER"; + public static final String KEY_GOOGLE_EMAIL = "GOOGLE_EMAIL"; + public static final String KEY_PREF_SORT_ORDER_LISTS = "PERF_SORT_ORDER_LISTS"; + public static final String KEY_SHARED_WITH_USERS = "SHARED_WITH_USERS"; + + + /** + * Constants for Firebase login + */ + public static final String PASSWORD_PROVIDER = "password"; + public static final String GOOGLE_PROVIDER = "google"; + public static final String PROVIDER_DATA_DISPLAY_NAME = "displayName"; + + + /** + * Constant for sorting + */ + public static final String ORDER_BY_KEY = "orderByPushKey"; + public static final String ORDER_BY_OWNER_EMAIL = "orderByOwnerEmail"; + } diff --git a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/utils/Utils.java b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/utils/Utils.java index a98ce1b..aac5969 100755 --- a/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/utils/Utils.java +++ b/app/src/main/java/com/udacity/firebase/shoppinglistplusplus/utils/Utils.java @@ -1,8 +1,19 @@ package com.udacity.firebase.shoppinglistplusplus.utils; import android.content.Context; +import android.util.Log; + +import com.firebase.client.DataSnapshot; +import com.firebase.client.Firebase; +import com.firebase.client.FirebaseError; +import com.firebase.client.ServerValue; +import com.firebase.client.ValueEventListener; +import com.udacity.firebase.shoppinglistplusplus.model.ShoppingList; +import com.udacity.firebase.shoppinglistplusplus.model.User; import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; /** * Utility class @@ -22,4 +33,139 @@ public Utils(Context con) { mContext = con; } + /** + * Return true if currentUserEmail equals to shoppingList.owner() + * Return false otherwise + */ + public static boolean checkIfOwner(ShoppingList shoppingList, String currentUserEmail) { + return (shoppingList.getOwner() != null && + shoppingList.getOwner().equals(currentUserEmail)); + } + + /** + * Encode user email to use it as a Firebase key (Firebase does not allow "." in the key name) + * Encoded email is also used as "userEmail", list and item "owner" value + */ + public static String encodeEmail(String userEmail) { + return userEmail.replace(".", ","); + } + + /** + * Email is being decoded just once to display real email in AutocompleteFriendAdapter + * + * @see com.udacity.firebase.shoppinglistplusplus.ui.sharing.AutocompleteFriendAdapter + */ + public static String decodeEmail(String userEmail) { + return userEmail.replace(",", "."); + } + + /** + * Adds values to a pre-existing HashMap for updating a property for all of the ShoppingList copies. + * The HashMap can then be used with {@link Firebase#updateChildren(Map)} to update the property + * for all ShoppingList copies. + * + * @param sharedWith The list of users the shopping list that has been updated is shared with. + * @param listId The id of the shopping list. + * @param owner The owner of the shopping list. + * @param mapToUpdate The map containing the key, value pairs which will be used + * to update the Firebase database. This MUST be a Hashmap of key + * value pairs who's urls are absolute (i.e. from the root node) + * @param propertyToUpdate The property to update + * @param valueToUpdate The value to update + * @return The updated HashMap with the new value inserted in all lists + */ + public static HashMap updateMapForAllWithValue + (final HashMap sharedWith, final String listId, + final String owner, HashMap mapToUpdate, + String propertyToUpdate, Object valueToUpdate) { + + mapToUpdate.put("/" + Constants.FIREBASE_LOCATION_USER_LISTS + "/" + owner + "/" + + listId + "/" + propertyToUpdate, valueToUpdate); + if (sharedWith != null) { + for (User user : sharedWith.values()) { + mapToUpdate.put("/" + Constants.FIREBASE_LOCATION_USER_LISTS + "/" + user.getEmail() + "/" + + listId + "/" + propertyToUpdate, valueToUpdate); + } + } + + return mapToUpdate; + } + + /** + * Adds values to a pre-existing HashMap for updating all Last Changed Timestamps for all of + * the ShoppingList copies. This method uses {@link #updateMapForAllWithValue} to update the + * last changed timestamp for all ShoppingList copies. + * + * @param sharedWith The list of users the shopping list that has been updated is shared with. + * @param listId The id of the shopping list. + * @param owner The owner of the shopping list. + * @param mapToAddDateToUpdate The map containing the key, value pairs which will be used + * to update the Firebase database. This MUST be a Hashmap of key + * value pairs who's urls are absolute (i.e. from the root node) + * @return + */ + public static HashMap updateMapWithTimestampLastChanged + (final HashMap sharedWith, final String listId, + final String owner, HashMap mapToAddDateToUpdate) { + /** + * Set raw version of date to the ServerValue.TIMESTAMP value and save into dateCreatedMap + */ + HashMap timestampNowHash = new HashMap<>(); + timestampNowHash.put(Constants.FIREBASE_PROPERTY_TIMESTAMP, ServerValue.TIMESTAMP); + + updateMapForAllWithValue(sharedWith, listId, owner, mapToAddDateToUpdate, + Constants.FIREBASE_PROPERTY_TIMESTAMP_LAST_CHANGED, timestampNowHash); + + return mapToAddDateToUpdate; + } + /** + * Once an update is made to a ShoppingList, this method is responsible for updating the + * reversed timestamp to be equal to the negation of the current timestamp. This comes after + * the updateMapWithTimestampChanged because ServerValue.TIMESTAMP must be resolved to a long + * value. + * + * @param firebaseError The Firebase error, if there was one, from the original update. This + * method should only run if the shopping list's timestamp last changed + * was successfully updated. + * @param logTagFromActivity The log tag from the activity calling this method + * @param listId The updated shopping list push ID + * @param sharedWith The list of users that this updated shopping list is shared with + * @param owner The owner of the updated shopping list + */ + public static void updateTimestampReversed(FirebaseError firebaseError, final String logTagFromActivity, + final String listId, final HashMap sharedWith, + final String owner) { + if (firebaseError != null) { + Log.d(logTagFromActivity, "Error updating timestamp: " + firebaseError.getMessage()); + } else { + final Firebase firebaseRef = new Firebase(Constants.FIREBASE_URL); + firebaseRef.child(Constants.FIREBASE_LOCATION_USER_LISTS).child(owner) + .child(listId).addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + + ShoppingList list = dataSnapshot.getValue(ShoppingList.class); + if (list != null) { + long timeReverse = -(list.getTimestampLastChangedLong()); + String timeReverseLocation = Constants.FIREBASE_PROPERTY_TIMESTAMP_LAST_CHANGED_REVERSE + + "/" + Constants.FIREBASE_PROPERTY_TIMESTAMP; + + /** + * Create map and fill it in with deep path multi write operations list + */ + HashMap updatedShoppingListData = new HashMap(); + + updateMapForAllWithValue(sharedWith, listId, owner, updatedShoppingListData, + timeReverseLocation, timeReverse); + firebaseRef.updateChildren(updatedShoppingListData); + } + } + + @Override + public void onCancelled(FirebaseError firebaseError) { + Log.d(logTagFromActivity, "Error updating data: " + firebaseError.getMessage()); + } + }); + } + } } diff --git a/app/src/main/res/layout-land/activity_create_account.xml b/app/src/main/res/layout-land/activity_create_account.xml new file mode 100644 index 0000000..52a6b80 --- /dev/null +++ b/app/src/main/res/layout-land/activity_create_account.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + +