package com.github.h01d.chatapp.activities; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.annotation.NonNull; import android.support.v4.app.NavUtils; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.github.h01d.chatapp.R; import com.github.h01d.chatapp.adapters.MessageAdapter; import com.github.h01d.chatapp.models.Message; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.auth.FirebaseAuth; 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.database.Query; import com.google.firebase.database.ServerValue; import com.google.firebase.database.ValueEventListener; 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; import java.util.Timer; import java.util.TimerTask; /** * This is a part of ChatApp Project (https://github.com/h01d/ChatApp) * Licensed under Apache License 2.0 * * @author Raf (https://github.com/h01d) * @version 1.1 * @since 27/02/2018 */ public class ChatActivity extends AppCompatActivity { private final String TAG = "CA/ChatActivity"; // Will handle all changes happening in database private DatabaseReference userDatabase, chatDatabase; private ValueEventListener userListener, chatListener; // Will handle old/new messages between users private Query messagesDatabase; private ChildEventListener messagesListener; private MessageAdapter messagesAdapter; private final List<Message> messagesList = new ArrayList<>(); // User data private String currentUserId; // activity_chat views private EditText messageEditText; private RecyclerView recyclerView; private Button sendButton; private ImageView sendPictureButton; // chat_bar views private TextView appBarName, appBarSeen; // Will be used on Notifications to detairminate if user has chat window open public static String otherUserId; public static boolean running = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_chat); running = true; messageEditText = findViewById(R.id.chat_message); recyclerView = findViewById(R.id.chat_recycler); currentUserId = FirebaseAuth.getInstance().getCurrentUser().getUid(); otherUserId = getIntent().getStringExtra("userid"); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(this)); messagesAdapter = new MessageAdapter(messagesList); recyclerView.setAdapter(messagesAdapter); // Action bar related ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowCustomEnabled(true); actionBar.setTitle(""); LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View actionBarView = inflater.inflate(R.layout.chat_bar, null); appBarName = actionBarView.findViewById(R.id.chat_bar_name); appBarSeen = actionBarView.findViewById(R.id.chat_bar_seen); actionBar.setCustomView(actionBarView); // Will handle the send button to send a message sendButton = findViewById(R.id.chat_send); sendButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { sendMessage(); } }); sendPictureButton = findViewById(R.id.chat_send_picture); sendPictureButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent galleryIntent = new Intent(); galleryIntent.setType("image/*"); galleryIntent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(galleryIntent, "Select Image"), 1); } }); // Will handle typing feature, 0 means no typing, 1 typing, 2 deleting and 3 thinking (5+ sec delay) messageEditText.addTextChangedListener(new TextWatcher() { private Timer timer = new Timer(); @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(messagesList.size() > 0) { if(charSequence.length() == 0) { FirebaseDatabase.getInstance().getReference().child("Chat").child(currentUserId).child(otherUserId).child("typing").setValue(0); timer.cancel(); } else if(i2 > 0) { FirebaseDatabase.getInstance().getReference().child("Chat").child(currentUserId).child(otherUserId).child("typing").setValue(1); timer.cancel(); timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { FirebaseDatabase.getInstance().getReference().child("Chat").child(currentUserId).child(otherUserId).child("typing").setValue(3); } }, 5000); } else if(i1 > 0) { FirebaseDatabase.getInstance().getReference().child("Chat").child(currentUserId).child(otherUserId).child("typing").setValue(2); timer.cancel(); timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { FirebaseDatabase.getInstance().getReference().child("Chat").child(currentUserId).child(otherUserId).child("typing").setValue(3); } }, 5000); } } } @Override public void afterTextChanged(Editable editable) { } }); // Checking if root layout changed to detect soft keyboard final RelativeLayout root = findViewById(R.id.chat_root); root.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { int previousHeight = root.getRootView().getHeight() - root.getHeight() - recyclerView.getHeight(); @Override public void onGlobalLayout() { int height = root.getRootView().getHeight() - root.getHeight() - recyclerView.getHeight(); if(previousHeight != height) { if(previousHeight > height) { previousHeight = height; } else if(previousHeight < height) { recyclerView.scrollToPosition(messagesList.size() - 1); previousHeight = height; } } } }); } @Override protected void onResume() { super.onResume(); running = true; FirebaseDatabase.getInstance().getReference().child("Users").child(currentUserId).child("online").setValue("true"); loadMessages(); initDatabases(); } @Override protected void onPause() { super.onPause(); running = false; FirebaseDatabase.getInstance().getReference().child("Users").child(currentUserId).child("online").setValue(ServerValue.TIMESTAMP); if(messagesList.size() > 0 && messageEditText.getText().length() > 0) { FirebaseDatabase.getInstance().getReference().child("Chat").child(currentUserId).child(otherUserId).child("typing").setValue(0); } removeListeners(); } @Override public void onBackPressed() { NavUtils.navigateUpFromSameTask(this); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case android.R.id.home: NavUtils.navigateUpFromSameTask(this); break; } return super.onOptionsItemSelected(item); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if(requestCode == 1 && resultCode == RESULT_OK) { Uri url = data.getData(); DatabaseReference messageRef = FirebaseDatabase.getInstance().getReference().child("Messages").child(currentUserId).child(otherUserId).push(); final String messageId = messageRef.getKey(); DatabaseReference notificationRef = FirebaseDatabase.getInstance().getReference().child("Notifications").child(otherUserId).push(); final String notificationId = notificationRef.getKey(); StorageReference file = FirebaseStorage.getInstance().getReference().child("message_images").child(messageId + ".jpg"); file.putFile(url).addOnCompleteListener(new OnCompleteListener<UploadTask.TaskSnapshot>() { @Override public void onComplete(@NonNull Task<UploadTask.TaskSnapshot> task) { if(task.isSuccessful()) { String imageUrl = task.getResult().getDownloadUrl().toString(); Map messageMap = new HashMap(); messageMap.put("message", imageUrl); messageMap.put("type", "image"); messageMap.put("from", currentUserId); messageMap.put("to", otherUserId); messageMap.put("timestamp", ServerValue.TIMESTAMP); HashMap<String, String> notificationData = new HashMap<>(); notificationData.put("from", currentUserId); notificationData.put("type", "message"); Map userMap = new HashMap(); userMap.put("Messages/" + currentUserId + "/" + otherUserId + "/" + messageId, messageMap); userMap.put("Messages/" + otherUserId + "/" + currentUserId + "/" + messageId, messageMap); userMap.put("Chat/" + currentUserId + "/" + otherUserId + "/message", "You have sent a picture."); userMap.put("Chat/" + currentUserId + "/" + otherUserId + "/timestamp", ServerValue.TIMESTAMP); userMap.put("Chat/" + currentUserId + "/" + otherUserId + "/seen", ServerValue.TIMESTAMP); userMap.put("Chat/" + otherUserId + "/" + currentUserId + "/message", "Has send you a picture."); userMap.put("Chat/" + otherUserId + "/" + currentUserId + "/timestamp", ServerValue.TIMESTAMP); userMap.put("Chat/" + otherUserId + "/" + currentUserId + "/seen", 0); userMap.put("Notifications/" + otherUserId + "/" + notificationId, notificationData); FirebaseDatabase.getInstance().getReference().updateChildren(userMap, new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { sendButton.setEnabled(true); if(databaseError != null) { Log.d(TAG, "sendMessage(): updateChildren failed: " + databaseError.getMessage()); } } }); } } }); } } private void initDatabases() { // Initialize/Update realtime other user data such as name and online status userDatabase = FirebaseDatabase.getInstance().getReference().child("Users").child(otherUserId); userListener = new ValueEventListener() { Timer timer; @Override public void onDataChange(DataSnapshot dataSnapshot) { try { String name = dataSnapshot.child("name").getValue().toString(); appBarName.setText(name); final String online = dataSnapshot.child("online").getValue().toString(); if(online.equals("true")) { if(timer != null) { timer.cancel(); timer = null; } appBarSeen.setText("Online"); } else { if(appBarSeen.getText().length() == 0) { appBarSeen.setText("Last Seen: " + getTimeAgo(Long.parseLong(online))); } else { timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { ChatActivity.this.runOnUiThread(new Runnable() { @Override public void run() { appBarSeen.setText("Last Seen: " + getTimeAgo(Long.parseLong(online))); } }); } }, 2000); } } } catch(Exception e) { Log.d(TAG, "setDatabase(): usersOtherUserListener exception: " + e.getMessage()); } } @Override public void onCancelled(DatabaseError databaseError) { Log.d(TAG, "setDatabase(): usersOtherUserListener failed: " + databaseError.getMessage()); } }; userDatabase.addValueEventListener(userListener); //Check if last message is unseen and mark it as seen with current timestamp chatDatabase = FirebaseDatabase.getInstance().getReference().child("Chat").child(currentUserId).child(otherUserId); chatListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { try { if(dataSnapshot.hasChild("seen")) { long seen = (long) dataSnapshot.child("seen").getValue(); if(seen == 0) { chatDatabase.child("seen").setValue(ServerValue.TIMESTAMP); } } } catch(Exception e) { Log.d(TAG, "setDatabase(): chatCurrentUserListener exception: " + e.getMessage()); e.printStackTrace(); } } @Override public void onCancelled(DatabaseError databaseError) { Log.d(TAG, "setDatabase(): chatCurrentUserListener failed: " + databaseError.getMessage()); } }; chatDatabase.addValueEventListener(chatListener); } private void loadMessages() { messagesList.clear(); // Load/Update all messages between current and other user messagesDatabase = FirebaseDatabase.getInstance().getReference().child("Messages").child(currentUserId).child(otherUserId); messagesListener = new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String s) { try { Message message = dataSnapshot.getValue(Message.class); messagesList.add(message); messagesAdapter.notifyDataSetChanged(); recyclerView.scrollToPosition(messagesList.size() - 1); } catch(Exception e) { Log.d(TAG, "loadMessages(): messegesListener exception: " + e.getMessage()); } } @Override public void onChildChanged(DataSnapshot dataSnapshot, String s) { messagesAdapter.notifyDataSetChanged(); } @Override public void onChildRemoved(DataSnapshot dataSnapshot) { messagesAdapter.notifyDataSetChanged(); } @Override public void onChildMoved(DataSnapshot dataSnapshot, String s) { messagesAdapter.notifyDataSetChanged(); } @Override public void onCancelled(DatabaseError databaseError) { Log.d(TAG, "loadMessages(): messegesListener failed: " + databaseError.getMessage()); } }; messagesDatabase.addChildEventListener(messagesListener); } private void removeListeners() { try { chatDatabase.removeEventListener(chatListener); chatListener = null; userDatabase.removeEventListener(userListener); userListener = null; messagesDatabase.removeEventListener(messagesListener); messagesListener = null; } catch(Exception e) { Log.d(TAG, "exception: " + e.getMessage()); e.printStackTrace(); } } private void sendMessage() { sendButton.setEnabled(false); String message = messageEditText.getText().toString(); if(message.length() == 0) { Toast.makeText(getApplicationContext(), "Message cannot be empty", Toast.LENGTH_SHORT).show(); sendButton.setEnabled(true); } else { messageEditText.setText(""); // Pushing message/notification so we can get keyIds DatabaseReference userMessage = FirebaseDatabase.getInstance().getReference().child("Messages").child(currentUserId).child(otherUserId).push(); String pushId = userMessage.getKey(); DatabaseReference notificationRef = FirebaseDatabase.getInstance().getReference().child("Notifications").child(otherUserId).push(); String notificationId = notificationRef.getKey(); // "Packing" message Map messageMap = new HashMap(); messageMap.put("message", message); messageMap.put("type", "text"); messageMap.put("from", currentUserId); messageMap.put("to", otherUserId); messageMap.put("timestamp", ServerValue.TIMESTAMP); HashMap<String, String> notificationData = new HashMap<>(); notificationData.put("from", currentUserId); notificationData.put("type", "message"); Map userMap = new HashMap(); userMap.put("Messages/" + currentUserId + "/" + otherUserId + "/" + pushId, messageMap); userMap.put("Messages/" + otherUserId + "/" + currentUserId + "/" + pushId, messageMap); userMap.put("Chat/" + currentUserId + "/" + otherUserId + "/message", message); userMap.put("Chat/" + currentUserId + "/" + otherUserId + "/timestamp", ServerValue.TIMESTAMP); userMap.put("Chat/" + currentUserId + "/" + otherUserId + "/seen", ServerValue.TIMESTAMP); userMap.put("Chat/" + otherUserId + "/" + currentUserId + "/message", message); userMap.put("Chat/" + otherUserId + "/" + currentUserId + "/timestamp", ServerValue.TIMESTAMP); userMap.put("Chat/" + otherUserId + "/" + currentUserId + "/seen", 0); userMap.put("Notifications/" + otherUserId + "/" + notificationId, notificationData); // Updating database with the new data including message, chat and notification FirebaseDatabase.getInstance().getReference().updateChildren(userMap, new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { sendButton.setEnabled(true); if(databaseError != null) { Log.d(TAG, "sendMessage(): updateChildren failed: " + databaseError.getMessage()); } } }); } } private String getTimeAgo(long time) { final long diff = System.currentTimeMillis() - time; if(diff < 1) { return " just now"; } if(diff < 60 * 1000) { if(diff / 1000 < 2) { return diff / 1000 + " second ago"; } else { return diff / 1000 + " seconds ago"; } } else if(diff < 60 * (60 * 1000)) { if(diff / (60 * 1000) < 2) { return diff / (60 * 1000) + " minute ago"; } else { return diff / (60 * 1000) + " minutes ago"; } } else if(diff < 24 * (60 * (60 * 1000))) { if(diff / (60 * (60 * 1000)) < 2) { return diff / (60 * (60 * 1000)) + " hour ago"; } else { return diff / (60 * (60 * 1000)) + " hours ago"; } } else { if(diff / (24 * (60 * (60 * 1000))) < 2) { return diff / (24 * (60 * (60 * 1000))) + " day ago"; } else { return diff / (24 * (60 * (60 * 1000))) + " days ago"; } } } }