From 62b42ea41217f90d994b5ee873ae8f47a41b3ac8 Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Thu, 23 Feb 2017 09:47:13 -0800 Subject: [PATCH 01/86] Update constants.gradle --- constants.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.gradle b/constants.gradle index 30a47fd71..bc09f37ef 100644 --- a/constants.gradle +++ b/constants.gradle @@ -11,5 +11,5 @@ project.ext { buildTools = '25.0.2' firebaseVersion = '10.2.0' - supportLibraryVersion = '25.1.1' + supportLibraryVersion = '25.2.0' } From 9d09ce873975b60983fd1e2b4faa4396b593ec9b Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Fri, 24 Feb 2017 09:05:22 -0800 Subject: [PATCH 02/86] Bug fixes and perf improvements for setting providers in AuthUI (#591) --- .../java/com/firebase/ui/auth/AuthUI.java | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java index 566c510c4..7db44dddf 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java +++ b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java @@ -55,7 +55,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -356,11 +355,33 @@ public void writeToParcel(Parcel parcel, int i) { parcel.writeStringList(mScopes); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IdpConfig config = (IdpConfig) o; + + return mProviderId.equals(config.mProviderId); + } + + @Override + public int hashCode() { + return mProviderId.hashCode(); + } + + @Override + public String toString() { + return "IdpConfig{" + + "mProviderId='" + mProviderId + '\'' + + ", mScopes=" + mScopes + + '}'; + } + public static class Builder { private String mProviderId; private List mScopes = new ArrayList<>(); - /** * Builds the configuration parameters for an identity provider. * @@ -406,13 +427,12 @@ public IdpConfig build() { public final class SignInIntentBuilder { private int mLogo = NO_LOGO; private int mTheme = getDefaultTheme(); - private LinkedHashSet mProviders = new LinkedHashSet<>(); + private List mProviders = new ArrayList<>(); private String mTosUrl; private boolean mIsSmartLockEnabled = true; private boolean mAllowNewEmailAccounts = true; private SignInIntentBuilder() { - mProviders.add(new IdpConfig.Builder(EMAIL_PROVIDER).build()); } /** @@ -458,15 +478,14 @@ public SignInIntentBuilder setTosUrl(@Nullable String tosUrl) { */ public SignInIntentBuilder setProviders(@NonNull List idpConfigs) { mProviders.clear(); - Set configuredProviders = new HashSet<>(); - for (IdpConfig idpConfig : idpConfigs) { - if (configuredProviders.contains(idpConfig.getProviderId())) { + for (IdpConfig config : idpConfigs) { + if (mProviders.contains(config)) { throw new IllegalArgumentException("Each provider can only be set once. " - + idpConfig.getProviderId() + + config.getProviderId() + " was set twice."); + } else { + mProviders.add(config); } - configuredProviders.add(idpConfig.getProviderId()); - mProviders.add(idpConfig); } return this; } @@ -495,6 +514,15 @@ public SignInIntentBuilder setProviders(@NonNull String... providers) { return this; } + private boolean isIdpAlreadyConfigured(@NonNull String providerId) { + for (IdpConfig config : mProviders) { + if (config.getProviderId().equals(providerId)) { + return true; + } + } + return false; + } + /** * Enables or disables the use of Smart Lock for Passwords in the sign in flow. *

@@ -515,23 +543,18 @@ public SignInIntentBuilder setAllowNewEmailAccounts(boolean enabled) { return this; } - private boolean isIdpAlreadyConfigured(@NonNull String providerId) { - for (IdpConfig config : mProviders) { - if (config.getProviderId().equals(providerId)) { - return true; - } - } - return false; - } - public Intent build() { return KickoffActivity.createIntent(mApp.getApplicationContext(), getFlowParams()); } @VisibleForTesting() public FlowParameters getFlowParams() { + if (mProviders.isEmpty()) { + mProviders.add(new IdpConfig.Builder(EMAIL_PROVIDER).build()); + } + return new FlowParameters(mApp.getName(), - new ArrayList<>(mProviders), + mProviders, mTheme, mLogo, mTosUrl, From a885a3471a663883ab704e64718b57bf3048a1dc Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Wed, 1 Mar 2017 18:03:45 -0800 Subject: [PATCH 03/86] Update dependencies Signed-off-by: Alex Saveau --- auth/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/build.gradle b/auth/build.gradle index 3d7b2149c..8d68afe08 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -44,8 +44,8 @@ dependencies { compile "com.google.firebase:firebase-auth:$firebaseVersion" compile "com.google.android.gms:play-services-auth:$firebaseVersion" - compile 'com.facebook.android:facebook-android-sdk:4.19.0' - compile("com.twitter.sdk.android:twitter:2.3.0@aar") { transitive = true } + compile 'com.facebook.android:facebook-android-sdk:4.20.0' + compile("com.twitter.sdk.android:twitter:2.3.1@aar") { transitive = true } // The following libraries are needed to prevent incompatibilities with the facebook // library when updating com.android.support libraries: @@ -53,7 +53,7 @@ dependencies { testCompile 'junit:junit:4.12' //noinspection NewerVersionAvailable, GradleDynamicVersion - testCompile 'org.mockito:mockito-core:2.6.+' + testCompile 'org.mockito:mockito-core:2.7.+' testCompile 'org.robolectric:robolectric:3.2.2' // See https://github.com/robolectric/robolectric/issues/1932#issuecomment-219796474 testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1' From ac5c8493bda870f1ca15788f63e210cd09de45e7 Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Sun, 5 Mar 2017 19:26:08 -0800 Subject: [PATCH 04/86] The Big Kahuna of firebase-ui-database (#544) --- .../uidemo/database/ChatActivity.java | 2 +- .../java/com/firebase/ui/database/Bean.java | 12 + .../database/FirebaseArrayOfObjectsTest.java | 60 +++-- .../ui/database/FirebaseArrayTest.java | 19 +- .../FirebaseIndexArrayOfObjectsTest.java | 29 ++- .../ui/database/FirebaseIndexArrayTest.java | 19 +- .../com/firebase/ui/database/TestUtils.java | 27 +- .../CachingObservableSnapshotArray.java | 64 +++++ .../ui/database/ChangeEventListener.java | 21 +- .../ui/database/ClassSnapshotParser.java | 24 ++ .../firebase/ui/database/FirebaseAdapter.java | 23 ++ .../firebase/ui/database/FirebaseArray.java | 141 +++++++---- .../ui/database/FirebaseIndexArray.java | 210 +++++++++++----- .../ui/database/FirebaseIndexListAdapter.java | 70 ++---- .../FirebaseIndexRecyclerAdapter.java | 99 +++----- .../ui/database/FirebaseListAdapter.java | 178 ++++++------- .../ui/database/FirebaseRecyclerAdapter.java | 237 +++++++----------- .../firebase/ui/database/ImmutableList.java | 202 +++++++++++++++ .../ui/database/ObservableSnapshotArray.java | 202 +++++++++++++++ .../firebase/ui/database/Preconditions.java | 14 ++ .../firebase/ui/database/SnapshotParser.java | 13 + 21 files changed, 1115 insertions(+), 551 deletions(-) create mode 100644 database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java create mode 100644 database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java create mode 100644 database/src/main/java/com/firebase/ui/database/FirebaseAdapter.java create mode 100644 database/src/main/java/com/firebase/ui/database/ImmutableList.java create mode 100644 database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java create mode 100644 database/src/main/java/com/firebase/ui/database/Preconditions.java create mode 100644 database/src/main/java/com/firebase/ui/database/SnapshotParser.java diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java index 96426eb71..ac3e97359 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java @@ -148,7 +148,7 @@ public void populateViewHolder(ChatHolder holder, Chat chat, int position) { } @Override - protected void onDataChanged() { + public void onDataChanged() { // If there are no chat messages, show a view that invites the user to add a message. mEmptyListMessage.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); } diff --git a/database/src/androidTest/java/com/firebase/ui/database/Bean.java b/database/src/androidTest/java/com/firebase/ui/database/Bean.java index faf5ccbcb..600497f0b 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/Bean.java +++ b/database/src/androidTest/java/com/firebase/ui/database/Bean.java @@ -30,4 +30,16 @@ public String getText() { public boolean isBool() { return mBool; } + + public void setNumber(int number) { + mNumber = number; + } + + public void setText(String text) { + mText = text; + } + + public void setBool(boolean bool) { + mBool = bool; + } } diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java index 40c521986..4d8c9ea4f 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java @@ -29,7 +29,6 @@ import java.util.concurrent.Callable; import static com.firebase.ui.database.TestUtils.getAppInstance; -import static com.firebase.ui.database.TestUtils.getBean; import static com.firebase.ui.database.TestUtils.runAndWaitUntil; @RunWith(AndroidJUnit4.class) @@ -37,7 +36,8 @@ public class FirebaseArrayOfObjectsTest { private static final int INITIAL_SIZE = 3; private DatabaseReference mRef; - private FirebaseArray mArray; + private FirebaseArray mArray; + private ChangeEventListener mListener; @Before public void setUp() throws Exception { @@ -46,9 +46,9 @@ public void setUp() throws Exception { .getReference() .child("firebasearray") .child("objects"); - mArray = new FirebaseArray(mRef); + mArray = new FirebaseArray<>(mRef, Bean.class); mRef.removeValue(); - runAndWaitUntil(mArray, new Runnable() { + mListener = runAndWaitUntil(mArray, new Runnable() { @Override public void run() { for (int i = 1; i <= INITIAL_SIZE; i++) { @@ -58,14 +58,14 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == INITIAL_SIZE; + return mArray.size() == INITIAL_SIZE; } }); } @After public void tearDown() throws Exception { - mArray.cleanup(); + mArray.removeChangeEventListener(mListener); mRef.getRoot().removeValue(); } @@ -79,7 +79,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == 4; + return mArray.size() == 4; } }); } @@ -94,7 +94,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Bean.class).getNumber() == 4; + return mArray.getObject(3).getNumber() == 4; } }); } @@ -109,9 +109,8 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Bean.class).getNumber() == 3 && mArray.getItem(0) - .getValue(Bean.class) - .getNumber() == 4; + return mArray.getObject(3).getNumber() == 3 + && mArray.getObject(0).getNumber() == 4; } }); } @@ -121,16 +120,47 @@ public void testChangePriorities() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mArray.getItem(2).getRef().setPriority(0.5); + mArray.get(2).getRef().setPriority(0.5); } }, new Callable() { @Override public Boolean call() throws Exception { - return getBean(mArray, 0).getNumber() == 3 - && getBean(mArray, 1).getNumber() == 1 - && getBean(mArray, 2).getNumber() == 2; + return mArray.getObject(0).getNumber() == 3 + && mArray.getObject(1).getNumber() == 1 + && mArray.getObject(2).getNumber() == 2; //return isValuesEqual(mArray, new int[]{3, 1, 2}); } }); } + + @Test + public void testCacheInvalidates() throws Exception { + final DatabaseReference pushRef = mRef.push(); + + // Set initial value to "5" + runAndWaitUntil(mArray, new Runnable() { + @Override + public void run() { + pushRef.setValue(new Bean(5), 100); + } + }, new Callable() { + @Override + public Boolean call() throws Exception { + return mArray.getObject(3).getNumber() == 5; + } + }); + + // Change the value to "6" and ensure that the change is propagated + runAndWaitUntil(mArray, new Runnable() { + @Override + public void run() { + pushRef.setValue(new Bean(6), 100); + } + }, new Callable() { + @Override + public Boolean call() throws Exception { + return mArray.getObject(3).getNumber() == 6; + } + }); + } } diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java index 2b7c4bf93..adfe0a843 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java @@ -37,6 +37,7 @@ public class FirebaseArrayTest { private static final int INITIAL_SIZE = 3; private DatabaseReference mRef; private FirebaseArray mArray; + private ChangeEventListener mListener; @Before public void setUp() throws Exception { @@ -44,7 +45,7 @@ public void setUp() throws Exception { mRef = FirebaseDatabase.getInstance(app).getReference().child("firebasearray"); mArray = new FirebaseArray(mRef); mRef.removeValue(); - runAndWaitUntil(mArray, new Runnable() { + mListener = runAndWaitUntil(mArray, new Runnable() { @Override public void run() { for (int i = 1; i <= INITIAL_SIZE; i++) { @@ -54,14 +55,14 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == INITIAL_SIZE; + return mArray.size() == INITIAL_SIZE; } }); } @After public void tearDown() throws Exception { - mArray.cleanup(); + mArray.removeChangeEventListener(mListener); mRef.getRoot().removeValue(); } @@ -75,7 +76,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == 4; + return mArray.size() == 4; } }); } @@ -90,7 +91,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Integer.class).equals(4); + return mArray.get(3).getValue(Integer.class).equals(4); } }); } @@ -105,8 +106,8 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Integer.class).equals(3) - && mArray.getItem(0).getValue(Integer.class).equals(4); + return mArray.get(3).getValue(Integer.class).equals(3) + && mArray.get(0).getValue(Integer.class).equals(4); } }); } @@ -116,7 +117,7 @@ public void testChangePriorityBackToFront() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mArray.getItem(2).getRef().setPriority(0.5); + mArray.get(2).getRef().setPriority(0.5); } }, new Callable() { @Override @@ -131,7 +132,7 @@ public void testChangePriorityFrontToBack() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mArray.getItem(0).getRef().setPriority(4); + mArray.get(0).getRef().setPriority(4); } }, new Callable() { @Override diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java index 959582a74..3377d5c56 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java @@ -28,7 +28,6 @@ import java.util.concurrent.Callable; import static com.firebase.ui.database.TestUtils.getAppInstance; -import static com.firebase.ui.database.TestUtils.getBean; import static com.firebase.ui.database.TestUtils.runAndWaitUntil; @RunWith(AndroidJUnit4.class) @@ -37,7 +36,8 @@ public class FirebaseIndexArrayOfObjectsTest { private DatabaseReference mRef; private DatabaseReference mKeyRef; - private FirebaseArray mArray; + private ObservableSnapshotArray mArray; + private ChangeEventListener mListener; @Before public void setUp() throws Exception { @@ -46,11 +46,11 @@ public void setUp() throws Exception { mRef = databaseInstance.getReference().child("firebasearray").child("objects"); mKeyRef = databaseInstance.getReference().child("firebaseindexarray").child("objects"); - mArray = new FirebaseIndexArray(mKeyRef, mRef); + mArray = new FirebaseIndexArray<>(mKeyRef, mRef, Bean.class); mRef.removeValue(); mKeyRef.removeValue(); - runAndWaitUntil(mArray, new Runnable() { + mListener = runAndWaitUntil(mArray, new Runnable() { @Override public void run() { for (int i = 1; i <= INITIAL_SIZE; i++) { @@ -60,14 +60,14 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == INITIAL_SIZE; + return mArray.size() == INITIAL_SIZE; } }); } @After public void tearDown() throws Exception { - mArray.cleanup(); + mArray.removeChangeEventListener(mListener); mRef.getRoot().removeValue(); } @@ -81,7 +81,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == 4; + return mArray.size() == 4; } }); } @@ -96,7 +96,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Bean.class).getNumber() == 4; + return mArray.getObject(3).getNumber() == 4; } }); } @@ -111,9 +111,8 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Bean.class).getNumber() == 3 && mArray.getItem(0) - .getValue(Bean.class) - .getNumber() == 4; + return mArray.getObject(3).getNumber() == 3 + && mArray.getObject(0).getNumber() == 4; } }); } @@ -123,14 +122,14 @@ public void testChangePriorities() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mKeyRef.child(mArray.getItem(2).getKey()).setPriority(0.5); + mKeyRef.child(mArray.get(2).getKey()).setPriority(0.5); } }, new Callable() { @Override public Boolean call() throws Exception { - return getBean(mArray, 0).getNumber() == 3 - && getBean(mArray, 1).getNumber() == 1 - && getBean(mArray, 2).getNumber() == 2; + return mArray.getObject(0).getNumber() == 3 + && mArray.getObject(1).getNumber() == 1 + && mArray.getObject(2).getNumber() == 2; //return isValuesEqual(mArray, new int[]{3, 1, 2}); } }); diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java index e0b884b3a..493c00b3e 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java @@ -37,7 +37,8 @@ public class FirebaseIndexArrayTest { private DatabaseReference mRef; private DatabaseReference mKeyRef; - private FirebaseIndexArray mArray; + private ObservableSnapshotArray mArray; + private ChangeEventListener mListener; @Before public void setUp() throws Exception { @@ -50,7 +51,7 @@ public void setUp() throws Exception { mRef.removeValue(); mKeyRef.removeValue(); - runAndWaitUntil(mArray, new Runnable() { + mListener = runAndWaitUntil(mArray, new Runnable() { @Override public void run() { for (int i = 1; i <= INITIAL_SIZE; i++) { @@ -60,14 +61,14 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == INITIAL_SIZE; + return mArray.size() == INITIAL_SIZE; } }); } @After public void tearDown() throws Exception { - mArray.cleanup(); + mArray.removeChangeEventListener(mListener); mRef.getRoot().removeValue(); } @@ -81,7 +82,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getCount() == 4; + return mArray.size() == 4; } }); } @@ -96,7 +97,7 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Integer.class).equals(4); + return mArray.get(3).getValue(Integer.class).equals(4); } }); } @@ -111,8 +112,8 @@ public void run() { }, new Callable() { @Override public Boolean call() throws Exception { - return mArray.getItem(3).getValue(Integer.class).equals(3) - && mArray.getItem(0).getValue(Integer.class).equals(4); + return mArray.get(3).getValue(Integer.class).equals(3) + && mArray.get(0).getValue(Integer.class).equals(4); } }); } @@ -122,7 +123,7 @@ public void testChangePriorities() throws Exception { runAndWaitUntil(mArray, new Runnable() { @Override public void run() { - mKeyRef.child(mArray.getItem(2).getKey()).setPriority(0.5); + mKeyRef.child(mArray.get(2).getKey()).setPriority(0.5); } }, new Callable() { @Override diff --git a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java index aa9151fc3..88dc0b693 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java +++ b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java @@ -4,6 +4,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; @@ -33,14 +34,14 @@ private static FirebaseApp initializeApp(Context context) { .build(), APP_NAME); } - public static void runAndWaitUntil(FirebaseArray array, - Runnable task, - Callable done) throws InterruptedException { + public static ChangeEventListener runAndWaitUntil(ObservableSnapshotArray array, + Runnable task, + Callable done) throws InterruptedException { final Semaphore semaphore = new Semaphore(0); - - array.setOnChangedListener(new ChangeEventListener() { + ChangeEventListener listener = array.addChangeEventListener(new ChangeEventListener() { @Override public void onChildChanged(ChangeEventListener.EventType type, + DataSnapshot snapshot, int index, int oldIndex) { semaphore.release(); @@ -56,6 +57,7 @@ public void onCancelled(DatabaseError error) { } }); task.run(); + boolean isDone = false; long startedAt = System.currentTimeMillis(); while (!isDone && System.currentTimeMillis() - startedAt < TIMEOUT) { @@ -68,23 +70,20 @@ public void onCancelled(DatabaseError error) { } } assertTrue("Timed out waiting for expected results on FirebaseArray", isDone); - array.setOnChangedListener(null); + + return listener; } - public static boolean isValuesEqual(FirebaseArray array, int[] expected) { - if (array.getCount() != expected.length) return false; - for (int i = 0; i < array.getCount(); i++) { - if (!array.getItem(i).getValue(Integer.class).equals(expected[i])) { + public static boolean isValuesEqual(ObservableSnapshotArray array, int[] expected) { + if (array.size() != expected.length) return false; + for (int i = 0; i < array.size(); i++) { + if (!array.get(i).getValue(Integer.class).equals(expected[i])) { return false; } } return true; } - public static Bean getBean(FirebaseArray array, int index) { - return array.getItem(index).getValue(Bean.class); - } - public static void pushValue(DatabaseReference keyRef, DatabaseReference ref, Object value, diff --git a/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java new file mode 100644 index 000000000..1a564f65e --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java @@ -0,0 +1,64 @@ +package com.firebase.ui.database; + +import android.support.annotation.NonNull; + +import com.google.firebase.database.DataSnapshot; + +import java.util.HashMap; +import java.util.Map; + +/** + * An extension of {@link ObservableSnapshotArray} that caches the result of {@link #getObject(int)} + * so that repeated calls for the same key are not expensive (unless the underlying snapshot has + * changed). + */ +public abstract class CachingObservableSnapshotArray extends ObservableSnapshotArray { + private Map mObjectCache = new HashMap<>(); + + /** + * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) + */ + public CachingObservableSnapshotArray(@NonNull Class tClass) { + super(tClass); + } + + /** + * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) + */ + public CachingObservableSnapshotArray(@NonNull SnapshotParser parser) { + super(parser); + } + + @Override + public T getObject(int index) { + String key = get(index).getKey(); + + // Return from the cache if possible, otherwise populate the cache and return + if (mObjectCache.containsKey(key)) { + return mObjectCache.get(key); + } else { + T object = super.getObject(index); + mObjectCache.put(key, object); + return object; + } + } + + protected void clearData() { + getSnapshots().clear(); + mObjectCache.clear(); + } + + protected DataSnapshot removeData(int index) { + DataSnapshot snapshot = getSnapshots().remove(index); + if (snapshot != null) { + mObjectCache.remove(snapshot.getKey()); + } + + return snapshot; + } + + protected void updateData(int index, DataSnapshot snapshot) { + getSnapshots().set(index, snapshot); + mObjectCache.remove(snapshot.getKey()); + } +} diff --git a/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java b/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java index a8028dc47..626773d67 100644 --- a/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java +++ b/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java @@ -39,19 +39,22 @@ enum EventType { * A callback for when a child has changed in FirebaseArray. * * @param type The type of event received + * @param snapshot the {@link DataSnapshot} of the changed child. * @param index The index at which the change occurred - * @param oldIndex If {@code type} is a moved event, the previous index of the moved child. - * For any other event, {@code oldIndex} will be -1. + * @param oldIndex If {@code type} is a moved event, the previous index of the moved child. For + * any other event, {@code oldIndex} will be -1. */ - void onChildChanged(EventType type, int index, int oldIndex); + void onChildChanged(EventType type, DataSnapshot snapshot, int index, int oldIndex); - /** This method will be triggered each time updates from the database have been completely processed. - * So the first time this method is called, the initial data has been loaded - including the case - * when no data at all is available. Each next time the method is called, a complete update (potentially - * consisting of updates to multiple child items) has been completed. + /** + * This method will be triggered each time updates from the database have been completely + * processed. So the first time this method is called, the initial data has been loaded - + * including the case when no data at all is available. Each next time the method is called, a + * complete update (potentially consisting of updates to multiple child items) has been + * completed. *

- * You would typically override this method to hide a loading indicator (after the initial load) or - * to complete a batch update to a UI element. + * You would typically override this method to hide a loading indicator (after the initial load) + * or to complete a batch update to a UI element. */ void onDataChanged(); diff --git a/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java b/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java new file mode 100644 index 000000000..215a78972 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java @@ -0,0 +1,24 @@ +package com.firebase.ui.database; + +import android.support.annotation.NonNull; + +import com.google.firebase.database.DataSnapshot; + +/** + * A convenience implementation of {@link SnapshotParser} that converts a {@link DataSnapshot} to + * the parametrized class via {@link DataSnapshot#getValue(Class)}. + * + * @param the POJO class to create from snapshots. + */ +public class ClassSnapshotParser implements SnapshotParser { + private Class mClass; + + public ClassSnapshotParser(@NonNull Class clazz) { + mClass = Preconditions.checkNotNull(clazz); + } + + @Override + public T parseSnapshot(DataSnapshot snapshot) { + return snapshot.getValue(mClass); + } +} diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseAdapter.java new file mode 100644 index 000000000..977958850 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/FirebaseAdapter.java @@ -0,0 +1,23 @@ +package com.firebase.ui.database; + +import android.support.annotation.RestrictTo; + +import com.google.firebase.database.DatabaseReference; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface FirebaseAdapter extends ChangeEventListener { + /** + * If you need to do some setup before the adapter starts listening for change events in the + * database, do so it here and then call {@code super.startListening()}. + */ + void startListening(); + + /** + * Removes listeners and clears all items in the backing {@link FirebaseArray}. + */ + void cleanup(); + + T getItem(int position); + + DatabaseReference getRef(int position); +} diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index 68b950c14..e26620372 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -14,6 +14,8 @@ package com.firebase.ui.database; +import android.support.annotation.NonNull; + import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -24,42 +26,70 @@ import java.util.List; /** - * This class implements an array-like collection on top of a Firebase location. + * This class implements a collection on top of a Firebase location. */ -class FirebaseArray implements ChildEventListener, ValueEventListener { +public class FirebaseArray extends CachingObservableSnapshotArray implements ChildEventListener, ValueEventListener { private Query mQuery; - private ChangeEventListener mListener; private List mSnapshots = new ArrayList<>(); - public FirebaseArray(Query ref) { - mQuery = ref; - mQuery.addChildEventListener(this); - mQuery.addValueEventListener(this); + /** + * Create a new FirebaseArray that parses snapshots as members of a given class. + * + * @param query The Firebase location to watch for data changes. Can also be a slice of a + * location, using some combination of {@code limit()}, {@code startAt()}, and + * {@code endAt()}. + * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) + */ + public FirebaseArray(Query query, Class tClass) { + super(tClass); + init(query); } - public void cleanup() { - mQuery.removeEventListener((ValueEventListener) this); - mQuery.removeEventListener((ChildEventListener) this); + /** + * Create a new FirebaseArray with a custom {@link SnapshotParser}. + * + * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) + * @see FirebaseArray#FirebaseArray(Query, Class) + */ + public FirebaseArray(Query query, SnapshotParser parser) { + super(parser); + init(query); } - public int getCount() { - return mSnapshots.size(); + private void init(Query query) { + mQuery = query; } - public DataSnapshot getItem(int index) { - return mSnapshots.get(index); + @Override + protected List getSnapshots() { + return mSnapshots; } - private int getIndexForKey(String key) { - int index = 0; - for (DataSnapshot snapshot : mSnapshots) { - if (snapshot.getKey().equals(key)) { - return index; - } else { - index++; - } + @Override + public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { + boolean wasListening = isListening(); + super.addChangeEventListener(listener); + + // Only start listening when the first listener is added + if (!wasListening) { + mQuery.addChildEventListener(this); + mQuery.addValueEventListener(this); + } + + return listener; + } + + @Override + public void removeChangeEventListener(@NonNull ChangeEventListener listener) { + super.removeChangeEventListener(listener); + + // Clear data when all listeners are removed + if (!isListening()) { + mQuery.removeEventListener((ValueEventListener) this); + mQuery.removeEventListener((ChildEventListener) this); + + clearData(); } - throw new IllegalArgumentException("Key not found"); } @Override @@ -68,60 +98,87 @@ public void onChildAdded(DataSnapshot snapshot, String previousChildKey) { if (previousChildKey != null) { index = getIndexForKey(previousChildKey) + 1; } + mSnapshots.add(index, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.ADDED, index); + + notifyChangeEventListeners(ChangeEventListener.EventType.ADDED, snapshot, index); } @Override public void onChildChanged(DataSnapshot snapshot, String previousChildKey) { int index = getIndexForKey(snapshot.getKey()); - mSnapshots.set(index, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.CHANGED, index); + + updateData(index, snapshot); + notifyChangeEventListeners(ChangeEventListener.EventType.CHANGED, snapshot, index); } @Override public void onChildRemoved(DataSnapshot snapshot) { int index = getIndexForKey(snapshot.getKey()); - mSnapshots.remove(index); - notifyChangedListeners(ChangeEventListener.EventType.REMOVED, index); + + removeData(index); + notifyChangeEventListeners(ChangeEventListener.EventType.REMOVED, snapshot, index); } @Override public void onChildMoved(DataSnapshot snapshot, String previousChildKey) { int oldIndex = getIndexForKey(snapshot.getKey()); mSnapshots.remove(oldIndex); + int newIndex = previousChildKey == null ? 0 : (getIndexForKey(previousChildKey) + 1); mSnapshots.add(newIndex, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.MOVED, newIndex, oldIndex); + + notifyChangeEventListeners(ChangeEventListener.EventType.MOVED, + snapshot, + newIndex, + oldIndex); } @Override public void onDataChange(DataSnapshot dataSnapshot) { - mListener.onDataChanged(); + notifyListenersOnDataChanged(); } @Override public void onCancelled(DatabaseError error) { - notifyCancelledListeners(error); + notifyListenersOnCancelled(error); } - public void setOnChangedListener(ChangeEventListener listener) { - mListener = listener; + private int getIndexForKey(String key) { + int index = 0; + for (DataSnapshot snapshot : mSnapshots) { + if (snapshot.getKey().equals(key)) { + return index; + } else { + index++; + } + } + throw new IllegalArgumentException("Key not found"); } - protected void notifyChangedListeners(ChangeEventListener.EventType type, int index) { - notifyChangedListeners(type, index, -1); + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + FirebaseArray snapshots = (FirebaseArray) obj; + + return mQuery.equals(snapshots.mQuery) && mSnapshots.equals(snapshots.mSnapshots); } - protected void notifyChangedListeners(ChangeEventListener.EventType type, int index, int oldIndex) { - if (mListener != null) { - mListener.onChildChanged(type, index, oldIndex); - } + @Override + public int hashCode() { + int result = mQuery.hashCode(); + result = 31 * result + mSnapshots.hashCode(); + return result; } - protected void notifyCancelledListeners(DatabaseError error) { - if (mListener != null) { - mListener.onCancelled(error); + @Override + public String toString() { + if (isListening()) { + return "FirebaseArray is listening at " + mQuery + ":\n" + mSnapshots; + } else { + return "FirebaseArray is inactive"; } } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java index 37077dff5..e7b700cf2 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java @@ -14,57 +14,125 @@ package com.firebase.ui.database; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; import android.util.Log; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.Query; import com.google.firebase.database.ValueEventListener; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -class FirebaseIndexArray extends FirebaseArray { - private static final String TAG = FirebaseIndexArray.class.getSimpleName(); +public class FirebaseIndexArray extends CachingObservableSnapshotArray implements ChangeEventListener { + private static final String TAG = "FirebaseIndexArray"; - private Query mQuery; - private ChangeEventListener mListener; + private DatabaseReference mDataRef; private Map mRefs = new HashMap<>(); + + private FirebaseArray mKeySnapshots; private List mDataSnapshots = new ArrayList<>(); - public FirebaseIndexArray(Query keyRef, Query dataRef) { - super(keyRef); - mQuery = dataRef; + /** + * Create a new FirebaseIndexArray that parses snapshots as members of a given class. + * + * @param keyQuery The Firebase location containing the list of keys to be found in {@code + * dataRef}. Can also be a slice of a location, using some combination of {@code + * limit()}, {@code startAt()}, and {@code endAt()}. + * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code + * keyQuery}'s location represents a list item in the {@link RecyclerView}. + * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) + */ + public FirebaseIndexArray(Query keyQuery, DatabaseReference dataRef, Class tClass) { + super(tClass); + init(keyQuery, dataRef); + } + + /** + * Create a new FirebaseIndexArray with a custom {@link SnapshotParser}. + * + * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) + * @see FirebaseIndexArray#FirebaseIndexArray(Query, DatabaseReference, Class) + */ + public FirebaseIndexArray(Query keyQuery, DatabaseReference dataRef, SnapshotParser parser) { + super(parser); + init(keyQuery, dataRef); + } + + private void init(Query keyQuery, DatabaseReference dataRef) { + mDataRef = dataRef; + mKeySnapshots = new FirebaseArray<>(keyQuery, new SnapshotParser() { + @Override + public String parseSnapshot(DataSnapshot snapshot) { + return snapshot.getKey(); + } + }); + + mKeySnapshots.addChangeEventListener(this); } @Override - public void cleanup() { - super.cleanup(); - Set refs = new HashSet<>(mRefs.keySet()); - for (Query ref : refs) { - ref.removeEventListener(mRefs.remove(ref)); + public void onChildChanged(EventType type, DataSnapshot snapshot, int index, int oldIndex) { + switch (type) { + case ADDED: + onKeyAdded(snapshot); + break; + case MOVED: + onKeyMoved(snapshot, index, oldIndex); + break; + case CHANGED: + // This is a no-op, we don't care when a key 'changes' since that should not + // be a supported operation + break; + case REMOVED: + onKeyRemoved(snapshot, index); + break; } } @Override - public int getCount() { - return mDataSnapshots.size(); + public void onDataChanged() { + // No-op, we don't listen to batch events for the key ref } @Override - public DataSnapshot getItem(int index) { - return mDataSnapshots.get(index); + public void onCancelled(DatabaseError error) { + Log.e(TAG, "A fatal error occurred retrieving the necessary keys to populate your adapter."); + } + + @Override + public void removeChangeEventListener(@NonNull ChangeEventListener listener) { + super.removeChangeEventListener(listener); + if (!isListening()) { + for (Query query : mRefs.keySet()) { + query.removeEventListener(mRefs.get(query)); + } + + clearData(); + } + } + + @Override + protected List getSnapshots() { + return mDataSnapshots; + } + + @Override + protected void clearData() { + super.clearData(); + mRefs.clear(); } private int getIndexForKey(String key) { - int dataCount = getCount(); + int dataCount = size(); int index = 0; for (int keyIndex = 0; index < dataCount; keyIndex++) { - String superKey = super.getItem(keyIndex).getKey(); + String superKey = mKeySnapshots.getObject(keyIndex); if (key.equals(superKey)) { break; } else if (mDataSnapshots.get(index).getKey().equals(superKey)) { @@ -74,73 +142,75 @@ private int getIndexForKey(String key) { return index; } + /** + * Determines if a DataSnapshot with the given key is present at the given index. + */ private boolean isKeyAtIndex(String key, int index) { - return index >= 0 && index < getCount() && mDataSnapshots.get(index).getKey().equals(key); + return index >= 0 && index < size() && mDataSnapshots.get(index).getKey().equals(key); } - @Override - public void onChildAdded(DataSnapshot keySnapshot, String previousChildKey) { - super.setOnChangedListener(null); - super.onChildAdded(keySnapshot, previousChildKey); - super.setOnChangedListener(mListener); + protected void onKeyAdded(DataSnapshot data) { + Query ref = mDataRef.child(data.getKey()); - Query ref = mQuery.getRef().child(keySnapshot.getKey()); + // Start listening mRefs.put(ref, ref.addValueEventListener(new DataRefListener())); } - @Override - public void onChildChanged(DataSnapshot snapshot, String previousChildKey) { - super.setOnChangedListener(null); - super.onChildChanged(snapshot, previousChildKey); - super.setOnChangedListener(mListener); - } + protected void onKeyMoved(DataSnapshot data, int index, int oldIndex) { + String key = data.getKey(); - @Override - public void onChildRemoved(DataSnapshot keySnapshot) { - String key = keySnapshot.getKey(); - int index = getIndexForKey(key); - mQuery.getRef().child(key).removeEventListener(mRefs.remove(mQuery.getRef().child(key))); + if (isKeyAtIndex(key, oldIndex)) { + DataSnapshot snapshot = removeData(oldIndex); + mDataSnapshots.add(index, snapshot); + notifyChangeEventListeners(ChangeEventListener.EventType.MOVED, + snapshot, + index, + oldIndex); + } + } - super.setOnChangedListener(null); - super.onChildRemoved(keySnapshot); - super.setOnChangedListener(mListener); + protected void onKeyRemoved(DataSnapshot data, int index) { + String key = data.getKey(); + mDataRef.child(key).removeEventListener(mRefs.remove(mDataRef.getRef().child(key))); if (isKeyAtIndex(key, index)) { - mDataSnapshots.remove(index); - notifyChangedListeners(ChangeEventListener.EventType.REMOVED, index); + DataSnapshot snapshot = removeData(index); + notifyChangeEventListeners(ChangeEventListener.EventType.REMOVED, snapshot, index); } } @Override - public void onChildMoved(DataSnapshot keySnapshot, String previousChildKey) { - String key = keySnapshot.getKey(); - int oldIndex = getIndexForKey(key); + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + if (!super.equals(obj)) return false; - super.setOnChangedListener(null); - super.onChildMoved(keySnapshot, previousChildKey); - super.setOnChangedListener(mListener); + FirebaseIndexArray array = (FirebaseIndexArray) obj; - if (isKeyAtIndex(key, oldIndex)) { - DataSnapshot snapshot = mDataSnapshots.remove(oldIndex); - int newIndex = getIndexForKey(key); - mDataSnapshots.add(newIndex, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.MOVED, newIndex, oldIndex); - } + return mDataRef.equals(array.mDataRef) && mDataSnapshots.equals(array.mDataSnapshots); } @Override - public void onCancelled(DatabaseError error) { - Log.e(TAG, "A fatal error occurred retrieving the necessary keys to populate your adapter."); - super.onCancelled(error); + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + mDataRef.hashCode(); + result = 31 * result + mDataSnapshots.hashCode(); + return result; } @Override - public void setOnChangedListener(ChangeEventListener listener) { - super.setOnChangedListener(listener); - mListener = listener; + public String toString() { + if (isListening()) { + return "FirebaseIndexArray is listening at " + mDataRef + ":\n" + mDataSnapshots; + } else { + return "FirebaseIndexArray is inactive"; + } } - private class DataRefListener implements ValueEventListener { + /** + * A ValueEventListener attached to the joined child data. + */ + protected class DataRefListener implements ValueEventListener { @Override public void onDataChange(DataSnapshot snapshot) { String key = snapshot.getKey(); @@ -148,17 +218,21 @@ public void onDataChange(DataSnapshot snapshot) { if (snapshot.getValue() != null) { if (!isKeyAtIndex(key, index)) { + // We don't already know about this data, add it mDataSnapshots.add(index, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.ADDED, index); + notifyChangeEventListeners(ChangeEventListener.EventType.ADDED, snapshot, index); } else { - mDataSnapshots.set(index, snapshot); - notifyChangedListeners(ChangeEventListener.EventType.CHANGED, index); + // We already know about this data, just update it + updateData(index, snapshot); + notifyChangeEventListeners(ChangeEventListener.EventType.CHANGED, snapshot, index); } } else { if (isKeyAtIndex(key, index)) { - mDataSnapshots.remove(index); - notifyChangedListeners(ChangeEventListener.EventType.REMOVED, index); + // This data has disappeared, remove it + removeData(index); + notifyChangeEventListeners(ChangeEventListener.EventType.REMOVED, snapshot, index); } else { + // Data does not exist Log.w(TAG, "Key not found at ref: " + snapshot.getRef()); } } @@ -166,7 +240,7 @@ public void onDataChange(DataSnapshot snapshot) { @Override public void onCancelled(DatabaseError error) { - notifyCancelledListeners(error); + notifyListenersOnCancelled(error); } } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java index a5878d632..06799a8ba 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java @@ -2,58 +2,40 @@ import android.app.Activity; import android.support.annotation.LayoutRes; +import android.widget.ListView; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.Query; -/** - * This class is a generic way of backing an Android ListView with a Firebase location. - * It handles all of the child events at the given Firebase location. It marshals received data into the given - * class type. Extend this class and provide an implementation of {@code populateView}, which will be given an - * instance of your list item mLayout and an instance your class that holds your data. - * Simply populate the view however you like and this class will handle updating the list as the data changes. - *

- * If your data is not indexed: - *

- *     DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
- *     ListAdapter adapter = new FirebaseListAdapter(
- *             this,
- *             ChatMessage.class,
- *             android.R.layout.two_line_list_item,
- *             keyRef,
- *             dataRef)
- *     {
- *         protected void populateView(View view, ChatMessage chatMessage, int position)
- *         {
- *             ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName());
- *             ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getMessage());
- *         }
- *     };
- *     listView.setListAdapter(adapter);
- * 
- * - * @param The class type to use as a model for the data - * contained in the children of the given Firebase location - */ public abstract class FirebaseIndexListAdapter extends FirebaseListAdapter { /** - * @param activity The activity containing the ListView - * @param modelClass Firebase will marshall the data at a location into - * an instance of a class that you provide - * @param modelLayout This is the layout used to represent a single list item. - * You will be responsible for populating an instance of the corresponding - * view with the data from an instance of modelClass. - * @param keyRef The Firebase location containing the list of keys to be found in {@code dataRef}. - * Can also be a slice of a location, using some - * combination of {@code limit()}, {@code startAt()}, and {@code endAt()}. - * @param dataRef The Firebase location to watch for data changes. - * Each key key found in {@code keyRef}'s location represents - * a list item in the {@code ListView}. + * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the + * model class + * @param keyQuery The Firebase location containing the list of keys to be found in {@code + * dataRef}. Can also be a slice of a location, using some combination of {@code + * limit()}, {@code startAt()}, and {@code endAt()}. + * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code + * keyQuery}'s location represents a list item in the {@link ListView}. + * @see FirebaseIndexListAdapter#FirebaseIndexListAdapter(Activity, SnapshotParser, int, Query, + * DatabaseReference) + */ + public FirebaseIndexListAdapter(Activity activity, + SnapshotParser parser, + @LayoutRes int modelLayout, + Query keyQuery, + DatabaseReference dataRef) { + super(activity, new FirebaseIndexArray<>(keyQuery, dataRef, parser), modelLayout); + } + + /** + * @see #FirebaseIndexListAdapter(Activity, SnapshotParser, int, Query, DatabaseReference) */ public FirebaseIndexListAdapter(Activity activity, Class modelClass, @LayoutRes int modelLayout, - Query keyRef, - Query dataRef) { - super(activity, modelClass, modelLayout, new FirebaseIndexArray(keyRef, dataRef)); + Query keyQuery, + DatabaseReference dataRef) { + this(activity, new ClassSnapshotParser<>(modelClass), modelLayout, keyQuery, dataRef); } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java index 19e45d401..b2577e44f 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java @@ -1,87 +1,44 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.firebase.ui.database; import android.support.annotation.LayoutRes; import android.support.v7.widget.RecyclerView; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.Query; -/** - * This class is a generic way of backing an RecyclerView with a Firebase location. - * It handles all of the child events at the given Firebase location. It marshals received data into the given - * class type. - *

- * To use this class in your app, subclass it passing in all required parameters and implement the - * populateViewHolder method. - *

- *

- *     private static class ChatMessageViewHolder extends RecyclerView.ViewHolder {
- *         TextView messageText;
- *         TextView nameText;
- *
- *         public ChatMessageViewHolder(View itemView) {
- *             super(itemView);
- *             nameText = (TextView)itemView.findViewById(android.R.id.text1);
- *             messageText = (TextView) itemView.findViewById(android.R.id.text2);
- *         }
- *     }
- *
- *     FirebaseIndexRecyclerAdapter adapter;
- *     DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
- *
- *     RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler);
- *     recycler.setHasFixedSize(true);
- *     recycler.setLayoutManager(new LinearLayoutManager(this));
- *
- *     adapter = new FirebaseIndexRecyclerAdapter(
- *          ChatMessage.class, android.R.layout.two_line_list_item, ChatMessageViewHolder.class, keyRef, dataRef) {
- *         public void populateViewHolder(ChatMessageViewHolder chatMessageViewHolder,
- *                                        ChatMessage chatMessage,
- *                                        int position) {
- *             chatMessageViewHolder.nameText.setText(chatMessage.getName());
- *             chatMessageViewHolder.messageText.setText(chatMessage.getMessage());
- *         }
- *     };
- *     recycler.setAdapter(mAdapter);
- * 
- * - * @param The Java class that maps to the type of objects stored in the Firebase location. - * @param The ViewHolder class that contains the Views in the layout that is shown for each object. - */ public abstract class FirebaseIndexRecyclerAdapter extends FirebaseRecyclerAdapter { /** - * @param modelClass Firebase will marshall the data at a location into an instance - * of a class that you provide - * @param modelLayout This is the layout used to represent a single item in the list. - * You will be responsible for populating an - * instance of the corresponding view with the data from an instance of modelClass. - * @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout. - * @param keyRef The Firebase location containing the list of keys to be found in {@code dataRef}. - * Can also be a slice of a location, using some - * combination of {@code limit()}, {@code startAt()}, and {@code endAt()}. - * @param dataRef The Firebase location to watch for data changes. - * Each key key found at {@code keyRef}'s location represents - * a list item in the {@code RecyclerView}. + * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the + * model class + * @param keyQuery The Firebase location containing the list of keys to be found in {@code + * dataRef}. Can also be a slice of a location, using some combination of {@code + * limit()}, {@code startAt()}, and {@code endAt()}. + * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code + * keyQuery}'s location represents a list item in the {@link RecyclerView}. + * @see FirebaseRecyclerAdapter#FirebaseRecyclerAdapter(ObservableSnapshotArray, int, Class) + */ + public FirebaseIndexRecyclerAdapter(SnapshotParser parser, + @LayoutRes int modelLayout, + Class viewHolderClass, + Query keyQuery, + DatabaseReference dataRef) { + super(new FirebaseIndexArray<>(keyQuery, dataRef, parser), modelLayout, viewHolderClass); + } + + /** + * @see #FirebaseIndexRecyclerAdapter(SnapshotParser, int, Class, Query, DatabaseReference) */ public FirebaseIndexRecyclerAdapter(Class modelClass, @LayoutRes int modelLayout, Class viewHolderClass, - Query keyRef, - Query dataRef) { - super(modelClass, modelLayout, viewHolderClass, new FirebaseIndexArray(keyRef, dataRef)); + Query keyQuery, + DatabaseReference dataRef) { + this(new ClassSnapshotParser<>(modelClass), + modelLayout, + viewHolderClass, + keyQuery, + dataRef); } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java index 5ae25c451..acb781a0d 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java @@ -1,17 +1,3 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.firebase.ui.database; import android.app.Activity; @@ -20,6 +6,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import android.widget.ListView; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -27,114 +14,113 @@ import com.google.firebase.database.Query; /** - * This class is a generic way of backing an Android ListView with a Firebase location. - * It handles all of the child events at the given Firebase location. It marshals received data into the given - * class type. Extend this class and provide an implementation of {@code populateView}, which will be given an - * instance of your list item mLayout and an instance your class that holds your data. - * Simply populate the view however you like and this class will handle updating the list as the data changes. + * This class is a generic way of backing an Android {@link android.widget.ListView} with a Firebase + * location. It handles all of the child events at the given Firebase location. It marshals received + * data into the given class type. *

- *

- *     DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
- *     ListAdapter adapter = new FirebaseListAdapter(
- *              this, ChatMessage.class, android.R.layout.two_line_list_item, ref)
- *     {
- *         protected void populateView(View view, ChatMessage chatMessage, int position)
- *         {
- *             ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName());
- *             ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getMessage());
- *         }
- *     };
- *     listView.setListAdapter(adapter);
- * 
+ * See the README + * for an in-depth tutorial on how to set up the FirebaseListAdapter. * - * @param The class type to use as a model for the data - * contained in the children of the given Firebase location + * @param The class type to use as a model for the data contained in the children of the given + * Firebase location */ -public abstract class FirebaseListAdapter extends BaseAdapter { +public abstract class FirebaseListAdapter extends BaseAdapter implements FirebaseAdapter { private static final String TAG = "FirebaseListAdapter"; - private FirebaseArray mSnapshots; - private final Class mModelClass; protected Activity mActivity; + protected ObservableSnapshotArray mSnapshots; protected int mLayout; - FirebaseListAdapter(Activity activity, - Class modelClass, - @LayoutRes int modelLayout, - FirebaseArray snapshots) { + /** + * @param activity The {@link Activity} containing the {@link ListView} + * @param modelLayout This is the layout used to represent a single list item. You will be + * responsible for populating an instance of the corresponding view with the + * data from an instance of modelClass. + * @param snapshots The data used to populate the adapter + */ + public FirebaseListAdapter(Activity activity, + ObservableSnapshotArray snapshots, + @LayoutRes int modelLayout) { mActivity = activity; - mModelClass = modelClass; - mLayout = modelLayout; mSnapshots = snapshots; + mLayout = modelLayout; - mSnapshots.setOnChangedListener(new ChangeEventListener() { - @Override - public void onChildChanged(EventType type, int index, int oldIndex) { - FirebaseListAdapter.this.onChildChanged(type, index, oldIndex); - } - - @Override - public void onDataChanged() { - FirebaseListAdapter.this.onDataChanged(); - } - - @Override - public void onCancelled(DatabaseError error) { - FirebaseListAdapter.this.onCancelled(error); - } - }); + startListening(); + } + + /** + * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the model + * class + * @param query The Firebase location to watch for data changes. Can also be a slice of a + * location, using some combination of {@code limit()}, {@code startAt()}, and + * {@code endAt()}. + * @see #FirebaseListAdapter(Activity, ObservableSnapshotArray, int) + */ + public FirebaseListAdapter(Activity activity, + SnapshotParser parser, + @LayoutRes int modelLayout, + Query query) { + this(activity, new FirebaseArray<>(query, parser), modelLayout); } /** - * @param activity The activity containing the ListView - * @param modelClass Firebase will marshall the data at a location into - * an instance of a class that you provide - * @param modelLayout This is the layout used to represent a single list item. - * You will be responsible for populating an instance of the corresponding - * view with the data from an instance of modelClass. - * @param ref The Firebase location to watch for data changes. Can also be a slice of a location, - * using some combination of {@code limit()}, {@code startAt()}, and {@code endAt()}. + * @see #FirebaseListAdapter(Activity, SnapshotParser, int, Query) */ public FirebaseListAdapter(Activity activity, Class modelClass, - int modelLayout, - Query ref) { - this(activity, modelClass, modelLayout, new FirebaseArray(ref)); + @LayoutRes int modelLayout, + Query query) { + this(activity, new ClassSnapshotParser<>(modelClass), modelLayout, query); } + @Override + public void startListening() { + if (!mSnapshots.isListening(this)) { + mSnapshots.addChangeEventListener(this); + } + } + + @Override public void cleanup() { - mSnapshots.cleanup(); + mSnapshots.removeChangeEventListener(this); } @Override - public int getCount() { - return mSnapshots.getCount(); + public void onChildChanged(ChangeEventListener.EventType type, + DataSnapshot snapshot, + int index, + int oldIndex) { + notifyDataSetChanged(); } @Override - public T getItem(int position) { - return parseSnapshot(mSnapshots.getItem(position)); + public void onDataChanged() { } - /** - * This method parses the DataSnapshot into the requested type. You can override it in subclasses - * to do custom parsing. - * - * @param snapshot the DataSnapshot to extract the model from - * @return the model extracted from the DataSnapshot - */ - protected T parseSnapshot(DataSnapshot snapshot) { - return snapshot.getValue(mModelClass); + @Override + public void onCancelled(DatabaseError error) { + Log.w(TAG, error.toException()); } + @Override + public T getItem(int position) { + return mSnapshots.getObject(position); + } + + @Override public DatabaseReference getRef(int position) { - return mSnapshots.getItem(position).getRef(); + return mSnapshots.get(position).getRef(); + } + + @Override + public int getCount() { + return mSnapshots.size(); } @Override public long getItemId(int i) { // http://stackoverflow.com/questions/5100071/whats-the-purpose-of-item-ids-in-android-listview-adapter - return mSnapshots.getItem(i).getKey().hashCode(); + return mSnapshots.get(i).getKey().hashCode(); } @Override @@ -150,26 +136,6 @@ public View getView(int position, View view, ViewGroup viewGroup) { return view; } - /** - * @see ChangeEventListener#onChildChanged(ChangeEventListener.EventType, int, int) - */ - protected void onChildChanged(ChangeEventListener.EventType type, int index, int oldIndex) { - notifyDataSetChanged(); - } - - /** - * @see ChangeEventListener#onDataChanged() - */ - protected void onDataChanged() { - } - - /** - * @see ChangeEventListener#onCancelled(DatabaseError) - */ - protected void onCancelled(DatabaseError error) { - Log.w(TAG, error.toException()); - } - /** * Each time the data at the given Firebase location changes, * this method will be called for each item that needs to be displayed. diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java index 834f0e01e..3cf86d878 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java @@ -1,17 +1,3 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.firebase.ui.database; import android.support.annotation.LayoutRes; @@ -30,135 +16,125 @@ import java.lang.reflect.InvocationTargetException; /** - * This class is a generic way of backing an RecyclerView with a Firebase location. - * It handles all of the child events at the given Firebase location. It marshals received data into the given - * class type. - *

- * To use this class in your app, subclass it passing in all required parameters and implement the - * populateViewHolder method. - *

- *

- *     private static class ChatMessageViewHolder extends RecyclerView.ViewHolder {
- *         TextView messageText;
- *         TextView nameText;
- *
- *         public ChatMessageViewHolder(View itemView) {
- *             super(itemView);
- *             nameText = (TextView)itemView.findViewById(android.R.id.text1);
- *             messageText = (TextView) itemView.findViewById(android.R.id.text2);
- *         }
- *     }
- *
- *     FirebaseRecyclerAdapter adapter;
- *     DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
- *
- *     RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler);
- *     recycler.setHasFixedSize(true);
- *     recycler.setLayoutManager(new LinearLayoutManager(this));
- *
- *     adapter = new FirebaseRecyclerAdapter(
- *           ChatMessage.class, android.R.layout.two_line_list_item, ChatMessageViewHolder.class, ref) {
- *         public void populateViewHolder(ChatMessageViewHolder chatMessageViewHolder,
- *                                        ChatMessage chatMessage,
- *                                        int position) {
- *             chatMessageViewHolder.nameText.setText(chatMessage.getName());
- *             chatMessageViewHolder.messageText.setText(chatMessage.getMessage());
- *         }
- *     };
- *     recycler.setAdapter(mAdapter);
- * 
- *

- * To avoid Context leaks, make sure you invoke {@link #cleanup() cleanup} + * This class is a generic way of backing a {@link RecyclerView} with a Firebase location. It + * handles all of the child events at the given Firebase location and marshals received data into + * the given class type. *

+ * See the README + * for an in-depth tutorial on how to set up the FirebaseRecyclerAdapter. * * @param The Java class that maps to the type of objects stored in the Firebase location. - * @param The ViewHolder class that contains the Views in the layout that is shown for each object. + * @param The {@link RecyclerView.ViewHolder} class that contains the Views in the layout that + * is shown for each object. */ public abstract class FirebaseRecyclerAdapter - extends RecyclerView.Adapter { + extends RecyclerView.Adapter implements FirebaseAdapter { private static final String TAG = "FirebaseRecyclerAdapter"; - private FirebaseArray mSnapshots; - private Class mModelClass; + protected ObservableSnapshotArray mSnapshots; protected Class mViewHolderClass; protected int mModelLayout; - FirebaseRecyclerAdapter(Class modelClass, - @LayoutRes int modelLayout, - Class viewHolderClass, - FirebaseArray snapshots) { - mModelClass = modelClass; - mModelLayout = modelLayout; - mViewHolderClass = viewHolderClass; + /** + * @param snapshots The data used to populate the adapter + * @param modelLayout This is the layout used to represent a single item in the list. You + * will be responsible for populating an instance of the corresponding + * view with the data from an instance of modelClass. + * @param viewHolderClass The class that hold references to all sub-views in an instance + * modelLayout. + */ + public FirebaseRecyclerAdapter(ObservableSnapshotArray snapshots, + @LayoutRes int modelLayout, + Class viewHolderClass) { mSnapshots = snapshots; + mViewHolderClass = viewHolderClass; + mModelLayout = modelLayout; - mSnapshots.setOnChangedListener(new ChangeEventListener() { - @Override - public void onChildChanged(EventType type, int index, int oldIndex) { - FirebaseRecyclerAdapter.this.onChildChanged(type, index, oldIndex); - } - - @Override - public void onDataChanged() { - FirebaseRecyclerAdapter.this.onDataChanged(); - } - - @Override - public void onCancelled(DatabaseError error) { - FirebaseRecyclerAdapter.this.onCancelled(error); - } - }); + startListening(); } /** - * @param modelClass Firebase will marshall the data at a location into - * an instance of a class that you provide - * @param modelLayout This is the layout used to represent a single item in the list. - * You will be responsible for populating an instance of the corresponding - * view with the data from an instance of modelClass. - * @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout. - * @param ref The Firebase location to watch for data changes. Can also be a slice of a location, - * using some combination of {@code limit()}, {@code startAt()}, and {@code endAt()}. + * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the model + * class + * @param query The Firebase location to watch for data changes. Can also be a slice of a + * location, using some combination of {@code limit()}, {@code startAt()}, and + * {@code endAt()}. + * @see #FirebaseRecyclerAdapter(ObservableSnapshotArray, int, Class) + */ + public FirebaseRecyclerAdapter(SnapshotParser parser, + @LayoutRes int modelLayout, + Class viewHolderClass, + Query query) { + this(new FirebaseArray<>(query, parser), modelLayout, viewHolderClass); + } + + /** + * @see #FirebaseRecyclerAdapter(SnapshotParser, int, Class, Query) */ public FirebaseRecyclerAdapter(Class modelClass, - int modelLayout, + @LayoutRes int modelLayout, Class viewHolderClass, - Query ref) { - this(modelClass, modelLayout, viewHolderClass, new FirebaseArray(ref)); + Query query) { + this(new ClassSnapshotParser<>(modelClass), modelLayout, viewHolderClass, query); } + @Override + public void startListening() { + if (!mSnapshots.isListening(this)) { + mSnapshots.addChangeEventListener(this); + } + } + + @Override public void cleanup() { - mSnapshots.cleanup(); + mSnapshots.removeChangeEventListener(this); } @Override - public int getItemCount() { - return mSnapshots.getCount(); + public void onChildChanged(ChangeEventListener.EventType type, + DataSnapshot snapshot, + int index, + int oldIndex) { + switch (type) { + case ADDED: + notifyItemInserted(index); + break; + case CHANGED: + notifyItemChanged(index); + break; + case REMOVED: + notifyItemRemoved(index); + break; + case MOVED: + notifyItemMoved(oldIndex, index); + break; + default: + throw new IllegalStateException("Incomplete case statement"); + } } - public T getItem(int position) { - return parseSnapshot(mSnapshots.getItem(position)); + @Override + public void onDataChanged() { } - /** - * This method parses the DataSnapshot into the requested type. You can override it in subclasses - * to do custom parsing. - * - * @param snapshot the DataSnapshot to extract the model from - * @return the model extracted from the DataSnapshot - */ - protected T parseSnapshot(DataSnapshot snapshot) { - return snapshot.getValue(mModelClass); + @Override + public void onCancelled(DatabaseError error) { + Log.w(TAG, error.toException()); + } + + @Override + public T getItem(int position) { + return mSnapshots.getObject(position); } + @Override public DatabaseReference getRef(int position) { - return mSnapshots.getItem(position).getRef(); + return mSnapshots.get(position).getRef(); } @Override - public long getItemId(int position) { - // http://stackoverflow.com/questions/5100071/whats-the-purpose-of-item-ids-in-android-listview-adapter - return mSnapshots.getItem(position).getKey().hashCode(); + public int getItemCount() { + return mSnapshots.size(); } @Override @@ -178,50 +154,15 @@ public VH onCreateViewHolder(ViewGroup parent, int viewType) { } } - @Override - public void onBindViewHolder(VH viewHolder, int position) { - T model = getItem(position); - populateViewHolder(viewHolder, model, position); - } - @Override public int getItemViewType(int position) { return mModelLayout; } - /** - * @see ChangeEventListener#onChildChanged(ChangeEventListener.EventType, int, int) - */ - protected void onChildChanged(ChangeEventListener.EventType type, int index, int oldIndex) { - switch (type) { - case ADDED: - notifyItemInserted(index); - break; - case CHANGED: - notifyItemChanged(index); - break; - case REMOVED: - notifyItemRemoved(index); - break; - case MOVED: - notifyItemMoved(oldIndex, index); - break; - default: - throw new IllegalStateException("Incomplete case statement"); - } - } - - /** - * @see ChangeEventListener#onDataChanged() - */ - protected void onDataChanged() { - } - - /** - * @see ChangeEventListener#onCancelled(DatabaseError) - */ - protected void onCancelled(DatabaseError error) { - Log.w(TAG, error.toException()); + @Override + public void onBindViewHolder(VH viewHolder, int position) { + T model = getItem(position); + populateViewHolder(viewHolder, model, position); } /** diff --git a/database/src/main/java/com/firebase/ui/database/ImmutableList.java b/database/src/main/java/com/firebase/ui/database/ImmutableList.java new file mode 100644 index 000000000..103174d0a --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/ImmutableList.java @@ -0,0 +1,202 @@ +package com.firebase.ui.database; + +import android.support.annotation.RestrictTo; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +public abstract class ImmutableList implements List { + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean add(E element) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final void clear() { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final E set(int index, E element) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final void add(int index, E element) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final E remove(int index) { + throw new UnsupportedOperationException(); + } + + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + protected final class ImmutableIterator implements Iterator { + protected Iterator mIterator; + + public ImmutableIterator(Iterator iterator) { + mIterator = iterator; + } + + @Override + public boolean hasNext() { + return mIterator.hasNext(); + } + + @Override + public E next() { + return mIterator.next(); + } + } + + protected final class ImmutableListIterator implements ListIterator { + protected ListIterator mListIterator; + + public ImmutableListIterator(ListIterator listIterator) { + mListIterator = listIterator; + } + + @Override + public boolean hasNext() { + return mListIterator.hasNext(); + } + + @Override + public E next() { + return mListIterator.next(); + } + + @Override + public boolean hasPrevious() { + return mListIterator.hasPrevious(); + } + + @Override + public E previous() { + return mListIterator.previous(); + } + + @Override + public int nextIndex() { + return mListIterator.nextIndex(); + } + + @Override + public int previousIndex() { + return mListIterator.previousIndex(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void set(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(E e) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java new file mode 100644 index 000000000..2918103d6 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -0,0 +1,202 @@ +package com.firebase.ui.database; + +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import android.support.annotation.RestrictTo; + +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Exposes a collection of items in Firebase as a {@link List} of {@link DataSnapshot}. To observe + * the list attach a {@link com.google.firebase.database.ChildEventListener}. + * + * @param a POJO class to which the DataSnapshots can be converted. + */ +public abstract class ObservableSnapshotArray extends ImmutableList { + protected final List mListeners = new CopyOnWriteArrayList<>(); + protected final SnapshotParser mParser; + + /** + * Create an ObservableSnapshotArray where snapshots are parsed as objects of a particular + * class. + * + * @param clazz the class as which DataSnapshots should be parsed. + * @see ClassSnapshotParser + */ + public ObservableSnapshotArray(@NonNull Class clazz) { + this(new ClassSnapshotParser<>(clazz)); + } + + /** + * Create an ObservableSnapshotArray with a custom {@link SnapshotParser}. + * + * @param parser the {@link SnapshotParser} to use + */ + public ObservableSnapshotArray(@NonNull SnapshotParser parser) { + mParser = Preconditions.checkNotNull(parser); + } + + /** + * Attach a {@link ChangeEventListener} to this array. The listener will receive one {@link + * ChangeEventListener.EventType#ADDED} event for each item that already exists in the array at + * the time of attachment, and then receive all future child events. + */ + @CallSuper + public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { + Preconditions.checkNotNull(listener); + + mListeners.add(listener); + for (int i = 0; i < size(); i++) { + listener.onChildChanged(ChangeEventListener.EventType.ADDED, get(i), i, -1); + } + + return listener; + } + + /** + * Detach a {@link com.google.firebase.database.ChildEventListener} from this array. + */ + @CallSuper + public void removeChangeEventListener(@NonNull ChangeEventListener listener) { + mListeners.remove(listener); + } + + /** + * Removes all {@link ChangeEventListener}s. The list will be empty after this call returns. + * + * @see #removeChangeEventListener(ChangeEventListener) + */ + @CallSuper + public void removeAllListeners() { + for (ChangeEventListener listener : mListeners) { + removeChangeEventListener(listener); + } + } + + protected abstract List getSnapshots(); + + protected final void notifyChangeEventListeners(ChangeEventListener.EventType type, + DataSnapshot snapshot, + int index) { + notifyChangeEventListeners(type, snapshot, index, -1); + } + + protected final void notifyChangeEventListeners(ChangeEventListener.EventType type, + DataSnapshot snapshot, + int index, + int oldIndex) { + for (ChangeEventListener listener : mListeners) { + listener.onChildChanged(type, snapshot, index, oldIndex); + } + } + + protected final void notifyListenersOnDataChanged() { + for (ChangeEventListener listener : mListeners) { + listener.onDataChanged(); + } + } + + protected final void notifyListenersOnCancelled(DatabaseError error) { + for (ChangeEventListener listener : mListeners) { + listener.onCancelled(error); + } + } + + /** + * @return true if {@link FirebaseArray} is listening for change events from the Firebase + * database, false otherwise + */ + public final boolean isListening() { + return !mListeners.isEmpty(); + } + + /** + * @return true if the provided {@link ChangeEventListener} is listening for changes + */ + public final boolean isListening(ChangeEventListener listener) { + return mListeners.contains(listener); + } + + /** + * Get the {@link DataSnapshot} at a given position converted to an object of the parametrized + * type. This uses the {@link SnapshotParser} passed to the constructor. If the parser was not + * initialized this will throw an unchecked exception. + */ + public E getObject(int index) { + return mParser.parseSnapshot(get(index)); + } + + @Override + public int size() { + return getSnapshots().size(); + } + + @Override + public boolean isEmpty() { + return getSnapshots().isEmpty(); + } + + @Override + public boolean contains(Object o) { + return getSnapshots().contains(o); + } + + @Override + public Iterator iterator() { + return new ImmutableIterator(getSnapshots().iterator()); + } + + @Override + public DataSnapshot[] toArray() { + return getSnapshots().toArray(new DataSnapshot[getSnapshots().size()]); + } + + @Override + public boolean containsAll(Collection c) { + return getSnapshots().containsAll(c); + } + + @Override + public DataSnapshot get(int index) { + return getSnapshots().get(index); + } + + @Override + public int indexOf(Object o) { + return getSnapshots().indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return getSnapshots().lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new ImmutableListIterator(getSnapshots().listIterator()); + } + + @Override + public ListIterator listIterator(int index) { + return new ImmutableListIterator(getSnapshots().listIterator(index)); + } + + /** + * Guaranteed to throw an exception. Use {@link #toArray()} instead to get an array of {@link + * DataSnapshot}s. + * + * @throws UnsupportedOperationException always + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public final T[] toArray(T[] a) { + throw new UnsupportedOperationException(); + } +} diff --git a/database/src/main/java/com/firebase/ui/database/Preconditions.java b/database/src/main/java/com/firebase/ui/database/Preconditions.java new file mode 100644 index 000000000..0fb15b2fa --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/Preconditions.java @@ -0,0 +1,14 @@ +package com.firebase.ui.database; + +import android.support.annotation.RestrictTo; + +/** + * Convenience class for checking argument conditions. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class Preconditions { + public static T checkNotNull(T o) { + if (o == null) throw new IllegalArgumentException("Argument cannot be null."); + return o; + } +} diff --git a/database/src/main/java/com/firebase/ui/database/SnapshotParser.java b/database/src/main/java/com/firebase/ui/database/SnapshotParser.java new file mode 100644 index 000000000..8801f54f9 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/SnapshotParser.java @@ -0,0 +1,13 @@ +package com.firebase.ui.database; + +import com.google.firebase.database.DataSnapshot; + +public interface SnapshotParser { + /** + * This method parses the DataSnapshot into the requested type. + * + * @param snapshot the DataSnapshot to extract the model from + * @return the model extracted from the DataSnapshot + */ + T parseSnapshot(DataSnapshot snapshot); +} From fa832d484bdddc88a6bdb3c7507a67f8302db9d8 Mon Sep 17 00:00:00 2001 From: Frank Yang Date: Mon, 6 Mar 2017 19:31:37 +0900 Subject: [PATCH 05/86] Support Facebook Login without email --- .../java/com/firebase/ui/auth/provider/FacebookProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/provider/FacebookProvider.java b/auth/src/main/java/com/firebase/ui/auth/provider/FacebookProvider.java index dd0f071b4..1f759875c 100644 --- a/auth/src/main/java/com/firebase/ui/auth/provider/FacebookProvider.java +++ b/auth/src/main/java/com/firebase/ui/auth/provider/FacebookProvider.java @@ -146,8 +146,8 @@ public void onCompleted(JSONObject object, GraphResponse response) { String email = object.getString("email"); onSuccess(email, loginResult); } catch (JSONException e) { - Log.e(TAG, "JSON Exception reading from Facebook GraphRequest", e); - onFailure(new Bundle()); + Log.e(TAG, "Failure retrieving Facebook email", e); + onSuccess(null, loginResult); } } } From cf144b4568d89271d094a131e9221dfab09705f5 Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Mon, 6 Mar 2017 12:59:38 -0800 Subject: [PATCH 06/86] Update documentation and sample for #544 (#567) --- app/src/main/AndroidManifest.xml | 3 + .../uidemo/database/ChatActivity.java | 96 +++++---- .../firebase/uidemo/database/ChatHolder.java | 28 ++- .../uidemo/database/ChatIndexActivity.java | 67 +++++++ database/README.md | 189 +++++++++--------- 5 files changed, 229 insertions(+), 154 deletions(-) create mode 100644 app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52b5e1fca..ce2b85437 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,9 @@ + mAdapter; - private TextView mEmptyListMessage; + protected FirebaseRecyclerAdapter mAdapter; + protected TextView mEmptyListMessage; @Override protected void onCreate(Bundle savedInstanceState) { @@ -64,28 +62,9 @@ protected void onCreate(Bundle savedInstanceState) { mMessageEdit = (EditText) findViewById(R.id.messageEdit); mEmptyListMessage = (TextView) findViewById(R.id.emptyTextView); - mRef = FirebaseDatabase.getInstance().getReference(); - mChatRef = mRef.child("chats"); + mChatRef = FirebaseDatabase.getInstance().getReference().child("chats"); - mSendButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String uid = mAuth.getCurrentUser().getUid(); - String name = "User " + uid.substring(0, 6); - - Chat chat = new Chat(name, mMessageEdit.getText().toString(), uid); - mChatRef.push().setValue(chat, new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference reference) { - if (error != null) { - Log.e(TAG, "Failed to write message", error.toException()); - } - } - }); - - mMessageEdit.setText(""); - } - }); + mSendButton.setOnClickListener(this); mManager = new LinearLayoutManager(this); mManager.setReverseLayout(false); @@ -126,33 +105,30 @@ public void onDestroy() { } @Override - public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { - updateUI(); - } + public void onClick(View v) { + String uid = mAuth.getCurrentUser().getUid(); + String name = "User " + uid.substring(0, 6); - private void attachRecyclerViewAdapter() { - Query lastFifty = mChatRef.limitToLast(50); - mAdapter = new FirebaseRecyclerAdapter( - Chat.class, R.layout.message, ChatHolder.class, lastFifty) { + Chat chat = new Chat(name, mMessageEdit.getText().toString(), uid); + mChatRef.push().setValue(chat, new DatabaseReference.CompletionListener() { @Override - public void populateViewHolder(ChatHolder holder, Chat chat, int position) { - holder.setName(chat.getName()); - holder.setText(chat.getMessage()); - - FirebaseUser currentUser = mAuth.getCurrentUser(); - if (currentUser != null && chat.getUid().equals(currentUser.getUid())) { - holder.setIsSender(true); - } else { - holder.setIsSender(false); + public void onComplete(DatabaseError error, DatabaseReference reference) { + if (error != null) { + Log.e(TAG, "Failed to write message", error.toException()); } } + }); - @Override - public void onDataChanged() { - // If there are no chat messages, show a view that invites the user to add a message. - mEmptyListMessage.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - } - }; + mMessageEdit.setText(""); + } + + @Override + public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { + updateUI(); + } + + private void attachRecyclerViewAdapter() { + mAdapter = getAdapter(); // Scroll to bottom on new messages mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @@ -165,6 +141,26 @@ public void onItemRangeInserted(int positionStart, int itemCount) { mMessages.setAdapter(mAdapter); } + protected FirebaseRecyclerAdapter getAdapter() { + Query lastFifty = mChatRef.limitToLast(50); + return new FirebaseRecyclerAdapter( + Chat.class, + R.layout.message, + ChatHolder.class, + lastFifty) { + @Override + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { + holder.bind(chat); + } + + @Override + public void onDataChanged() { + // If there are no chat messages, show a view that invites the user to add a message. + mEmptyListMessage.setVisibility(getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + }; + } + private void signInAnonymously() { Toast.makeText(this, "Signing in...", Toast.LENGTH_SHORT).show(); mAuth.signInAnonymously() diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java b/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java index 7c95dccdb..ce3be5248 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java +++ b/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java @@ -13,6 +13,8 @@ import android.widget.TextView; import com.firebase.uidemo.R; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; public class ChatHolder extends RecyclerView.ViewHolder { private final TextView mNameField; @@ -36,7 +38,23 @@ public ChatHolder(View itemView) { mGray300 = ContextCompat.getColor(itemView.getContext(), R.color.material_gray_300); } - public void setIsSender(boolean isSender) { + public void bind(Chat chat) { + setName(chat.getName()); + setText(chat.getMessage()); + + FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser(); + setIsSender(currentUser != null && chat.getUid().equals(currentUser.getUid())); + } + + private void setName(String name) { + mNameField.setText(name); + } + + private void setText(String text) { + mTextField.setText(text); + } + + private void setIsSender(boolean isSender) { final int color; if (isSender) { color = mGreen300; @@ -56,12 +74,4 @@ public void setIsSender(boolean isSender) { ((RotateDrawable) mRightArrow.getBackground()).getDrawable() .setColorFilter(color, PorterDuff.Mode.SRC); } - - public void setName(String name) { - mNameField.setText(name); - } - - public void setText(String text) { - mTextField.setText(text); - } } diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java new file mode 100644 index 000000000..10186ad61 --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java @@ -0,0 +1,67 @@ +package com.firebase.uidemo.database; + +import android.os.Bundle; +import android.view.View; + +import com.firebase.ui.database.FirebaseIndexRecyclerAdapter; +import com.firebase.ui.database.FirebaseRecyclerAdapter; +import com.firebase.uidemo.R; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; + +public class ChatIndexActivity extends ChatActivity { + private DatabaseReference mChatIndicesRef; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mChatIndicesRef = FirebaseDatabase.getInstance().getReference().child("chatIndices"); + } + + @Override + public void onClick(View v) { + String uid = FirebaseAuth.getInstance().getCurrentUser().getUid(); + String name = "User " + uid.substring(0, 6); + Chat chat = new Chat(name, mMessageEdit.getText().toString(), uid); + + DatabaseReference chatRef = mChatRef.push(); + mChatIndicesRef.child(chatRef.getKey()).setValue(true); + chatRef.setValue(chat); + + mMessageEdit.setText(""); + } + + @Override + protected FirebaseRecyclerAdapter getAdapter() { + return new FirebaseIndexRecyclerAdapter( + Chat.class, + R.layout.message, + ChatHolder.class, + mChatIndicesRef.limitToLast(50), + mChatRef) { + @Override + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { + holder.bind(chat); + } + + @Override + public void onChildChanged(EventType type, + DataSnapshot snapshot, + int index, + int oldIndex) { + super.onChildChanged(type, snapshot, index, oldIndex); + + // TODO temporary fix for https://github.com/firebase/FirebaseUI-Android/issues/546 + onDataChanged(); + } + + @Override + public void onDataChanged() { + // If there are no chat messages, show a view that invites the user to add a message. + mEmptyListMessage.setVisibility(getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + }; + } +} diff --git a/database/README.md b/database/README.md index 0b572b71b..28124e387 100644 --- a/database/README.md +++ b/database/README.md @@ -1,15 +1,15 @@ -# firebase-ui-database +# FirebaseUI Database -## Using FirebaseUI to Populate a ListView +## Using FirebaseUI to populate a `RecyclerView` To use the FirebaseUI to display Firebase data, we need a few things: 1. A Java class that represents our database objects - 1. A custom list adapter to map from a collection from Firebase to Android + 1. A custom recycler view adapter to map from a collection from Firebase to Android ### Creating a model class -In your app, create a class that represents the data from Firebase that you want to show in the ListView. +In your app, create a class that represents the data from Firebase that you want to show in the RecyclerView. So say we have these chat messages in our Firebase database: @@ -61,7 +61,7 @@ public static class Chat { A few things to note here: * The fields have the exact same name as the properties in Firebase. This allows Firebase to automatically map the properties to these fields. - * There is a default (parameterless constructor) that is necessary for Firebase to be able to create a new instance of this class. + * There is a default (parameter-less constructor) that is necessary for Firebase to be able to create a new instance of this class. * There is a convenience constructor that takes the member fields, so that we easily create a fully initialized `Chat` in our app * the `getText`, `getUid`, and `getName` methods are so-called getters and follow a JavaBean pattern @@ -96,11 +96,11 @@ we get the `Chat` objects from the `DataSnapshot` with `getValue(Chat.class)`. T then read the properties that it got from the database and map them to the fields of our `Chat` class. But when we build our app using FirebaseUI, we often won't need to register our own EventListener. The -`FirebaseListAdapter` takes care of that for us. +`FirebaseRecyclerAdapter` takes care of that for us. -### Find the ListView +### Find the RecyclerView -We'll assume you've already added a `ListView` to your layout and have looked it up in the `onCreate` method of your activity: +We'll assume you've already added a `RecyclerView` to your layout and have looked it up in the `onCreate` method of your activity: ```java @Override @@ -108,10 +108,40 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView messagesView = (ListView) findViewById(R.id.messages_list); + RecyclerView messages = (RecyclerView) findViewById(R.id.messages); + messages.setLayoutManager(new LinearLayoutManager(this)); } ``` +### Create a custom ViewHolder + +A ViewHolder is similar to container of a ViewGroup that allows simple lookup of the sub-views of the group. +If we use the same layout as before (`android.R.layout.two_line_list_item`), there are two `TextView`s in there. +We can wrap that in a ViewHolder with: + +```java +public static class ChatHolder extends RecyclerView.ViewHolder { + private final TextView mNameField; + private final TextView mTextField; + + public ChatHolder(View itemView) { + super(itemView); + mNameField = (TextView) itemView.findViewById(android.R.id.text1); + mTextField = (TextView) itemView.findViewById(android.R.id.text2); + } + + public void setName(String name) { + mNameField.setText(name); + } + + public void setText(String text) { + mTextField.setText(text); + } +} +``` + +There's nothing magical going on here; we're just mapping numeric IDs and casts into a nice, type-safe contract. + ### Connect to Firebase First we'll set up a reference to the database of chat messages: @@ -122,15 +152,17 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView messagesView = (ListView) findViewById(R.id.messages_list); + RecyclerView messages = (RecyclerView) findViewById(R.id.messages); + messages.setLayoutManager(new LinearLayoutManager(this)); DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); } ``` -### Create custom FirebaseListAdapter subclass +### Create custom FirebaseRecyclerAdapter subclass -Next, we need to create a subclass of the `FirebaseListAdapter` with the correct parameters and implement its `populateView` method: +Next, we need to create a subclass of the `FirebaseRecyclerAdapter` with the correct parameters +and implement its `populateViewHolder` method: ```java @Override @@ -138,31 +170,36 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView messagesView = (ListView) findViewById(R.id.messages_list); + RecyclerView messages = (RecyclerView) findViewById(R.id.messages); + messages.setLayoutManager(new LinearLayoutManager(this)); DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); - - mAdapter = new FirebaseListAdapter(this, Chat.class, android.R.layout.two_line_list_item, ref) { - @Override - protected void populateView(View view, Chat chatMessage, int position) { - ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName()); - ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getText()); + mAdapter = new FirebaseRecyclerAdapter( + Chat.class, + ChatHolder.class, + R.layout.message, + ref) { + @Override + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { + holder.setName(chat.getName()); + holder.setText(chat.getMessage()); } }; - messagesView.setAdapter(mAdapter); + + messages.setAdapter(mAdapter); } ``` -In this last snippet we create a subclass of `FirebaseListAdapter`. +In this last snippet we create a subclass of `FirebaseRecyclerAdapter`. We tell is that it is of type ``, so that it is a type-safe collection. We also tell it to use `Chat.class` when reading messages from the database. Next we say that each message will be displayed in a `android.R.layout.two_line_list_item`, which is a built-in layout in Android that has two `TextView` elements under each other. Then we say that the adapter belongs to `this` activity and that it needs to monitor the data location in `ref`. -We also have to override the `populateView()` method, from the `FirebaseListAdapter`. The -`FirebaseListAdapter` will call our `populateView` method for each `Chat` it finds in the database. +We also have to override the `populateViewHolder` method, from the `FirebaseRecyclerAdapter`. The +`FirebaseRecyclerAdapter` will call our `populateViewHolder` method for each `Chat` it finds in the database. It passes us the `Chat` and a `View`, which is an instance of the `android.R.layout.two_line_list_item` we specified in the constructor. So what we do in our subclass is map the fields from `chatMessage` to the correct `TextView` controls from the `view`. The code is a bit verbose, but hey... that's Java and Android for you. @@ -170,7 +207,7 @@ correct `TextView` controls from the `view`. The code is a bit verbose, but hey. ### Clean up When the Activity is Destroyed Finally, we need to clean up after ourselves. When the activity is destroyed, we need to call `cleanup()` -on the `ListAdapter` so that it can stop listening for changes in the Firebase database. +on the adapter so that it can stop listening for changes in the Firebase database. ```java @Override @@ -191,25 +228,31 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView messagesView = (ListView) findViewById(R.id.messages_list); + RecyclerView messages = (RecyclerView) findViewById(R.id.messages); + messages.setLayoutManager(new LinearLayoutManager(this)); DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); - mAdapter = new FirebaseListAdapter(this, Chat.class, android.R.layout.two_line_list_item, ref) { + mAdapter = new FirebaseRecyclerAdapter( + Chat.class, + ChatHolder.class, + R.layout.message, + ref) { @Override - protected void populateView(View view, Chat chatMessage, int position) { - ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName()); - ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getText()); + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { + holder.setName(chat.getName()); + holder.setText(chat.getMessage()); } }; - messagesView.setAdapter(mAdapter); - final EditText mMessage = (EditText) findViewById(R.id.message_text); + messages.setAdapter(mAdapter); + + final EditText message = (EditText) findViewById(R.id.message_text); findViewById(R.id.send_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - ref.push().setValue(new Chat("puf", "1234", mMessage.getText().toString())); - mMessage.setText(""); + ref.push().setValue(new Chat("puf", "1234", message.getText().toString())); + message.setText(""); } }); } @@ -223,82 +266,38 @@ protected void onDestroy() { You're done! You now have a minimal, yet fully functional, chat app in about 30 lines of code. Not bad, right? -## Using FirebaseUI to Populate a RecyclerView - -RecyclerView is the new preferred way to handle potentially long lists of items. Since Firebase collections -can contain many items, there is an `FirebaseRecyclerAdapter` too. Here's how you use it: - -1. Create a custom ViewHolder class -2. Create a custom subclass FirebaseRecyclerAdapter - -The rest of the steps is the same as for the `FirebaseListAdapter` above, so be sure to read that first. - -### Create a custom ViewHolder - -A ViewHolder is similar to container of a ViewGroup that allows simple lookup of the sub-views of the group. -If we use the same layout as before (`android.R.layout.two_line_list_item`), there are two `TextView`s in there. -We can wrap that in a ViewHolder with: +## Using FirebaseUI to populate a `ListView` +ListView is the older, yet simpler way to handle lists of items. Using it is analogous to +using a `FirebaseRecyclerAdapter`, but with `FirebaseListAdapter` instead and no `ViewHolder`: ```java -public static class ChatHolder extends RecyclerView.ViewHolder { - private final TextView mNameField; - private final TextView mTextField; - - public ChatHolder(View itemView) { - super(itemView); - mNameField = (TextView) itemView.findViewById(android.R.id.text1); - mTextField = (TextView) itemView.findViewById(android.R.id.text2); - } - - public void setName(String name) { - mNameField.setText(name); - } - - public void setText(String text) { - mTextField.setText(text); - } -} -``` - -There's nothing magical going on here; we're just mapping numeric IDs and casts into a nice, type-safe contract. +ListView messagesView = (ListView) findViewById(R.id.messages_list); -### Create a custom FirebaseRecyclerAdapter - -Just like we did for `FirebaseListAdapter`, we'll create an anonymous subclass for our Chats, but this time we'll use `FirebaseRecyclerAdapter`: - -```java -RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler); -recycler.setHasFixedSize(true); -recycler.setLayoutManager(new LinearLayoutManager(this)); +DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); -mAdapter = new FirebaseRecyclerAdapter(Chat.class, android.R.layout.two_line_list_item, ChatHolder.class, mRef) { +mAdapter = new FirebaseListAdapter(this, Chat.class, android.R.layout.two_line_list_item, ref) { @Override - public void populateViewHolder(ChatHolder chatMessageViewHolder, Chat chatMessage, int position) { - chatMessageViewHolder.setName(chatMessage.getName()); - chatMessageViewHolder.setText(chatMessage.getText()); + protected void populateView(View view, Chat chatMessage, int position) { + ((TextView) view.findViewById(android.R.id.text1)).setText(chatMessage.getName()); + ((TextView) view.findViewById(android.R.id.text2)).setText(chatMessage.getText()); + } }; -recycler.setAdapter(mAdapter); +messagesView.setAdapter(mAdapter); ``` -Like before, we get a custom RecyclerView populated with data from Firebase by setting the properties to the correct fields. - ## Using FirebaseUI with indexed data -If your data is [properly indexed](https://firebase.google.com/docs/database/android/structure-data#best_practices_for_data_structure), change your adapter initalization like so: +If your data is [properly indexed](https://firebase.google.com/docs/database/android/structure-data#best_practices_for_data_structure), change your adapter initialization like so: For a `RecyclerView`, use `FirebaseIndexRecyclerAdapter` instead of `FirebaseRecyclerAdapter`: ```java -new FirebaseIndexRecyclerAdapter(Chat.class, - android.R.layout.two_line_list_item, - ChatHolder.class, - keyRef, // The Firebase location containing the list of keys to be found in dataRef. - dataRef) //The Firebase location to watch for data changes. Each key key found at keyRef's location represents a list item in the RecyclerView. -``` - -And for a `ListView`, use `FirebaseIndexListAdapter`; -```java -new FirebaseIndexListAdapter(this, Chat.class, android.R.layout.two_line_list_item, keyRef, dataRef) +new FirebaseIndexRecyclerAdapter( + Chat.class, + android.R.layout.two_line_list_item, + ChatHolder.class, + keyRef, // The Firebase location containing the list of keys to be found in dataRef. + dataRef) //The Firebase location to watch for data changes. Each key key found at keyRef's location represents a list item in the RecyclerView. ``` `keyRef` is the location of your keys, and `dataRef` is the location of your data. From 9ae131ae617f91853d98c4cfc901392686ede4d3 Mon Sep 17 00:00:00 2001 From: Aaron Mandle Date: Mon, 13 Mar 2017 09:52:54 -0700 Subject: [PATCH 07/86] Add reauthentication (#583) --- .../firebase/uidemo/auth/AuthUiActivity.java | 22 ++- .../uidemo/auth/SignedInActivity.java | 107 ++++++++++++- app/src/main/res/layout/signed_in_layout.xml | 9 ++ app/src/main/res/values/strings.xml | 2 + .../java/com/firebase/ui/auth/AuthUI.java | 143 +++++++++++++----- .../com/firebase/ui/auth/KickoffActivity.java | 37 ++++- .../ui/auth/provider/GoogleProvider.java | 12 +- .../firebase/ui/auth/ui/FlowParameters.java | 20 ++- .../WelcomeBackPasswordPrompt.java | 12 +- .../auth/ui/email/RegisterEmailActivity.java | 4 + .../util/signincontainer/SignInDelegate.java | 104 +++++++++---- .../welcome_back_password_prompt_layout.xml | 1 + auth/src/main/res/values/strings.xml | 9 ++ .../ui/auth/testhelpers/TestHelper.java | 8 +- 14 files changed, 406 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java index 04a4ec262..d1bed5efa 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java +++ b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java @@ -126,16 +126,15 @@ public class AuthUiActivity extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.auth_ui_layout); + ButterKnife.bind(this); FirebaseAuth auth = FirebaseAuth.getInstance(); if (auth.getCurrentUser() != null) { - startActivity(SignedInActivity.createIntent(this, null)); + startSignedInActivity(null); finish(); } - setContentView(R.layout.auth_ui_layout); - ButterKnife.bind(this); - if (!isGoogleConfigured()) { mUseGoogleProvider.setChecked(false); mUseGoogleProvider.setEnabled(false); @@ -208,7 +207,7 @@ private void handleSignInResponse(int resultCode, Intent data) { // Successfully signed in if (resultCode == ResultCodes.OK) { - startActivity(SignedInActivity.createIntent(this, response)); + startSignedInActivity(response); finish(); return; } else { @@ -233,6 +232,19 @@ private void handleSignInResponse(int resultCode, Intent data) { showSnackbar(R.string.unknown_sign_in_response); } + private void startSignedInActivity(IdpResponse response) { + startActivity( + SignedInActivity.createIntent( + this, + response, + new SignedInActivity.SignedInConfig( + getSelectedLogo(), + getSelectedTheme(), + getSelectedProviders(), + getSelectedTosUrl(), + mEnableSmartLock.isChecked()))); + } + @MainThread private void setGoogleScopesEnabled(boolean enabled) { mGoogleScopesLabel.setEnabled(enabled); diff --git a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java index 4642ff034..ebc374e3b 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java +++ b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java @@ -18,6 +18,8 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.StringRes; @@ -31,6 +33,7 @@ import com.bumptech.glide.Glide; import com.firebase.ui.auth.AuthUI; +import com.firebase.ui.auth.AuthUI.IdpConfig; import com.firebase.ui.auth.IdpResponse; import com.firebase.uidemo.R; import com.google.android.gms.tasks.OnCompleteListener; @@ -41,13 +44,19 @@ import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.GoogleAuthProvider; +import java.util.ArrayList; import java.util.Iterator; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import java.util.List; public class SignedInActivity extends AppCompatActivity { + private static final String EXTRA_SIGNED_IN_CONFIG = "extra_signed_in_config"; + + private static final int RC_REAUTH = 100; + @BindView(android.R.id.content) View mRootView; @@ -65,6 +74,8 @@ public class SignedInActivity extends AppCompatActivity { private IdpResponse mIdpResponse; + private SignedInConfig mSignedInConfig; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -77,6 +88,7 @@ public void onCreate(Bundle savedInstanceState) { } mIdpResponse = IdpResponse.fromResultIntent(getIntent()); + mSignedInConfig = getIntent().getParcelableExtra(EXTRA_SIGNED_IN_CONFIG); setContentView(R.layout.signed_in_layout); ButterKnife.bind(this); @@ -101,6 +113,21 @@ public void onComplete(@NonNull Task task) { }); } + @OnClick(R.id.reauthenticate) + public void reauthenticate() { + Intent reauthIntent = AuthUI.getInstance() + .createReauthIntentBuilder() + .setProviders(mSignedInConfig.providerInfo) + .setIsSmartLockEnabled(mSignedInConfig.isSmartLockEnabled) + .setLogo(mSignedInConfig.logo) + .setTheme(mSignedInConfig.theme) + .setTosUrl(mSignedInConfig.tosUrl) + .setReauthReason(getString(R.string.reauthentication_reason)) + .build(); + + startActivityForResult(reauthIntent, RC_REAUTH); + } + @OnClick(R.id.delete_account) public void deleteAccountClicked() { @@ -185,14 +212,18 @@ private void populateIdpToken() { token = mIdpResponse.getIdpToken(); secret = mIdpResponse.getIdpSecret(); } + View idpTokenLayout = findViewById(R.id.idp_token_layout); if (token == null) { - findViewById(R.id.idp_token_layout).setVisibility(View.GONE); + idpTokenLayout.setVisibility(View.GONE); } else { + idpTokenLayout.setVisibility(View.VISIBLE); ((TextView) findViewById(R.id.idp_token)).setText(token); } + View idpSecretLayout = findViewById(R.id.idp_secret_layout); if (secret == null) { - findViewById(R.id.idp_secret_layout).setVisibility(View.GONE); + idpSecretLayout.setVisibility(View.GONE); } else { + idpSecretLayout.setVisibility(View.VISIBLE); ((TextView) findViewById(R.id.idp_secret)).setText(secret); } } @@ -203,9 +234,79 @@ private void showSnackbar(@StringRes int errorMessageRes) { .show(); } - public static Intent createIntent(Context context, IdpResponse idpResponse) { + static final class SignedInConfig implements Parcelable { + int logo; + int theme; + List providerInfo; + String tosUrl; + boolean isSmartLockEnabled; + + SignedInConfig( + int logo, + int theme, + List providerInfo, + String tosUrl, + boolean isSmartLockEnabled) { + this.logo = logo; + this.theme = theme; + this.providerInfo = providerInfo; + this.tosUrl = tosUrl; + this.isSmartLockEnabled = isSmartLockEnabled; + } + + SignedInConfig(Parcel in) { + logo = in.readInt(); + theme = in.readInt(); + providerInfo = new ArrayList<>(); + in.readList(providerInfo, IdpConfig.class.getClassLoader()); + tosUrl = in.readString(); + isSmartLockEnabled = in.readInt() != 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public SignedInConfig createFromParcel(Parcel in) { + return new SignedInConfig(in); + } + + @Override + public SignedInConfig[] newArray(int size) { + return new SignedInConfig[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(logo); + dest.writeInt(theme); + dest.writeList(providerInfo); + dest.writeString(tosUrl); + dest.writeInt(isSmartLockEnabled ? 1 : 0); + } + } + + public static Intent createIntent( + Context context, + IdpResponse idpResponse, + SignedInConfig signedInConfig) { Intent in = IdpResponse.getIntent(idpResponse); in.setClass(context, SignedInActivity.class); + in.putExtra(EXTRA_SIGNED_IN_CONFIG, signedInConfig); return in; } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == RC_REAUTH) { + mIdpResponse = IdpResponse.fromResultIntent(data); + populateIdpToken(); + populateProfile(); + } + } } diff --git a/app/src/main/res/layout/signed_in_layout.xml b/app/src/main/res/layout/signed_in_layout.xml index 908966256..1f020404f 100644 --- a/app/src/main/res/layout/signed_in_layout.xml +++ b/app/src/main/res/layout/signed_in_layout.xml @@ -40,6 +40,15 @@ android:layout_margin="16dp" android:text="@string/sign_out"/> +