package de.tum.in.tumcampusapp.activities;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import com.google.common.net.UrlEscapers;
import com.google.gson.Gson;
import java.io.IOException;
import java.text.DateFormat;
import java.util.List;
import de.tum.in.tumcampusapp.R;
import de.tum.in.tumcampusapp.adapters.ChatHistoryAdapter;
import de.tum.in.tumcampusapp.api.TUMCabeClient;
import de.tum.in.tumcampusapp.auxiliary.ChatMessageValidator;
import de.tum.in.tumcampusapp.auxiliary.Const;
import de.tum.in.tumcampusapp.auxiliary.ImplicitCounter;
import de.tum.in.tumcampusapp.auxiliary.NetUtils;
import de.tum.in.tumcampusapp.auxiliary.Utils;
import de.tum.in.tumcampusapp.exceptions.NoPrivateKey;
import de.tum.in.tumcampusapp.managers.CardManager;
import de.tum.in.tumcampusapp.managers.ChatMessageManager;
import de.tum.in.tumcampusapp.managers.ChatRoomManager;
import de.tum.in.tumcampusapp.models.gcm.GCMChat;
import de.tum.in.tumcampusapp.models.tumcabe.ChatMember;
import de.tum.in.tumcampusapp.models.tumcabe.ChatMessage;
import de.tum.in.tumcampusapp.models.tumcabe.ChatPublicKey;
import de.tum.in.tumcampusapp.models.tumcabe.ChatRoom;
import de.tum.in.tumcampusapp.models.tumcabe.ChatVerification;
import de.tum.in.tumcampusapp.services.SendMessageService;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Shows an ongoing chat conversation
* <p/>
* NEEDS: Const.CURRENT_CHAT_ROOM set in incoming bundle (json serialised object of class ChatRoom)
* Const.CURRENT_CHAT_MEMBER set in incoming bundle (json serialised object of class ChatMember)
*/
public class ChatActivity extends AppCompatActivity implements DialogInterface.OnClickListener, OnClickListener, AbsListView.OnScrollListener, AdapterView.OnItemLongClickListener {
// Key for the string that's delivered in the action's intent
public static final String EXTRA_VOICE_REPLY = "extra_voice_reply";
private static final int MAX_EDIT_TIMESPAN = 120000;
public static ChatRoom mCurrentOpenChatRoom;
private final Handler mUpdateHandler = new Handler();
/**
* UI elements
*/
private ListView lvMessageHistory;
private ChatHistoryAdapter chatHistoryAdapter;
private EditText etMessage;
private ImageButton btnSend;
private ProgressBar bar;
private ChatRoom currentChatRoom;
private ChatMember currentChatMember;
private boolean loadingMore;
private ActionMode mActionMode;
private ChatMessageManager chatManager;
private final ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
// Called when the action mode is created; startActionMode() was called
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.chat_context_menu, menu);
return true;
}
// Called each time the action mode is shown. Always called after onCreateActionMode, but may be called multiple times if the mode is invalidated.
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
ChatMessage msg = chatHistoryAdapter.mCheckedItem;
int i = item.getItemId();
if (i == R.id.action_edit) {// If item is not sent at the moment, stop sending
if (msg.getStatus() == ChatMessage.STATUS_SENDING) {
chatManager.removeFromUnsent(msg);
chatHistoryAdapter.removeUnsent(msg);
} else { // set editing item
chatHistoryAdapter.mEditedItem = msg;
}
// Show soft keyboard
InputMethodManager imm = (InputMethodManager) ChatActivity.this.getSystemService(Service.INPUT_METHOD_SERVICE);
imm.showSoftInput(etMessage, 0);
// Set text and set cursor to end of the text
etMessage.setText(msg.getText());
int position = msg.getText().length();
etMessage.setSelection(position);
mode.finish();
return true;
} else if (i == R.id.action_info) {
showInfo(msg);
mode.finish();
return true;
} else {
return false;
}
}
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
chatHistoryAdapter.mCheckedItem = null;
chatHistoryAdapter.notifyDataSetChanged();
}
};
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Utils.logv("Message send. Trying to parse...");
GCMChat extras = (GCMChat) intent.getSerializableExtra("GCMChat");
if (extras == null) {
return;
}
Utils.log("Broadcast receiver got room=" + extras.room + " member=" + extras.member);
handleRoomBroadcast(extras);
}
};
/**
* Gets the text from speech input and returns null if no input was provided
*/
private static CharSequence getMessageText(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(EXTRA_VOICE_REPLY);
}
return null;
}
private void handleRoomBroadcast(GCMChat extras) {
//If same room just refresh
if (!(extras.room == currentChatRoom.getId() && chatHistoryAdapter != null)) {
return;
}
if (extras.member == currentChatMember.getId()) {
// Remove this message from the adapter
chatHistoryAdapter.setUnsentMessages(chatManager.getAllUnsent());
} else if (extras.message == -1) {
//Check first, if sounds are enabled
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (am.getRingerMode() == AudioManager.RINGER_MODE_NORMAL) {
//Play a nice notification sound
MediaPlayer mediaPlayer = MediaPlayer.create(ChatActivity.this, R.raw.message);
mediaPlayer.start();
} else if (am.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { //Possibly only vibration is enabled
Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
v.vibrate(500);
}
}
//Update the history
chatHistoryAdapter.changeCursor(chatManager.getAll());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ImplicitCounter.count(this);
this.setContentView(R.layout.activity_chat);
Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
this.getIntentData();
this.bindUIElements();
}
@Override
protected void onResume() {
super.onResume();
getNextHistoryFromServer(true);
mCurrentOpenChatRoom = currentChatRoom;
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel((currentChatRoom.getId() << 4) + CardManager.CARD_CHAT);
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, new IntentFilter("chat-message-received"));
}
@Override
protected void onPause() {
super.onPause();
mCurrentOpenChatRoom = null;
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
}
/**
* User pressed on the notification and wants to view the room with the new messages
*
* @param intent Intent
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
//Try to get the room from the extras
final ChatRoom room = new Gson().fromJson(intent.getExtras().getString(Const.CURRENT_CHAT_ROOM), ChatRoom.class);
//Check, maybe it wasn't there
if (room != null && room.getId() != currentChatRoom.getId()) {
//If currently in a room which does not match the one from the notification --> Switch
currentChatRoom = room;
if (getSupportActionBar() != null) {
getSupportActionBar().setSubtitle(currentChatRoom.getName().substring(4));
}
chatHistoryAdapter = null;
chatManager = new ChatMessageManager(this, currentChatRoom.getId());
getNextHistoryFromServer(true);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu_activity_chat, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int i = item.getItemId();
if (i == R.id.action_add_chat_member) {
showQRCode();
return true;
} else if (i == R.id.action_leave_chat_room) {
new AlertDialog.Builder(this).setTitle(R.string.leave_chat_room)
.setMessage(getResources().getString(R.string.leave_chat_room_body))
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).create().show();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
private void showQRCode() {
String url = "http://chart.apis.google.com/chart?cht=qr&chs=500x500&chld=M&choe=UTF-8&chl=" +
UrlEscapers.urlPathSegmentEscaper().escape(currentChatRoom.getName());
final ImageView qrCode = new ImageView(this);
new NetUtils(this).loadAndSetImage(url, qrCode);
new AlertDialog.Builder(this)
.setTitle(R.string.add_chat_member)
.setView(qrCode)
.setPositiveButton(android.R.string.ok, null).show();
}
/**
* Handles clicks on send and load messages buttons
*
* @param view Handle of the button
*/
@Override
public void onClick(View view) {
// Create / send message
if (view.getId() == btnSend.getId()) {
//Check if something was entered
if (etMessage.getText().toString().isEmpty()) {
return;
}
this.sendMessage(etMessage.getText().toString());
//Set TextField to empty, when done
etMessage.setText("");
}
}
private void sendMessage(String text) {
if (chatHistoryAdapter.mEditedItem == null) {
final ChatMessage message = new ChatMessage(text, currentChatMember);
chatHistoryAdapter.add(message);
chatManager.addToUnsent(message);
} else {
chatHistoryAdapter.mEditedItem.setText(etMessage.getText().toString());
chatManager.addToUnsent(chatHistoryAdapter.mEditedItem);
chatHistoryAdapter.mEditedItem.setStatus(ChatMessage.STATUS_SENDING);
chatManager.replaceMessage(chatHistoryAdapter.mEditedItem);
chatHistoryAdapter.mEditedItem = null;
chatHistoryAdapter.changeCursor(chatManager.getAll());
}
// start service to send the message
startService(new Intent(this, SendMessageService.class));
}
/**
* Sets the actionbar title to the current chat room
*/
private void getIntentData() {
Bundle extras = getIntent().getExtras();
currentChatRoom = new Gson().fromJson(extras.getString(Const.CURRENT_CHAT_ROOM), ChatRoom.class);
currentChatMember = Utils.getSetting(this, Const.CHAT_MEMBER, ChatMember.class);
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(currentChatRoom.getName().substring(4));
}
chatManager = new ChatMessageManager(this, currentChatRoom.getId());
CharSequence message = getMessageText(getIntent());
if (message != null) {
sendMessage(message.toString());
}
}
/**
* Sets UI elements listeners
*/
private void bindUIElements() {
lvMessageHistory = (ListView) findViewById(R.id.lvMessageHistory);
lvMessageHistory.setOnItemLongClickListener(this);
lvMessageHistory.setOnScrollListener(this);
// Add the button for loading more messages to list header
bar = new ProgressBar(this);
lvMessageHistory.addHeaderView(bar);
lvMessageHistory.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
etMessage = (EditText) findViewById(R.id.etMessage);
btnSend = (ImageButton) findViewById(R.id.btnSend);
btnSend.setOnClickListener(this);
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// Noop
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//is the top item is visible & not loading more already ? Load more !
if (firstVisibleItem == 0 && !loadingMore && chatHistoryAdapter != null) {
getNextHistoryFromServer(false);
}
}
/**
* Loads older chat messages from the server and sets the adapter accordingly
*/
private void getNextHistoryFromServer(final boolean newMsg) {
loadingMore = true;
new Thread(new Runnable() {
@Override
public void run() {
// Download chat messages in new Thread
// If currently nothing has been shown load newest messages from server
ChatVerification verification;
try {
verification = new ChatVerification(ChatActivity.this, currentChatMember);
} catch (NoPrivateKey noPrivateKey) {
return; //In this case we simply cannot do anything
}
List<ChatMessage> downloadedChatHistory;
try {
if (chatHistoryAdapter == null || chatHistoryAdapter.getSentCount() == 0 || newMsg) {
downloadedChatHistory = TUMCabeClient.getInstance(ChatActivity.this).getNewMessages(currentChatRoom.getId(), verification);
} else {
long id = chatHistoryAdapter.getItemId(ChatMessageManager.COL_ID);
downloadedChatHistory = TUMCabeClient.getInstance(ChatActivity.this).getMessages(currentChatRoom.getId(), id, verification);
}
} catch (IOException e) {
Utils.log(e);
return;
}
//Save it to our local cache
chatManager.replaceInto(downloadedChatHistory);
// Got results from webservice
Utils.logv("Success loading additional chat history: " + downloadedChatHistory.size());
final Cursor cur = chatManager.getAll();
// Update results in UI
runOnUiThread(new Runnable() {
@Override
public void run() {
if (chatHistoryAdapter == null) {
chatHistoryAdapter = new ChatHistoryAdapter(ChatActivity.this, cur, currentChatMember);
lvMessageHistory.setAdapter(chatHistoryAdapter);
} else {
chatHistoryAdapter.changeCursor(cur);
chatHistoryAdapter.notifyDataSetChanged();
}
// If all messages are loaded hide header view
if ((cur.moveToFirst() && cur.getLong(ChatMessageManager.COL_PREVIOUS) == 0) || cur.getCount() == 0) {
lvMessageHistory.removeHeaderView(bar);
} else {
loadingMore = false;
}
}
});
}
}).start();
}
/**
* When user confirms the leave dialog send the request to the server
*
* @param dialog Dialog handle
* @param which The users choice (ignored because this is only called when the user confirms)
*/
@Override
public void onClick(DialogInterface dialog, int which) {
// Send request to the server to remove the user from this room
ChatVerification verification;
try {
verification = new ChatVerification(this, currentChatMember);
} catch (NoPrivateKey noPrivateKey) {
return;
}
TUMCabeClient.getInstance(this).leaveChatRoom(currentChatRoom, verification, new Callback<ChatRoom>() {
@Override
public void onResponse(Call<ChatRoom> call, Response<ChatRoom> room) {
Utils.logv("Success leaving chat room: " + room.body().getName());
new ChatRoomManager(ChatActivity.this).leave(currentChatRoom);
// Move back to ChatRoomsActivity
Intent intent = new Intent(ChatActivity.this, ChatRoomsActivity.class);
startActivity(intent);
}
@Override
public void onFailure(Call<ChatRoom> call, Throwable t) {
Utils.log(t, "Failure leaving chat room");
}
});
}
/**
* Validates chat message if long clicked on an item
*
* @param parent ListView
* @param view View of the selected message
* @param position Index of the selected view
* @param id Id of the selected item
* @return True if the method consumed the on long click event
*/
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, final int position, long id) {
if (mActionMode != null) {
return false;
}
//Calculate the proper position of the item without the header from pull to refresh
int positionActual = position - lvMessageHistory.getHeaderViewsCount();
//Get the correct message
ChatMessage message = (ChatMessage) chatHistoryAdapter.getItem(positionActual);
// If we are in a certain timespan and its the users own message allow editing
if ((System.currentTimeMillis() - message.getTimestampDate().getTime()) < ChatActivity.MAX_EDIT_TIMESPAN && message.getMember().getId() == currentChatMember.getId()) {
// Hide keyboard if opened
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(etMessage.getWindowToken(), 0);
// Start the CAB using the ActionMode.Callback defined above
mActionMode = this.startSupportActionMode(mActionModeCallback);
chatHistoryAdapter.mCheckedItem = message;
chatHistoryAdapter.notifyDataSetChanged();
} else {
this.showInfo(message);
}
return true;
}
private void showInfo(final ChatMessage message) {
//Verify the message with RSA
TUMCabeClient.getInstance(ChatActivity.this).getPublicKeysForMember(message.getMember(), new Callback<List<ChatPublicKey>>() {
@Override
public void onResponse(Call<List<ChatPublicKey>> call, Response<List<ChatPublicKey>> response) {
ChatMessageValidator validator = new ChatMessageValidator(response.body());
final boolean result = validator.validate(message);
//Show a nice dialog with more information about the message
String messageStr = String.format(getString(R.string.message_detail_text),
message.getMember().getDisplayName(),
message.getMember().getLrzId(),
DateFormat.getDateTimeInstance().format(message.getTimestampDate()),
getString(message.getStatusStringRes()),
getString(result ? R.string.valid : R.string.not_valid));
new AlertDialog.Builder(ChatActivity.this)
.setTitle(R.string.message_details)
.setMessage(Utils.fromHtml(messageStr))
.create().show();
}
@Override
public void onFailure(Call<List<ChatPublicKey>> call, Throwable t) {
Utils.log(t, "Failure verifying message");
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
mUpdateHandler.removeCallbacksAndMessages(null);
}
}