diff --git a/MainActivity.java b/MainActivity.java new file mode 100644 index 000000000..ad85e5967 --- /dev/null +++ b/MainActivity.java @@ -0,0 +1,379 @@ +/** + * Copyright 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.google.firebase.udacity.friendlychat; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +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; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +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.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MainActivity extends AppCompatActivity { + + private static final String TAG = "MainActivity"; + + 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; + private ProgressBar mProgressBar; + private ImageButton mPhotoPickerButton; + private EditText mMessageEditText; + private Button mSendButton; + + 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; + private Uri downloadUrl; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + 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); + mPhotoPickerButton = (ImageButton) findViewById(R.id.photoPickerButton); + mMessageEditText = (EditText) findViewById(R.id.messageEditText); + mSendButton = (Button) findViewById(R.id.sendButton); + + // Initialize message ListView and its adapter + List friendlyMessages = new ArrayList<>(); + mMessageAdapter = new MessageAdapter(this, R.layout.item_message, friendlyMessages); + mMessageListView.setAdapter(mMessageAdapter); + + // Initialize provider list + List providers; + providers = new ArrayList<>(); + + // Initialize progress bar + mProgressBar.setVisibility(ProgressBar.INVISIBLE); + + // ImagePickerButton shows an image picker to upload a image for a message + mPhotoPickerButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + 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); + } + }); + + // Enable Send button when there's text to send + mMessageEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + if (charSequence.toString().trim().length() > 0) { + mSendButton.setEnabled(true); + } else { + mSendButton.setEnabled(false); + } + } + + @Override + public void afterTextChanged(Editable editable) { + } + }); + mMessageEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(DEFAULT_MSG_LENGTH_LIMIT)}); + + // Send button sends a message and clears the EditText + mSendButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + 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 + // deprecated method .setProviders has been replaced follwing example code + // https://github.com/firebase/FirebaseUI-Android/tree/master/auth#adding-providers + onSignedOutCleanup(); + startActivityForResult( + AuthUI.getInstance() + .createSignInIntentBuilder() + .setIsSmartLockEnabled(false) + .setAvailableProviders(Arrays.asList( + new AuthUI.IdpConfig.GoogleBuilder().build(), + new AuthUI.IdpConfig.EmailBuilder().build())) + .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/ + final 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 + // DONE fix deprecated method error .getDownloadUrl() + // .getDownloadUrl() method is deprecated + // Original example code- + // Uri downloadUrl = taskSnapshot.getDownloadUrl(); + // Code snippet from stackoverflow- + // https://stackoverflow.com/a/50743192/8811523 + + photoRef.getDownloadUrl().addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Uri uri) {downloadUrl = uri;} + }); + + // 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 + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem 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 f5a38fc74..67d4c6805 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,14 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() mavenLocal() + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.2' - classpath 'com.google.gms:google-services:3.0.0' + classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.google.gms:google-services:4.2.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -18,6 +19,7 @@ allprojects { repositories { jcenter() mavenLocal() + google() } }