/***************************************************************************
* Copyright 2006-2016 by Christian Ihle *
* contact@kouchat.net *
* *
* This file is part of KouChat. *
* *
* KouChat is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 3 of *
* the License, or (at your option) any later version. *
* *
* KouChat is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with KouChat. *
* If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
package net.usikkert.kouchat.android.controller;
import net.usikkert.kouchat.android.R;
import net.usikkert.kouchat.android.chatwindow.AndroidUserInterface;
import net.usikkert.kouchat.android.component.AboutDialog;
import net.usikkert.kouchat.android.component.ComeBackDialog;
import net.usikkert.kouchat.android.component.GoAwayDialog;
import net.usikkert.kouchat.android.component.TopicDialog;
import net.usikkert.kouchat.android.service.ChatService;
import net.usikkert.kouchat.android.service.ChatServiceBinder;
import net.usikkert.kouchat.android.userlist.UserListAdapter;
import net.usikkert.kouchat.android.userlist.UserListAdapterWithChatState;
import net.usikkert.kouchat.event.UserListListener;
import net.usikkert.kouchat.misc.User;
import net.usikkert.kouchat.misc.UserList;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ScrollView;
import android.widget.TextView;
/**
* Controller for the main chat.
*
* <p>Life cycle:</p>
* <ul>
* <li>Application starts:
* <ul>
* <li>ChatService is created</li>
* <li>ChatService is bound</li>
* </ul>
* </li>
* <li>Application is hidden: ChatService is unbound</li>
* <li>Application is shown: ChatService is bound</li>
* <li>Application shuts down:
* <ul>
* <li>ChatService is unbound</li>
* <li>ChatService is stopped</li>
* </ul>
* </li>
* </ul>
*
* @author Christian Ihle
*/
public class MainChatController extends AppCompatActivity implements UserListListener {
private ControllerUtils controllerUtils = new ControllerUtils();
private Intent chatServiceIntent;
private ServiceConnection serviceConnection;
private EditText mainChatInput;
private ListView mainChatUserList;
private TextView mainChatView;
private ScrollView mainChatScroll;
private UserListAdapter userListAdapter;
private TextWatcher textWatcher;
private ActionBar actionBar;
private AndroidUserInterface androidUserInterface;
private UserList userList;
/** If the main chat is currently visible. */
private boolean visible;
/** If the main chat has been destroyed. */
private boolean destroyed;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_chat);
mainChatInput = (EditText) findViewById(R.id.mainChatInput);
mainChatUserList = (ListView) findViewById(R.id.mainChatUserList);
mainChatView = (TextView) findViewById(R.id.mainChatView);
mainChatScroll = (ScrollView) findViewById(R.id.mainChatScroll);
actionBar = getSupportActionBar();
registerMainChatInputListener();
registerMainChatTextListener();
registerUserListClickListener();
controllerUtils.makeLinksClickable(mainChatView);
setupMainChatUserList();
openKeyboard();
chatServiceIntent = createChatServiceIntent();
startService(chatServiceIntent);
serviceConnection = createServiceConnection();
bindService(chatServiceIntent, serviceConnection, Context.BIND_NOT_FOREGROUND);
}
private ServiceConnection createServiceConnection() {
return new ServiceConnection() {
@Override
public void onServiceConnected(final ComponentName componentName, final IBinder iBinder) {
final ChatServiceBinder binder = (ChatServiceBinder) iBinder;
androidUserInterface = binder.getAndroidUserInterface();
androidUserInterface.registerMainChatController(MainChatController.this);
androidUserInterface.resetAllMessageNotifications();
androidUserInterface.showTopic();
userList = androidUserInterface.getUserList();
userList.addUserListListener(MainChatController.this);
userListAdapter.addUsers(userList);
}
@Override
public void onServiceDisconnected(final ComponentName componentName) { }
};
}
private void registerMainChatInputListener() {
mainChatInput.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(final View v, final int keyCode, final KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) {
sendMessage(mainChatInput.getText().toString());
mainChatInput.setText("");
return true;
}
return false;
}
});
}
private void registerMainChatTextListener() {
textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
}
@Override
public void afterTextChanged(final Editable s) {
if (androidUserInterface != null) { // Might be null on orientation changes
androidUserInterface.updateMeWriting(!mainChatInput.getText().toString().isEmpty());
}
}
};
mainChatInput.addTextChangedListener(textWatcher);
}
private void registerUserListClickListener() {
mainChatUserList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(final AdapterView<?> userAdapter, final View view, final int position, final long id) {
final User selectedUser = (User) userAdapter.getItemAtPosition(position);
// No point in having a private chat with one self (at least not here)
if (selectedUser.isMe()) {
return;
}
final Intent privateChatIntent = new Intent(MainChatController.this, PrivateChatController.class);
privateChatIntent.putExtra("userCode", selectedUser.getCode());
startActivity(privateChatIntent);
}
});
}
private void setupMainChatUserList() {
userListAdapter = new UserListAdapterWithChatState(this);
mainChatUserList.setAdapter(userListAdapter);
}
private void openKeyboard() {
mainChatInput.requestFocus();
}
@Override
protected void onResume() {
super.onResume();
visible = true;
if (androidUserInterface != null) { // Is null during initial startup. Doesn't matter.
androidUserInterface.resetAllMessageNotifications();
}
}
@Override
protected void onPause() {
visible = false;
super.onPause();
}
@Override
protected void onDestroy() {
destroyed = true;
if (androidUserInterface != null) {
userList.removeUserListListener(this);
androidUserInterface.unregisterMainChatController(this);
unbindService(serviceConnection);
}
userListAdapter.onDestroy();
mainChatInput.removeTextChangedListener(textWatcher);
mainChatInput.setOnKeyListener(null);
mainChatUserList.setOnItemClickListener(null);
mainChatUserList.setAdapter(null);
controllerUtils.removeReferencesToTextViewFromText(mainChatView);
controllerUtils.removeReferencesToTextViewFromText(mainChatInput);
androidUserInterface = null;
userList = null;
controllerUtils = null;
chatServiceIntent = null;
serviceConnection = null;
mainChatInput = null;
mainChatUserList = null;
mainChatView = null;
mainChatScroll = null;
userListAdapter = null;
textWatcher = null;
actionBar = null;
super.onDestroy();
}
/**
* Creates the main chat menu.
*
* {@inheritDoc}
*
*/
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_chat_menu, menu);
return true;
}
/**
* Selects the actions to run after a menu item in the main chat has been selected.
*
* {@inheritDoc}
*/
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.mainChatMenuQuit:
return shutdownApplication();
case R.id.mainChatMenuAway:
return showAwayDialog();
case R.id.mainChatMenuTopic:
return showTopicDialog();
case R.id.mainChatMenuAbout:
return showAboutDialog();
case R.id.mainChatMenuSettings:
return showSettingsDialog();
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Makes sure regular key events from anywhere in the activity are sent to the input field,
* and giving it focus if it doesn't currently have focus.
*
* <p>Always asks the activity first, to make sure special keys are handled correctly, like the back button.</p>
*
* {@inheritDoc}
*/
@Override
public boolean dispatchKeyEvent(final KeyEvent event) {
if (super.dispatchKeyEvent(event)) {
return true;
}
if (!mainChatInput.hasFocus()) {
mainChatInput.requestFocus();
}
return mainChatInput.dispatchKeyEvent(event);
}
private boolean showAwayDialog() {
if (androidUserInterface.isAway()) {
new ComeBackDialog(this, androidUserInterface);
} else {
new GoAwayDialog(this, androidUserInterface);
}
return true;
}
private boolean showTopicDialog() {
new TopicDialog(this, androidUserInterface);
return true;
}
private boolean showAboutDialog() {
new AboutDialog(this);
return true;
}
private boolean shutdownApplication() {
finish();
stopService(chatServiceIntent);
return true;
}
private boolean showSettingsDialog() {
startActivity(new Intent(this, SettingsController.class));
return true;
}
private Intent createChatServiceIntent() {
return new Intent(this, ChatService.class);
}
public void appendToChat(final CharSequence message) {
runOnUiThread(new Runnable() {
public void run() {
if (destroyed) {
return; // If rotating fast, this activity could already be destroyed before this runs
}
mainChatView.append(message);
// Allow a way to avoid automatic scrolling to the bottom.
// Just scroll somewhere and click on the text to remove focus from the input field.
// Also fixes the annoying jumping scroll that happens sometimes.
if (mainChatInput.hasFocus()) {
controllerUtils.scrollTextViewToBottom(mainChatView, mainChatScroll);
}
}
});
}
protected void sendMessage(final String message) {
if (message != null && message.trim().length() > 0) {
androidUserInterface.sendMessage(message);
}
}
public void updateChat(final CharSequence savedChat) {
mainChatView.setText(savedChat);
// Run this after 1 second, because right after a rotate the layout is null and it's not possible to scroll yet
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// If rotating fast, this activity could already be destroyed before this runs
if (!destroyed) {
controllerUtils.scrollTextViewToBottom(mainChatView, mainChatScroll);
}
}
}, ControllerUtils.ONE_SECOND);
}
public void updateTitleAndSubtitle(final String title, final String subtitle) {
runOnUiThread(new Runnable() {
public void run() {
if (!destroyed) {
actionBar.setTitle(title);
actionBar.setSubtitle(subtitle);
}
}
});
}
@Override
public void userAdded(final int pos, final User user) {
runOnUiThread(new Runnable() {
public void run() {
if (!destroyed) {
userListAdapter.add(user);
}
}
});
}
@Override
public void userRemoved(final int pos, final User user) {
runOnUiThread(new Runnable() {
public void run() {
if (!destroyed) {
userListAdapter.remove(user);
}
}
});
}
@Override
public void userChanged(final int pos, final User user) {
runOnUiThread(new Runnable() {
public void run() {
if (!destroyed) {
userListAdapter.sort();
}
}
});
}
/**
* Returns if the main chat view is currently visible.
*
* @return If the view is visible.
*/
public boolean isVisible() {
return visible;
}
}