diff --git a/app/build.gradle b/app/build.gradle index 113f182b5..35a0938ab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,4 +40,16 @@ dependencies { // Displaying images compile 'com.github.bumptech.glide:glide:3.6.1' -} \ No newline at end of file + + // Firebase + compile 'com.google.firebase:firebase-database:9.6.0' + compile 'com.google.firebase:firebase-auth:9.6.0' + compile 'com.google.firebase:firebase-storage:9.6.0' + compile 'com.google.firebase:firebase-messaging:9.6.0' + compile 'com.google.firebase:firebase-config:9.6.0' + + // FirebaseUI + compile 'com.firebaseui:firebase-ui-auth:0.6.0' +} + +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/src/main/java/com/google/firebase/udacity/friendlychat/MainActivity.java b/app/src/main/java/com/google/firebase/udacity/friendlychat/MainActivity.java index b9b486cd1..5a3d0ae37 100644 --- a/app/src/main/java/com/google/firebase/udacity/friendlychat/MainActivity.java +++ b/app/src/main/java/com/google/firebase/udacity/friendlychat/MainActivity.java @@ -15,11 +15,15 @@ */ package com.google.firebase.udacity.friendlychat; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.text.Editable; import android.text.InputFilter; import android.text.TextWatcher; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -29,9 +33,28 @@ import android.widget.ImageButton; import android.widget.ListView; import android.widget.ProgressBar; +import android.widget.Toast; + +import com.firebase.ui.auth.AuthUI; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.remoteconfig.FirebaseRemoteConfig; +import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings; +import com.google.firebase.storage.FirebaseStorage; +import com.google.firebase.storage.StorageReference; +import com.google.firebase.storage.UploadTask; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class MainActivity extends AppCompatActivity { @@ -39,6 +62,10 @@ public class MainActivity extends AppCompatActivity { public static final String ANONYMOUS = "anonymous"; public static final int DEFAULT_MSG_LENGTH_LIMIT = 1000; + public static final String FRIENDLY_MSG_LENGTH_KEY = "friendly_msg_length"; + + public static final int RC_SIGN_IN = 1; + private static final int RC_PHOTO_PICKER = 2; private ListView mMessageListView; private MessageAdapter mMessageAdapter; @@ -49,6 +76,16 @@ public class MainActivity extends AppCompatActivity { private String mUsername; + // Firebase instance variables + private FirebaseDatabase mFirebaseDatabase; + private DatabaseReference mMessagesDatabaseReference; + private ChildEventListener mChildEventListener; + private FirebaseAuth mFirebaseAuth; + private FirebaseAuth.AuthStateListener mAuthStateListener; + private FirebaseStorage mFirebaseStorage; + private StorageReference mChatPhotosStorageReference; + private FirebaseRemoteConfig mFirebaseRemoteConfig; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -56,6 +93,15 @@ protected void onCreate(Bundle savedInstanceState) { mUsername = ANONYMOUS; + // Initialize Firebase components + mFirebaseDatabase = FirebaseDatabase.getInstance(); + mFirebaseAuth = FirebaseAuth.getInstance(); + mFirebaseStorage = FirebaseStorage.getInstance(); + mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance(); + + mMessagesDatabaseReference = mFirebaseDatabase.getReference().child("messages"); + mChatPhotosStorageReference = mFirebaseStorage.getReference().child("chat_photos"); + // Initialize references to views mProgressBar = (ProgressBar) findViewById(R.id.progressBar); mMessageListView = (ListView) findViewById(R.id.messageListView); @@ -75,7 +121,10 @@ protected void onCreate(Bundle savedInstanceState) { mPhotoPickerButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - // TODO: Fire an intent to show an image picker + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/jpeg"); + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + startActivityForResult(Intent.createChooser(intent, "Complete action using"), RC_PHOTO_PICKER); } }); @@ -104,12 +153,101 @@ public void afterTextChanged(Editable editable) { mSendButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - // TODO: Send messages on click + FriendlyMessage friendlyMessage = new FriendlyMessage(mMessageEditText.getText().toString(), mUsername, null); + mMessagesDatabaseReference.push().setValue(friendlyMessage); // Clear input box mMessageEditText.setText(""); } }); + + mAuthStateListener = new FirebaseAuth.AuthStateListener() { + @Override + public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { + FirebaseUser user = firebaseAuth.getCurrentUser(); + if (user != null) { + // User is signed in + onSignedInInitialize(user.getDisplayName()); + } else { + // User is signed out + onSignedOutCleanup(); + startActivityForResult( + AuthUI.getInstance() + .createSignInIntentBuilder() + .setIsSmartLockEnabled(false) + .setProviders( + AuthUI.EMAIL_PROVIDER, + AuthUI.GOOGLE_PROVIDER) + .build(), + RC_SIGN_IN); + } + } + }; + + // Create Remote Config Setting to enable developer mode. + // Fetching configs from the server is normally limited to 5 requests per hour. + // Enabling developer mode allows many more requests to be made per hour, so developers + // can test different config values during development. + FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder() + .setDeveloperModeEnabled(BuildConfig.DEBUG) + .build(); + mFirebaseRemoteConfig.setConfigSettings(configSettings); + + // Define default config values. Defaults are used when fetched config values are not + // available. Eg: if an error occurred fetching values from the server. + Map defaultConfigMap = new HashMap<>(); + defaultConfigMap.put(FRIENDLY_MSG_LENGTH_KEY, DEFAULT_MSG_LENGTH_LIMIT); + mFirebaseRemoteConfig.setDefaults(defaultConfigMap); + fetchConfig(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == RC_SIGN_IN) { + if (resultCode == RESULT_OK) { + // Sign-in succeeded, set up the UI + Toast.makeText(this, "Signed in!", Toast.LENGTH_SHORT).show(); + } else if (resultCode == RESULT_CANCELED) { + // Sign in was canceled by the user, finish the activity + Toast.makeText(this, "Sign in canceled", Toast.LENGTH_SHORT).show(); + finish(); + } + } else if (requestCode == RC_PHOTO_PICKER && resultCode == RESULT_OK) { + Uri selectedImageUri = data.getData(); + + // Get a reference to store file at chat_photos/ + StorageReference photoRef = mChatPhotosStorageReference.child(selectedImageUri.getLastPathSegment()); + + // Upload file to Firebase Storage + photoRef.putFile(selectedImageUri) + .addOnSuccessListener(this, new OnSuccessListener() { + public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) { + // When the image has successfully uploaded, we get its download URL + Uri downloadUrl = taskSnapshot.getDownloadUrl(); + + // Set the download URL to the message box, so that the user can send it to the database + FriendlyMessage friendlyMessage = new FriendlyMessage(null, mUsername, downloadUrl.toString()); + mMessagesDatabaseReference.push().setValue(friendlyMessage); + } + }); + } + } + + @Override + protected void onResume() { + super.onResume(); + mFirebaseAuth.addAuthStateListener(mAuthStateListener); + } + + @Override + protected void onPause() { + super.onPause(); + if (mAuthStateListener != null) { + mFirebaseAuth.removeAuthStateListener(mAuthStateListener); + } + mMessageAdapter.clear(); + detachDatabaseReadListener(); } @Override @@ -121,6 +259,92 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - return super.onOptionsItemSelected(item); + switch (item.getItemId()) { + case R.id.sign_out_menu: + AuthUI.getInstance().signOut(this); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void onSignedInInitialize(String username) { + mUsername = username; + attachDatabaseReadListener(); + } + + private void onSignedOutCleanup() { + mUsername = ANONYMOUS; + mMessageAdapter.clear(); + detachDatabaseReadListener(); + } + + private void attachDatabaseReadListener() { + if (mChildEventListener == null) { + mChildEventListener = new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot dataSnapshot, String s) { + FriendlyMessage friendlyMessage = dataSnapshot.getValue(FriendlyMessage.class); + mMessageAdapter.add(friendlyMessage); + } + + public void onChildChanged(DataSnapshot dataSnapshot, String s) {} + public void onChildRemoved(DataSnapshot dataSnapshot) {} + public void onChildMoved(DataSnapshot dataSnapshot, String s) {} + public void onCancelled(DatabaseError databaseError) {} + }; + mMessagesDatabaseReference.addChildEventListener(mChildEventListener); + } + } + + private void detachDatabaseReadListener() { + if (mChildEventListener != null) { + mMessagesDatabaseReference.removeEventListener(mChildEventListener); + mChildEventListener = null; + } + } + + // Fetch the config to determine the allowed length of messages. + public void fetchConfig() { + long cacheExpiration = 3600; // 1 hour in seconds + // If developer mode is enabled reduce cacheExpiration to 0 so that each fetch goes to the + // server. This should not be used in release builds. + if (mFirebaseRemoteConfig.getInfo().getConfigSettings().isDeveloperModeEnabled()) { + cacheExpiration = 0; + } + mFirebaseRemoteConfig.fetch(cacheExpiration) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void aVoid) { + // Make the fetched config available + // via FirebaseRemoteConfig get calls, e.g., getLong, getString. + mFirebaseRemoteConfig.activateFetched(); + + // Update the EditText length limit with + // the newly retrieved values from Remote Config. + applyRetrievedLengthLimit(); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + // An error occurred when fetching the config. + Log.w(TAG, "Error fetching config", e); + + // Update the EditText length limit with + // the newly retrieved values from Remote Config. + applyRetrievedLengthLimit(); + } + }); + } + + /** + * Apply retrieved length limit to edit text field. This result may be fresh from the server or it may be from + * cached values. + */ + private void applyRetrievedLengthLimit() { + Long friendly_msg_length = mFirebaseRemoteConfig.getLong(FRIENDLY_MSG_LENGTH_KEY); + mMessageEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(friendly_msg_length.intValue())}); + Log.d(TAG, FRIENDLY_MSG_LENGTH_KEY + " = " + friendly_msg_length); } } diff --git a/build.gradle b/build.gradle index 67fa30cbc..f5a38fc74 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:2.2.2' + classpath 'com.google.gms:google-services:3.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files