diff --git a/README.md b/README.md index f19ddf4ad..6113f9dd0 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ [![Build Status](https://travis-ci.org/firebase/FirebaseUI-Android.svg?branch=master)](https://travis-ci.org/firebase/FirebaseUI-Android) FirebaseUI is an open-source library for Android that allows you to -quickly connect common UI elements to [Firebase](https://firebase.google.com) -APIs like the Realtime Database or Firebase Authentication. +quickly connect common UI elements to [Firebase](https://firebase.google.com) APIs. A compatible FirebaseUI client is also available for [iOS](https://github.com/firebase/firebaseui-ios). @@ -19,10 +18,11 @@ A compatible FirebaseUI client is also available for [iOS](https://github.com/fi ## Usage -FirebaseUI has separate modules for using Firebase Database, Auth, and Storage. To -get started, see the individual instructions for each module: +FirebaseUI has separate modules for using Firebase Realtime Database, Cloud Firestore, +Firebase Auth, and Cloud Storage. To get started, see the individual instructions for each module: * [firebase-ui-database](database/README.md) + * [firebase-ui-firestore](firestore/README.md) * [firebase-ui-auth](auth/README.md) * [firebase-ui-storage](storage/README.md) @@ -38,17 +38,17 @@ libraries. ```groovy dependencies { - // FirebaseUI Database only - compile 'com.firebaseui:firebase-ui-database:2.4.0' + // FirebaseUI for Firebase Realtime Database + compile 'com.firebaseui:firebase-ui-database:3.0.0' + + // FirebaseUI for Cloud Firestore + compile 'com.firebaseui:firebase-ui-firestore:3.0.0' - // FirebaseUI Auth only - compile 'com.firebaseui:firebase-ui-auth:2.4.0' + // FirebaseUI for Firebase Auth + compile 'com.firebaseui:firebase-ui-auth:3.0.0' - // FirebaseUI Storage only - compile 'com.firebaseui:firebase-ui-storage:2.4.0' - - // Single target that includes all FirebaseUI libraries above - compile 'com.firebaseui:firebase-ui:2.4.0' + // FirebaseUI for Cloud Storage + compile 'com.firebaseui:firebase-ui-storage:3.0.0' } ``` @@ -63,6 +63,7 @@ After the project is synchronized, we're ready to start using Firebase functiona If you are using an old version of FirebaseUI and upgrading, please see the appropriate migration guide: + * [Upgrade from 2.3.0 to 3.x.x](./docs/upgrade-to-3.0.md) * [Upgrade from 1.2.0 to 2.x.x](./docs/upgrade-to-2.0.md) ## Dependencies @@ -78,6 +79,9 @@ firebase-ui-auth firebase-ui-database |--- com.google.firebase:firebase-database +firebase-ui-firestore +|--- com.google.firebase:firebase-firestore + firebase-ui-storage |--- com.google.firebase:firebase-storage ``` @@ -91,6 +95,7 @@ For convenience, here are some recent examples: | FirebaseUI Version | Firebase/Play Services Version | |--------------------|--------------------------------| +| 3.0.0 | 11.4.2 | | 2.4.0 | 11.4.0 | | 2.3.0 | 11.0.4 | | 2.2.0 | 11.0.4 | @@ -121,7 +126,7 @@ compile "com.android.support:customtabs:$BAR" compile "com.android.support:cardview-v7:$BAR" ``` -Database: +Realtime Database: ```groovy compile "com.google.firebase:firebase-database:$FOO" @@ -130,6 +135,15 @@ compile "com.android.support:recyclerview-v7:$BAR" compile "com.android.support:support-v4:$BAR" ``` +Firestore: + +```groovy +compile "com.google.firebase:firebase-firestore:$FOO" + +compile "com.android.support:recyclerview-v7:$BAR" +compile "com.android.support:support-v4:$BAR" +``` + Storage: ```groovy diff --git a/app/build.gradle b/app/build.gradle index 822abcaf8..c6ae97be1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,7 @@ android { versionName "1.0" vectorDrawables.useSupportLibrary = true + multiDexEnabled true } buildTypes { @@ -34,9 +35,11 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile "com.android.support:design:$supportLibraryVersion" + compile 'com.android.support:multidex:1.0.1' compile project(path: ':auth') compile project(path: ':database') + compile project(path: ':firestore') compile project(path: ':storage') compile "com.google.android.gms:play-services-auth:$firebaseVersion" @@ -44,18 +47,24 @@ dependencies { compile "com.google.firebase:firebase-database:$firebaseVersion" compile "com.google.firebase:firebase-storage:$firebaseVersion" - compile('com.facebook.android:facebook-android-sdk:4.25.0') + compile('com.facebook.android:facebook-login:4.27.0') compile("com.twitter.sdk.android:twitter-core:3.0.0@aar") { transitive = true } + compile "android.arch.lifecycle:runtime:$architectureVersion" + compile "android.arch.lifecycle:extensions:$architectureVersion" + + compile 'com.github.bumptech.glide:glide:4.1.1' + annotationProcessor 'com.github.bumptech.glide:compiler:4.1.1' + // The following dependencies are not required to use the Firebase UI library. // They are used to make some aspects of the demo app implementation simpler for // demonstrative purposes, and you may find them useful in your own apps; YMMV. - compile 'pub.devrel:easypermissions:0.4.3' + compile 'pub.devrel:easypermissions:1.0.1' compile 'com.jakewharton:butterknife:8.7.0' annotationProcessor 'com.jakewharton:butterknife-compiler:8.7.0' - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' - testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' + debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4' + releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' + testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' } apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e6d93103a..6b85e38c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,33 +21,40 @@ + + - - - - + android:label="@string/title_auth_activity" /> + android:label="@string/title_auth_activity" /> + + + + + + + - + + android:label="@string/title_storage_activity" /> diff --git a/app/src/main/java/com/firebase/uidemo/ChooserActivity.java b/app/src/main/java/com/firebase/uidemo/ChooserActivity.java index d1c36ac63..38b3deea3 100644 --- a/app/src/main/java/com/firebase/uidemo/ChooserActivity.java +++ b/app/src/main/java/com/firebase/uidemo/ChooserActivity.java @@ -26,7 +26,8 @@ import android.widget.TextView; import com.firebase.uidemo.auth.AuthUiActivity; -import com.firebase.uidemo.database.ChatActivity; +import com.firebase.uidemo.database.firestore.FirestoreChatActivity; +import com.firebase.uidemo.database.realtime.RealtimeDbChatActivity; import com.firebase.uidemo.storage.ImageActivity; import butterknife.BindView; @@ -49,21 +50,24 @@ protected void onCreate(Bundle savedInstanceState) { private static class ActivityChooserAdapter extends RecyclerView.Adapter { private static final Class[] CLASSES = new Class[]{ - ChatActivity.class, AuthUiActivity.class, + FirestoreChatActivity.class, + RealtimeDbChatActivity.class, ImageActivity.class, }; private static final int[] DESCRIPTION_NAMES = new int[]{ - R.string.name_chat, - R.string.name_auth_ui, - R.string.name_image + R.string.title_auth_activity, + R.string.title_firestore_activity, + R.string.title_realtime_database_activity, + R.string.title_storage_activity }; private static final int[] DESCRIPTION_IDS = new int[]{ - R.string.desc_chat, - R.string.desc_auth_ui, - R.string.desc_image + R.string.desc_auth, + R.string.desc_firestore, + R.string.desc_realtime_database, + R.string.desc_storage }; @Override @@ -92,8 +96,8 @@ private static class ActivityStarterHolder extends RecyclerView.ViewHolder imple public ActivityStarterHolder(View itemView) { super(itemView); - mTitle = (TextView) itemView.findViewById(R.id.text1); - mDescription = (TextView) itemView.findViewById(R.id.text2); + mTitle = itemView.findViewById(R.id.text1); + mDescription = itemView.findViewById(R.id.text2); } private void bind(Class aClass, @StringRes int name, @StringRes int description) { diff --git a/app/src/main/java/com/firebase/uidemo/auth/FirebaseUIDemo.java b/app/src/main/java/com/firebase/uidemo/auth/FirebaseUIDemo.java index f9d7bbe1d..fcd419cb2 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/FirebaseUIDemo.java +++ b/app/src/main/java/com/firebase/uidemo/auth/FirebaseUIDemo.java @@ -1,13 +1,13 @@ package com.firebase.uidemo.auth; -import android.app.Application; import android.content.Context; +import android.support.multidex.MultiDexApplication; import android.support.v7.app.AppCompatDelegate; import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.RefWatcher; -public class FirebaseUIDemo extends Application { +public class FirebaseUIDemo extends MultiDexApplication { private RefWatcher mRefWatcher; static { 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 72ec52629..3879ab10f 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java +++ b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java @@ -31,11 +31,11 @@ import android.widget.ImageView; import android.widget.TextView; -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.firebase.uidemo.storage.GlideApp; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.auth.EmailAuthProvider; @@ -69,7 +69,6 @@ public class SignedInActivity extends AppCompatActivity { @BindView(R.id.user_display_name) TextView mUserDisplayName; - @BindView(R.id.user_phone_number) TextView mUserPhoneNumber; @@ -165,7 +164,7 @@ public void onComplete(@NonNull Task task) { private void populateProfile() { FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser(); if (user.getPhotoUrl() != null) { - Glide.with(this) + GlideApp.with(this) .load(user.getPhotoUrl()) .fitCenter() .into(mUserProfilePicture); diff --git a/app/src/main/java/com/firebase/uidemo/database/AbstractChat.java b/app/src/main/java/com/firebase/uidemo/database/AbstractChat.java new file mode 100644 index 000000000..74cd4350d --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/AbstractChat.java @@ -0,0 +1,14 @@ +package com.firebase.uidemo.database; + +/** + * Common interface for chat messages, helps share code between RTDB and Firestore examples. + */ +public abstract class AbstractChat { + + public abstract String getName(); + + public abstract String getMessage(); + + public abstract String getUid(); + +} diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java deleted file mode 100644 index 6429b0a2c..000000000 --- a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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.uidemo.database; - -import android.arch.lifecycle.LifecycleRegistry; -import android.arch.lifecycle.LifecycleRegistryOwner; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; - -import com.firebase.ui.database.FirebaseRecyclerAdapter; -import com.firebase.uidemo.R; -import com.firebase.uidemo.util.SignInResultNotifier; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.firebase.auth.AuthResult; -import com.google.firebase.auth.FirebaseAuth; -import com.google.firebase.database.DatabaseError; -import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.FirebaseDatabase; -import com.google.firebase.database.Query; - -public class ChatActivity extends AppCompatActivity - implements FirebaseAuth.AuthStateListener, View.OnClickListener, LifecycleRegistryOwner { - private static final String TAG = "RecyclerViewDemo"; - - // TODO remove once arch components are merged into support lib - private final LifecycleRegistry mRegistry = new LifecycleRegistry(this); - - private FirebaseAuth mAuth; - protected DatabaseReference mChatRef; - private Button mSendButton; - protected EditText mMessageEdit; - - private RecyclerView mMessages; - private LinearLayoutManager mManager; - private FirebaseRecyclerAdapter mAdapter; - protected TextView mEmptyListMessage; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_chat); - - mAuth = FirebaseAuth.getInstance(); - mAuth.addAuthStateListener(this); - - mSendButton = (Button) findViewById(R.id.sendButton); - mMessageEdit = (EditText) findViewById(R.id.messageEdit); - mEmptyListMessage = (TextView) findViewById(R.id.emptyTextView); - - mChatRef = FirebaseDatabase.getInstance().getReference().child("chats"); - - mSendButton.setOnClickListener(this); - - mManager = new LinearLayoutManager(this); - mManager.setReverseLayout(false); - - mMessages = (RecyclerView) findViewById(R.id.messagesList); - mMessages.setHasFixedSize(true); - mMessages.setLayoutManager(mManager); - - if (isSignedIn()) { attachRecyclerViewAdapter(); } - } - - @Override - public void onStart() { - super.onStart(); - - // Default Database rules do not allow unauthenticated reads, so we need to - // sign in before attaching the RecyclerView adapter otherwise the Adapter will - // not be able to read any data from the Database. - if (!isSignedIn()) { signInAnonymously(); } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mAuth != null) { - mAuth.removeAuthStateListener(this); - } - } - - @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(""); - } - - @Override - public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { - updateUI(); - } - - private void attachRecyclerViewAdapter() { - mAdapter = getAdapter(); - - // Scroll to bottom on new messages - mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - mManager.smoothScrollToPosition(mMessages, null, mAdapter.getItemCount()); - } - }); - - mMessages.setAdapter(mAdapter); - } - - protected FirebaseRecyclerAdapter getAdapter() { - Query lastFifty = mChatRef.limitToLast(50); - return new FirebaseRecyclerAdapter( - Chat.class, - R.layout.message, - ChatHolder.class, - lastFifty, - this) { - @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() - .addOnSuccessListener(this, new OnSuccessListener() { - @Override - public void onSuccess(AuthResult result) { - attachRecyclerViewAdapter(); - } - }) - .addOnCompleteListener(new SignInResultNotifier(this)); - } - - private boolean isSignedIn() { - return mAuth.getCurrentUser() != null; - } - - private void updateUI() { - // Sending only allowed when signed in - mSendButton.setEnabled(isSignedIn()); - mMessageEdit.setEnabled(isSignedIn()); - } - - @Override - public LifecycleRegistry getLifecycle() { - return mRegistry; - } -} 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 ce3be5248..1d42e3d8e 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java +++ b/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java @@ -28,17 +28,17 @@ public class ChatHolder extends RecyclerView.ViewHolder { public ChatHolder(View itemView) { super(itemView); - mNameField = (TextView) itemView.findViewById(R.id.name_text); - mTextField = (TextView) itemView.findViewById(R.id.message_text); - mLeftArrow = (FrameLayout) itemView.findViewById(R.id.left_arrow); - mRightArrow = (FrameLayout) itemView.findViewById(R.id.right_arrow); - mMessageContainer = (RelativeLayout) itemView.findViewById(R.id.message_container); - mMessage = (LinearLayout) itemView.findViewById(R.id.message); + mNameField = itemView.findViewById(R.id.name_text); + mTextField = itemView.findViewById(R.id.message_text); + mLeftArrow = itemView.findViewById(R.id.left_arrow); + mRightArrow = itemView.findViewById(R.id.right_arrow); + mMessageContainer = itemView.findViewById(R.id.message_container); + mMessage = itemView.findViewById(R.id.message); mGreen300 = ContextCompat.getColor(itemView.getContext(), R.color.material_green_300); mGray300 = ContextCompat.getColor(itemView.getContext(), R.color.material_gray_300); } - public void bind(Chat chat) { + public void bind(AbstractChat chat) { setName(chat.getName()); setText(chat.getMessage()); diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java deleted file mode 100644 index e2e4f7671..000000000 --- a/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.firebase.uidemo.database; - -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.DatabaseReference; -import com.google.firebase.database.FirebaseDatabase; - -public class ChatIndexActivity extends ChatActivity { - private DatabaseReference mChatIndicesRef; - - @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() { - mChatIndicesRef = FirebaseDatabase.getInstance() - .getReference() - .child("chatIndices") - .child(FirebaseAuth.getInstance().getCurrentUser().getUid()); - - return new FirebaseIndexRecyclerAdapter( - Chat.class, - R.layout.message, - ChatHolder.class, - mChatIndicesRef.limitToLast(50), - mChatRef, - this) { - @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); - } - }; - } -} diff --git a/app/src/main/java/com/firebase/uidemo/database/firestore/Chat.java b/app/src/main/java/com/firebase/uidemo/database/firestore/Chat.java new file mode 100644 index 000000000..d24b095b8 --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/firestore/Chat.java @@ -0,0 +1,59 @@ +package com.firebase.uidemo.database.firestore; + +import com.firebase.uidemo.database.AbstractChat; +import com.google.firebase.firestore.IgnoreExtraProperties; +import com.google.firebase.firestore.ServerTimestamp; + +import java.util.Date; + +@IgnoreExtraProperties +public class Chat extends AbstractChat { + + private String mName; + private String mMessage; + private String mUid; + private Date mTimestamp; + + public Chat() { + // Needed for Firebase + } + + public Chat(String name, String message, String uid) { + mName = name; + mMessage = message; + mUid = uid; + } + + public String getName() { + return mName; + } + + public void setName(String name) { + mName = name; + } + + public String getMessage() { + return mMessage; + } + + public void setMessage(String message) { + mMessage = message; + } + + public String getUid() { + return mUid; + } + + public void setUid(String uid) { + mUid = uid; + } + + @ServerTimestamp + public Date getTimestamp() { + return mTimestamp; + } + + public void setTimestamp(Date timestamp) { + mTimestamp = timestamp; + } +} diff --git a/app/src/main/java/com/firebase/uidemo/database/firestore/FirestoreChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/firestore/FirestoreChatActivity.java new file mode 100644 index 000000000..271342ce1 --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/firestore/FirestoreChatActivity.java @@ -0,0 +1,173 @@ +package com.firebase.uidemo.database.firestore; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.firebase.ui.auth.ui.ImeHelper; +import com.firebase.ui.firestore.FirestoreRecyclerAdapter; +import com.firebase.ui.firestore.FirestoreRecyclerOptions; +import com.firebase.uidemo.R; +import com.firebase.uidemo.database.ChatHolder; +import com.firebase.uidemo.database.realtime.Chat; +import com.firebase.uidemo.util.LifecycleActivity; +import com.firebase.uidemo.util.SignInResultNotifier; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.CollectionReference; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.Query; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * Class demonstrating how to setup a {@link RecyclerView} with an adapter while taking sign-in + * states into consideration. Also demonstrates adding data to a ref and then reading it back using + * the {@link FirestoreRecyclerAdapter} to build a simple chat app. + *

+ * For a general intro to the RecyclerView, see Creating + * Lists. + */ +public class FirestoreChatActivity extends LifecycleActivity + implements FirebaseAuth.AuthStateListener { + private static final String TAG = "FirestoreChatActivity"; + + private static final CollectionReference sChatCollection = + FirebaseFirestore.getInstance().collection("chats"); + /** Get the last 50 chat messages ordered by timestamp . */ + private static final Query sChatQuery = sChatCollection.orderBy("timestamp").limit(50); + + static { + FirebaseFirestore.setLoggingEnabled(true); + } + + @BindView(R.id.messagesList) + RecyclerView mRecyclerView; + + @BindView(R.id.sendButton) + Button mSendButton; + + @BindView(R.id.messageEdit) + EditText mMessageEdit; + + @BindView(R.id.emptyTextView) + TextView mEmptyListMessage; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_chat); + ButterKnife.bind(this); + + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + + ImeHelper.setImeOnDoneListener(mMessageEdit, new ImeHelper.DonePressedListener() { + @Override + public void onDonePressed() { + onSendClick(); + } + }); + } + + @Override + public void onStart() { + super.onStart(); + if (isSignedIn()) { attachRecyclerViewAdapter(); } + FirebaseAuth.getInstance().addAuthStateListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + FirebaseAuth.getInstance().removeAuthStateListener(this); + } + + @Override + public void onAuthStateChanged(@NonNull FirebaseAuth auth) { + mSendButton.setEnabled(isSignedIn()); + mMessageEdit.setEnabled(isSignedIn()); + + if (isSignedIn()) { + attachRecyclerViewAdapter(); + } else { + Toast.makeText(this, R.string.signing_in, Toast.LENGTH_SHORT).show(); + auth.signInAnonymously().addOnCompleteListener(new SignInResultNotifier(this)); + } + } + + private boolean isSignedIn() { + return FirebaseAuth.getInstance().getCurrentUser() != null; + } + + private void attachRecyclerViewAdapter() { + final RecyclerView.Adapter adapter = newAdapter(); + + // Scroll to bottom on new messages + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + mRecyclerView.smoothScrollToPosition(adapter.getItemCount()); + } + }); + + mRecyclerView.setAdapter(adapter); + } + + @OnClick(R.id.sendButton) + public void onSendClick() { + String uid = FirebaseAuth.getInstance().getCurrentUser().getUid(); + String name = "User " + uid.substring(0, 6); + + onAddMessage(new Chat(name, mMessageEdit.getText().toString(), uid)); + + mMessageEdit.setText(""); + } + + protected RecyclerView.Adapter newAdapter() { + FirestoreRecyclerOptions options = + new FirestoreRecyclerOptions.Builder() + .setQuery(sChatQuery, Chat.class) + .setLifecycleOwner(this) + .build(); + + return new FirestoreRecyclerAdapter(options) { + @Override + public ChatHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ChatHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.message, parent, false)); + } + + @Override + protected void onBindViewHolder(ChatHolder holder, int position, Chat model) { + holder.bind(model); + } + + @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); + } + }; + } + + protected void onAddMessage(Chat chat) { + sChatCollection.add(chat).addOnFailureListener(this, new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.e(TAG, "Failed to write message", e); + } + }); + } +} diff --git a/app/src/main/java/com/firebase/uidemo/database/Chat.java b/app/src/main/java/com/firebase/uidemo/database/realtime/Chat.java similarity index 75% rename from app/src/main/java/com/firebase/uidemo/database/Chat.java rename to app/src/main/java/com/firebase/uidemo/database/realtime/Chat.java index c4fc6efdb..3e37eb98a 100644 --- a/app/src/main/java/com/firebase/uidemo/database/Chat.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/Chat.java @@ -1,6 +1,11 @@ -package com.firebase.uidemo.database; +package com.firebase.uidemo.database.realtime; + +import com.firebase.uidemo.database.AbstractChat; +import com.google.firebase.database.IgnoreExtraProperties; + +@IgnoreExtraProperties +public class Chat extends AbstractChat { -public class Chat { private String mName; private String mMessage; private String mUid; diff --git a/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatActivity.java new file mode 100644 index 000000000..131990997 --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatActivity.java @@ -0,0 +1,171 @@ +package com.firebase.uidemo.database.realtime; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.firebase.ui.auth.ui.ImeHelper; +import com.firebase.ui.database.FirebaseRecyclerAdapter; +import com.firebase.ui.database.FirebaseRecyclerOptions; +import com.firebase.uidemo.R; +import com.firebase.uidemo.database.ChatHolder; +import com.firebase.uidemo.util.LifecycleActivity; +import com.firebase.uidemo.util.SignInResultNotifier; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.Query; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * Class demonstrating how to setup a {@link RecyclerView} with an adapter while taking sign-in + * states into consideration. Also demonstrates adding data to a ref and then reading it back using + * the {@link FirebaseRecyclerAdapter} to build a simple chat app. + *

+ * For a general intro to the RecyclerView, see Creating + * Lists. + */ +public class RealtimeDbChatActivity extends LifecycleActivity + implements FirebaseAuth.AuthStateListener { + private static final String TAG = "RealtimeDatabaseDemo"; + + /** + * Get the last 50 chat messages. + */ + protected static final Query sChatQuery = + FirebaseDatabase.getInstance().getReference().child("chats").limitToLast(50); + + @BindView(R.id.messagesList) + RecyclerView mRecyclerView; + + @BindView(R.id.sendButton) + Button mSendButton; + + @BindView(R.id.messageEdit) + EditText mMessageEdit; + + @BindView(R.id.emptyTextView) + TextView mEmptyListMessage; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_chat); + ButterKnife.bind(this); + + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + + ImeHelper.setImeOnDoneListener(mMessageEdit, new ImeHelper.DonePressedListener() { + @Override + public void onDonePressed() { + onSendClick(); + } + }); + } + + @Override + public void onStart() { + super.onStart(); + if (isSignedIn()) { attachRecyclerViewAdapter(); } + FirebaseAuth.getInstance().addAuthStateListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + FirebaseAuth.getInstance().removeAuthStateListener(this); + } + + @Override + public void onAuthStateChanged(@NonNull FirebaseAuth auth) { + mSendButton.setEnabled(isSignedIn()); + mMessageEdit.setEnabled(isSignedIn()); + + if (isSignedIn()) { + attachRecyclerViewAdapter(); + } else { + Toast.makeText(this, R.string.signing_in, Toast.LENGTH_SHORT).show(); + auth.signInAnonymously().addOnCompleteListener(new SignInResultNotifier(this)); + } + } + + private boolean isSignedIn() { + return FirebaseAuth.getInstance().getCurrentUser() != null; + } + + private void attachRecyclerViewAdapter() { + final RecyclerView.Adapter adapter = newAdapter(); + + // Scroll to bottom on new messages + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + mRecyclerView.smoothScrollToPosition(adapter.getItemCount()); + } + }); + + mRecyclerView.setAdapter(adapter); + } + + @OnClick(R.id.sendButton) + public void onSendClick() { + String uid = FirebaseAuth.getInstance().getCurrentUser().getUid(); + String name = "User " + uid.substring(0, 6); + + onAddMessage(new Chat(name, mMessageEdit.getText().toString(), uid)); + + mMessageEdit.setText(""); + } + + protected RecyclerView.Adapter newAdapter() { + FirebaseRecyclerOptions options = + new FirebaseRecyclerOptions.Builder() + .setQuery(sChatQuery, Chat.class) + .setLifecycleOwner(this) + .build(); + + return new FirebaseRecyclerAdapter(options) { + @Override + public ChatHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ChatHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.message, parent, false)); + } + + @Override + protected void onBindViewHolder(ChatHolder holder, int position, Chat model) { + holder.bind(model); + } + + @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); + } + }; + } + + protected void onAddMessage(Chat chat) { + sChatQuery.getRef().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()); + } + } + }); + } +} diff --git a/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatIndexActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatIndexActivity.java new file mode 100644 index 000000000..b6c57498c --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatIndexActivity.java @@ -0,0 +1,58 @@ +package com.firebase.uidemo.database.realtime; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.firebase.ui.database.FirebaseRecyclerAdapter; +import com.firebase.ui.database.FirebaseRecyclerOptions; +import com.firebase.uidemo.R; +import com.firebase.uidemo.database.ChatHolder; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; + +public class RealtimeDbChatIndexActivity extends RealtimeDbChatActivity { + private DatabaseReference mChatIndicesRef; + + @Override + protected FirebaseRecyclerAdapter newAdapter() { + mChatIndicesRef = FirebaseDatabase.getInstance() + .getReference() + .child("chatIndices") + .child(FirebaseAuth.getInstance().getCurrentUser().getUid()); + + FirebaseRecyclerOptions options = + new FirebaseRecyclerOptions.Builder() + .setIndexedQuery( + mChatIndicesRef.limitToFirst(50), sChatQuery.getRef(), Chat.class) + .setLifecycleOwner(this) + .build(); + + return new FirebaseRecyclerAdapter(options) { + @Override + public ChatHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ChatHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.message, parent, false)); + } + + @Override + protected void onBindViewHolder(ChatHolder holder, int position, Chat model) { + holder.bind(model); + } + + @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); + } + }; + } + + @Override + protected void onAddMessage(Chat chat) { + DatabaseReference chatRef = sChatQuery.getRef().push(); + mChatIndicesRef.child(chatRef.getKey()).setValue(true); + chatRef.setValue(chat); + } +} diff --git a/app/src/main/java/com/firebase/uidemo/storage/ImageActivity.java b/app/src/main/java/com/firebase/uidemo/storage/ImageActivity.java index 6f39bf783..71949baf8 100644 --- a/app/src/main/java/com/firebase/uidemo/storage/ImageActivity.java +++ b/app/src/main/java/com/firebase/uidemo/storage/ImageActivity.java @@ -13,8 +13,7 @@ import android.widget.ImageView; import android.widget.Toast; -import com.bumptech.glide.Glide; -import com.firebase.ui.storage.images.FirebaseImageLoader; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.firebase.uidemo.R; import com.firebase.uidemo.util.SignInResultNotifier; import com.google.android.gms.tasks.OnFailureListener; @@ -131,11 +130,11 @@ public void onFailure(@NonNull Exception e) { @OnClick(R.id.button_download_direct) protected void downloadDirect() { // Download directly from StorageReference using Glide - Glide.with(this) - .using(new FirebaseImageLoader()) + // (See MyAppGlideModule for Loader registration) + GlideApp.with(this) .load(mImageRef) .centerCrop() - .crossFade() + .transition(DrawableTransitionOptions.withCrossFade()) .into(mImageView); } diff --git a/app/src/main/java/com/firebase/uidemo/storage/MyAppGlideModule.java b/app/src/main/java/com/firebase/uidemo/storage/MyAppGlideModule.java new file mode 100644 index 000000000..ccbffbbf8 --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/storage/MyAppGlideModule.java @@ -0,0 +1,28 @@ +package com.firebase.uidemo.storage; + +import android.content.Context; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; +import com.firebase.ui.storage.images.FirebaseImageLoader; +import com.google.firebase.storage.StorageReference; + +import java.io.InputStream; + +/** + * Glide module to register {@link com.firebase.ui.storage.images.FirebaseImageLoader}. + * See: http://bumptech.github.io/glide/doc/generatedapi.html + */ +@GlideModule +public class MyAppGlideModule extends AppGlideModule { + + @Override + public void registerComponents(Context context, Glide glide, Registry registry) { + // Register FirebaseImageLoader to handle StorageReference + registry.append(StorageReference.class, InputStream.class, + new FirebaseImageLoader.Factory()); + } + +} diff --git a/app/src/main/java/com/firebase/uidemo/util/LifecycleActivity.java b/app/src/main/java/com/firebase/uidemo/util/LifecycleActivity.java new file mode 100644 index 000000000..808cb071d --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/util/LifecycleActivity.java @@ -0,0 +1,16 @@ +package com.firebase.uidemo.util; + +import android.arch.lifecycle.LifecycleRegistry; +import android.arch.lifecycle.LifecycleRegistryOwner; +import android.support.v7.app.AppCompatActivity; + +// TODO remove once arch components are merged into support lib +@SuppressWarnings("Registered") +public class LifecycleActivity extends AppCompatActivity implements LifecycleRegistryOwner { + private final LifecycleRegistry mRegistry = new LifecycleRegistry(this); + + @Override + public LifecycleRegistry getLifecycle() { + return mRegistry; + } +} diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 7c0171e08..fc6a78ae4 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".database.ChatActivity"> + tools:context=".database.realtime.RealtimeDbChatActivity">