diff --git a/app/build.gradle b/app/build.gradle index c6c1e10..d53a6dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' - - +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { compileSdkVersion 25 @@ -22,10 +23,25 @@ android { } } +kotlin { + experimental { + coroutines 'enable' + } +} + dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.0.+' - compile 'com.android.support:recyclerview-v7:25.0.+' + compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:recyclerview-v7:25.3.1' + + kapt 'com.android.databinding:compiler:3.0.0-alpha6' + + compile 'org.jetbrains.anko:anko-commons:0.10.1' + compile 'org.jetbrains.anko:anko-sdk25:0.10.1' + compile 'org.jetbrains.anko:anko-sdk25-coroutines:0.10.1' + + compile "org.jetbrains.kotlin:kotlin-stdlib:1.1.3-2" + compile ('io.socket:socket.io-client:0.8.3') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/ChatApplication.java b/app/src/main/java/com/github/nkzawa/socketio/androidchat/ChatApplication.java deleted file mode 100644 index ea62589..0000000 --- a/app/src/main/java/com/github/nkzawa/socketio/androidchat/ChatApplication.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.nkzawa.socketio.androidchat; - -import android.app.Application; -import io.socket.client.IO; -import io.socket.client.Socket; - -import java.net.URISyntaxException; - -public class ChatApplication extends Application { - - private Socket mSocket; - { - try { - mSocket = IO.socket(Constants.CHAT_SERVER_URL); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - public Socket getSocket() { - return mSocket; - } -} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/ChatApplication.kt b/app/src/main/java/com/github/nkzawa/socketio/androidchat/ChatApplication.kt new file mode 100644 index 0000000..b099d85 --- /dev/null +++ b/app/src/main/java/com/github/nkzawa/socketio/androidchat/ChatApplication.kt @@ -0,0 +1,17 @@ +package com.github.nkzawa.socketio.androidchat + +import android.app.Application +import io.socket.client.IO +import io.socket.client.Socket + +import java.net.URISyntaxException + +class ChatApplication : Application() { + + var socket: Socket = try { + IO.socket(Constants.CHAT_SERVER_URL) + } catch (e: URISyntaxException) { + throw RuntimeException(e) + } + +} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/Constants.java b/app/src/main/java/com/github/nkzawa/socketio/androidchat/Constants.java deleted file mode 100644 index 84b0ba0..0000000 --- a/app/src/main/java/com/github/nkzawa/socketio/androidchat/Constants.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.nkzawa.socketio.androidchat; - -public class Constants { - public static final String CHAT_SERVER_URL = "https://socketio-chat.now.sh/"; -} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/Constants.kt b/app/src/main/java/com/github/nkzawa/socketio/androidchat/Constants.kt new file mode 100644 index 0000000..c810076 --- /dev/null +++ b/app/src/main/java/com/github/nkzawa/socketio/androidchat/Constants.kt @@ -0,0 +1,5 @@ +package com.github.nkzawa.socketio.androidchat + +object Constants { + const val CHAT_SERVER_URL = "https://socketio-chat.now.sh/" +} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/LoginActivity.java b/app/src/main/java/com/github/nkzawa/socketio/androidchat/LoginActivity.java deleted file mode 100644 index e3477c1..0000000 --- a/app/src/main/java/com/github/nkzawa/socketio/androidchat/LoginActivity.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.github.nkzawa.socketio.androidchat; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.inputmethod.EditorInfo; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; -import io.socket.client.Socket; -import io.socket.emitter.Emitter; -import org.json.JSONException; -import org.json.JSONObject; - - -/** - * A login screen that offers login via username. - */ -public class LoginActivity extends Activity { - - private EditText mUsernameView; - - private String mUsername; - - private Socket mSocket; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_login); - - ChatApplication app = (ChatApplication) getApplication(); - mSocket = app.getSocket(); - - // Set up the login form. - mUsernameView = (EditText) findViewById(R.id.username_input); - mUsernameView.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { - if (id == R.id.login || id == EditorInfo.IME_NULL) { - attemptLogin(); - return true; - } - return false; - } - }); - - Button signInButton = (Button) findViewById(R.id.sign_in_button); - signInButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - attemptLogin(); - } - }); - - mSocket.on("login", onLogin); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - mSocket.off("login", onLogin); - } - - /** - * Attempts to sign in the account specified by the login form. - * If there are form errors (invalid username, missing fields, etc.), the - * errors are presented and no actual login attempt is made. - */ - private void attemptLogin() { - // Reset errors. - mUsernameView.setError(null); - - // Store values at the time of the login attempt. - String username = mUsernameView.getText().toString().trim(); - - // Check for a valid username. - if (TextUtils.isEmpty(username)) { - // There was an error; don't attempt login and focus the first - // form field with an error. - mUsernameView.setError(getString(R.string.error_field_required)); - mUsernameView.requestFocus(); - return; - } - - mUsername = username; - - // perform the user login attempt. - mSocket.emit("add user", username); - } - - private Emitter.Listener onLogin = new Emitter.Listener() { - @Override - public void call(Object... args) { - JSONObject data = (JSONObject) args[0]; - - int numUsers; - try { - numUsers = data.getInt("numUsers"); - } catch (JSONException e) { - return; - } - - Intent intent = new Intent(); - intent.putExtra("username", mUsername); - intent.putExtra("numUsers", numUsers); - setResult(RESULT_OK, intent); - finish(); - } - }; -} - - - diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/LoginActivity.kt b/app/src/main/java/com/github/nkzawa/socketio/androidchat/LoginActivity.kt new file mode 100644 index 0000000..1ba31e4 --- /dev/null +++ b/app/src/main/java/com/github/nkzawa/socketio/androidchat/LoginActivity.kt @@ -0,0 +1,103 @@ +package com.github.nkzawa.socketio.androidchat + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.KeyEvent +import android.view.View +import android.view.View.OnClickListener +import android.view.inputmethod.EditorInfo +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import io.socket.client.Socket +import io.socket.emitter.Emitter +import kotlinx.android.synthetic.main.activity_login.* +import org.jetbrains.anko.sdk25.coroutines.onClick +import org.jetbrains.anko.sdk25.coroutines.onEditorAction +import org.json.JSONException +import org.json.JSONObject + + +/** + * A login screen that offers login via username. + */ +class LoginActivity : Activity() { + + private var userName: String? = null + + private lateinit var socket: Socket + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + val app = application as ChatApplication + socket = app.socket + + // Set up the login form. + username_input.onEditorAction(returnValue = true) { v, actionId, event -> + if (actionId == R.id.login || actionId == EditorInfo.IME_NULL) { + attemptLogin() + } + } + + sign_in_button.onClick { attemptLogin() } + + socket.on("login", onLogin) + } + + override fun onDestroy() { + super.onDestroy() + + socket.off("login", onLogin) + } + + /** + * Attempts to sign in the account specified by the login form. + * If there are form errors (invalid username, missing fields, etc.), the + * errors are presented and no actual login attempt is made. + */ + private fun attemptLogin() { + // Reset errors. + username_input.error = null + + // Store values at the time of the login attempt. + val username = username_input.text.toString().trim() + + // Check for a valid username. + if (username.isNullOrBlank()) { + // There was an error; don't attempt login and focus the first + // form field with an error. + username_input.error = getString(R.string.error_field_required) + username_input.requestFocus() + return + } + + userName = username + + // perform the user login attempt. + socket.emit("add user", username) + } + + private val onLogin = Emitter.Listener { args -> + val data = args[0] as JSONObject + + val numUsers: Int + try { + numUsers = data.getInt("numUsers") + } catch (e: JSONException) { + return@Listener + } + + val intent = Intent() + intent.putExtra("username", userName) + intent.putExtra("numUsers", numUsers) + setResult(Activity.RESULT_OK, intent) + finish() + } +} + + + diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainActivity.java b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainActivity.java deleted file mode 100644 index a5e010c..0000000 --- a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainActivity.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.nkzawa.socketio.androidchat; - -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; - - -public class MainActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_main); - } -} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainActivity.kt b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainActivity.kt new file mode 100644 index 0000000..c290be0 --- /dev/null +++ b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainActivity.kt @@ -0,0 +1,12 @@ +package com.github.nkzawa.socketio.androidchat + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainFragment.java b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainFragment.java deleted file mode 100644 index 4060468..0000000 --- a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainFragment.java +++ /dev/null @@ -1,453 +0,0 @@ -package com.github.nkzawa.socketio.androidchat; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.support.v4.app.Fragment; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.TextView; -import android.widget.Toast; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -import io.socket.client.Socket; -import io.socket.emitter.Emitter; - - -/** - * A chat fragment containing messages view and input form. - */ -public class MainFragment extends Fragment { - - private static final String TAG = "MainFragment"; - - private static final int REQUEST_LOGIN = 0; - - private static final int TYPING_TIMER_LENGTH = 600; - - private RecyclerView mMessagesView; - private EditText mInputMessageView; - private List mMessages = new ArrayList(); - private RecyclerView.Adapter mAdapter; - private boolean mTyping = false; - private Handler mTypingHandler = new Handler(); - private String mUsername; - private Socket mSocket; - - private Boolean isConnected = true; - - public MainFragment() { - super(); - } - - - // This event fires 1st, before creation of fragment or any views - // The onAttach method is called when the Fragment instance is associated with an Activity. - // This does not mean the Activity is fully initialized. - @Override - public void onAttach(Context context) { - super.onAttach(context); - mAdapter = new MessageAdapter(context, mMessages); - if (context instanceof Activity){ - //this.listener = (MainActivity) context; - } - } - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setHasOptionsMenu(true); - - ChatApplication app = (ChatApplication) getActivity().getApplication(); - mSocket = app.getSocket(); - mSocket.on(Socket.EVENT_CONNECT,onConnect); - mSocket.on(Socket.EVENT_DISCONNECT,onDisconnect); - mSocket.on(Socket.EVENT_CONNECT_ERROR, onConnectError); - mSocket.on(Socket.EVENT_CONNECT_TIMEOUT, onConnectError); - mSocket.on("new message", onNewMessage); - mSocket.on("user joined", onUserJoined); - mSocket.on("user left", onUserLeft); - mSocket.on("typing", onTyping); - mSocket.on("stop typing", onStopTyping); - mSocket.connect(); - - startSignIn(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_main, container, false); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - mSocket.disconnect(); - - mSocket.off(Socket.EVENT_CONNECT, onConnect); - mSocket.off(Socket.EVENT_DISCONNECT, onDisconnect); - mSocket.off(Socket.EVENT_CONNECT_ERROR, onConnectError); - mSocket.off(Socket.EVENT_CONNECT_TIMEOUT, onConnectError); - mSocket.off("new message", onNewMessage); - mSocket.off("user joined", onUserJoined); - mSocket.off("user left", onUserLeft); - mSocket.off("typing", onTyping); - mSocket.off("stop typing", onStopTyping); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - mMessagesView = (RecyclerView) view.findViewById(R.id.messages); - mMessagesView.setLayoutManager(new LinearLayoutManager(getActivity())); - mMessagesView.setAdapter(mAdapter); - - mInputMessageView = (EditText) view.findViewById(R.id.message_input); - mInputMessageView.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int id, KeyEvent event) { - if (id == R.id.send || id == EditorInfo.IME_NULL) { - attemptSend(); - return true; - } - return false; - } - }); - mInputMessageView.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (null == mUsername) return; - if (!mSocket.connected()) return; - - if (!mTyping) { - mTyping = true; - mSocket.emit("typing"); - } - - mTypingHandler.removeCallbacks(onTypingTimeout); - mTypingHandler.postDelayed(onTypingTimeout, TYPING_TIMER_LENGTH); - } - - @Override - public void afterTextChanged(Editable s) { - } - }); - - ImageButton sendButton = (ImageButton) view.findViewById(R.id.send_button); - sendButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - attemptSend(); - } - }); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (Activity.RESULT_OK != resultCode) { - getActivity().finish(); - return; - } - - mUsername = data.getStringExtra("username"); - int numUsers = data.getIntExtra("numUsers", 1); - - addLog(getResources().getString(R.string.message_welcome)); - addParticipantsLog(numUsers); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - // Inflate the menu; this adds items to the action bar if it is present. - inflater.inflate(R.menu.menu_main, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_leave) { - leave(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - private void addLog(String message) { - mMessages.add(new Message.Builder(Message.TYPE_LOG) - .message(message).build()); - mAdapter.notifyItemInserted(mMessages.size() - 1); - scrollToBottom(); - } - - private void addParticipantsLog(int numUsers) { - addLog(getResources().getQuantityString(R.plurals.message_participants, numUsers, numUsers)); - } - - private void addMessage(String username, String message) { - mMessages.add(new Message.Builder(Message.TYPE_MESSAGE) - .username(username).message(message).build()); - mAdapter.notifyItemInserted(mMessages.size() - 1); - scrollToBottom(); - } - - private void addTyping(String username) { - mMessages.add(new Message.Builder(Message.TYPE_ACTION) - .username(username).build()); - mAdapter.notifyItemInserted(mMessages.size() - 1); - scrollToBottom(); - } - - private void removeTyping(String username) { - for (int i = mMessages.size() - 1; i >= 0; i--) { - Message message = mMessages.get(i); - if (message.getType() == Message.TYPE_ACTION && message.getUsername().equals(username)) { - mMessages.remove(i); - mAdapter.notifyItemRemoved(i); - } - } - } - - private void attemptSend() { - if (null == mUsername) return; - if (!mSocket.connected()) return; - - mTyping = false; - - String message = mInputMessageView.getText().toString().trim(); - if (TextUtils.isEmpty(message)) { - mInputMessageView.requestFocus(); - return; - } - - mInputMessageView.setText(""); - addMessage(mUsername, message); - - // perform the sending message attempt. - mSocket.emit("new message", message); - } - - private void startSignIn() { - mUsername = null; - Intent intent = new Intent(getActivity(), LoginActivity.class); - startActivityForResult(intent, REQUEST_LOGIN); - } - - private void leave() { - mUsername = null; - mSocket.disconnect(); - mSocket.connect(); - startSignIn(); - } - - private void scrollToBottom() { - mMessagesView.scrollToPosition(mAdapter.getItemCount() - 1); - } - - private Emitter.Listener onConnect = new Emitter.Listener() { - @Override - public void call(Object... args) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - if(!isConnected) { - if(null!=mUsername) - mSocket.emit("add user", mUsername); - Toast.makeText(getActivity().getApplicationContext(), - R.string.connect, Toast.LENGTH_LONG).show(); - isConnected = true; - } - } - }); - } - }; - - private Emitter.Listener onDisconnect = new Emitter.Listener() { - @Override - public void call(Object... args) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - Log.i(TAG, "diconnected"); - isConnected = false; - Toast.makeText(getActivity().getApplicationContext(), - R.string.disconnect, Toast.LENGTH_LONG).show(); - } - }); - } - }; - - private Emitter.Listener onConnectError = new Emitter.Listener() { - @Override - public void call(Object... args) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - Log.e(TAG, "Error connecting"); - Toast.makeText(getActivity().getApplicationContext(), - R.string.error_connect, Toast.LENGTH_LONG).show(); - } - }); - } - }; - - private Emitter.Listener onNewMessage = new Emitter.Listener() { - @Override - public void call(final Object... args) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - JSONObject data = (JSONObject) args[0]; - String username; - String message; - try { - username = data.getString("username"); - message = data.getString("message"); - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); - return; - } - - removeTyping(username); - addMessage(username, message); - } - }); - } - }; - - private Emitter.Listener onUserJoined = new Emitter.Listener() { - @Override - public void call(final Object... args) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - JSONObject data = (JSONObject) args[0]; - String username; - int numUsers; - try { - username = data.getString("username"); - numUsers = data.getInt("numUsers"); - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); - return; - } - - addLog(getResources().getString(R.string.message_user_joined, username)); - addParticipantsLog(numUsers); - } - }); - } - }; - - private Emitter.Listener onUserLeft = new Emitter.Listener() { - @Override - public void call(final Object... args) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - JSONObject data = (JSONObject) args[0]; - String username; - int numUsers; - try { - username = data.getString("username"); - numUsers = data.getInt("numUsers"); - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); - return; - } - - addLog(getResources().getString(R.string.message_user_left, username)); - addParticipantsLog(numUsers); - removeTyping(username); - } - }); - } - }; - - private Emitter.Listener onTyping = new Emitter.Listener() { - @Override - public void call(final Object... args) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - JSONObject data = (JSONObject) args[0]; - String username; - try { - username = data.getString("username"); - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); - return; - } - addTyping(username); - } - }); - } - }; - - private Emitter.Listener onStopTyping = new Emitter.Listener() { - @Override - public void call(final Object... args) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - JSONObject data = (JSONObject) args[0]; - String username; - try { - username = data.getString("username"); - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); - return; - } - removeTyping(username); - } - }); - } - }; - - private Runnable onTypingTimeout = new Runnable() { - @Override - public void run() { - if (!mTyping) return; - - mTyping = false; - mSocket.emit("stop typing"); - } - }; -} - diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainFragment.kt b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainFragment.kt new file mode 100644 index 0000000..68ae7a7 --- /dev/null +++ b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MainFragment.kt @@ -0,0 +1,378 @@ +package com.github.nkzawa.socketio.androidchat + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.support.v4.app.Fragment +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.ImageButton +import android.widget.TextView +import android.widget.Toast + +import org.json.JSONException +import org.json.JSONObject + +import java.util.ArrayList + +import io.socket.client.Socket +import io.socket.emitter.Emitter +import kotlinx.android.synthetic.main.fragment_main.* +import kotlinx.android.synthetic.main.item_action.* +import org.jetbrains.anko.custom.onUiThread +import org.jetbrains.anko.runOnUiThread +import org.jetbrains.anko.sdk25.coroutines.onClick +import org.jetbrains.anko.sdk25.coroutines.onEditorAction +import org.jetbrains.anko.sdk25.coroutines.textChangedListener + + +/** + * A chat fragment containing messages view and input form. + */ +class MainFragment : Fragment() { + + companion object { + + private val TAG = "MainFragment" + + private val REQUEST_LOGIN = 0 + + private val TYPING_TIMER_LENGTH = 600 + } + + private val messageList = ArrayList() + + private var typing = false + private val typingHandler = Handler() + private var userName: String? = null + + private lateinit var socket: Socket + + private lateinit var adapter: RecyclerView.Adapter<*> + + private var isConnected: Boolean = true + + // This event fires 1st, before creation of fragment or any views + // The onAttach method is called when the Fragment instance is associated with an Activity. + // This does not mean the Activity is fully initialized. + override fun onAttach(context: Context?) { + super.onAttach(context) + adapter = MessageAdapter(context!!, messageList) + if (context is Activity) { + //this.listener = (MainActivity) context; + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setHasOptionsMenu(true) + + val app = activity.application as ChatApplication + socket = app.socket + socket.on(Socket.EVENT_CONNECT, onConnect) + socket.on(Socket.EVENT_DISCONNECT, onDisconnect) + socket.on(Socket.EVENT_CONNECT_ERROR, onConnectError) + socket.on(Socket.EVENT_CONNECT_TIMEOUT, onConnectError) + socket.on("new message", onNewMessage) + socket.on("user joined", onUserJoined) + socket.on("user left", onUserLeft) + socket.on("typing", onTyping) + socket.on("stop typing", onStopTyping) + socket.connect() + + startSignIn() + } + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater?.inflate(R.layout.fragment_main, container, false) + } + + override fun onDestroy() { + super.onDestroy() + + socket.disconnect() + + socket.off(Socket.EVENT_CONNECT, onConnect) + socket.off(Socket.EVENT_DISCONNECT, onDisconnect) + socket.off(Socket.EVENT_CONNECT_ERROR, onConnectError) + socket.off(Socket.EVENT_CONNECT_TIMEOUT, onConnectError) + socket.off("new message", onNewMessage) + socket.off("user joined", onUserJoined) + socket.off("user left", onUserLeft) + socket.off("typing", onTyping) + socket.off("stop typing", onStopTyping) + } + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + messages?.run { + layoutManager = LinearLayoutManager(activity) + adapter = this@MainFragment.adapter + } + + message_input.onEditorAction { v, actionId, event -> + if (id == R.id.send || id == EditorInfo.IME_NULL) { + attemptSend() + } + } + + message_input.textChangedListener { + onTextChanged { text, _, _, _ -> + if (userName == null) return@onTextChanged + if (!socket.connected()) return@onTextChanged + + if (!typing) { + typing = true + socket.emit("typing") + } + + typingHandler.removeCallbacks(onTypingTimeout) + typingHandler.postDelayed(onTypingTimeout, TYPING_TIMER_LENGTH.toLong()) + } + } + + send_button.onClick { attemptSend() } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (Activity.RESULT_OK != resultCode) { + activity.finish() + return + } + + userName = data!!.getStringExtra("username") + val numUsers = data.getIntExtra("numUsers", 1) + + addLog(resources.getString(R.string.message_welcome)) + addParticipantsLog(numUsers) + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + // Inflate the menu; this adds items to the action bar if it is present. + inflater!!.inflate(R.menu.menu_main, menu) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + val id = item!!.itemId + + + if (id == R.id.action_leave) { + leave() + return true + } + + return super.onOptionsItemSelected(item) + } + + private fun addLog(message: String) { + messageList.add(Message(Message.TYPE_LOG, message)) + adapter.notifyItemInserted(messageList.size - 1) + scrollToBottom() + } + + private fun addParticipantsLog(numUsers: Int) { + addLog(resources.getQuantityString(R.plurals.message_participants, numUsers, numUsers)) + } + + private fun addMessage(username: String, message: String) { + messageList.add(Message(Message.TYPE_MESSAGE, message, username)) + adapter.notifyItemInserted(messageList.size - 1) + scrollToBottom() + } + + private fun addTyping(username: String) { + messageList.add(Message(Message.TYPE_ACTION, username = username)) + adapter.notifyItemInserted(messageList.size - 1) + scrollToBottom() + } + + private fun removeTyping(username: String) { + for (i in messageList.indices.reversed()) { + val message = messageList[i] + if (message.type == Message.TYPE_ACTION && message.username == username) { + messageList.removeAt(i) + adapter.notifyItemRemoved(i) + } + } + } + + private fun attemptSend() { + if (userName == null) return + if (!socket.connected()) return + + typing = false + + val message = message_input.text.toString().trim() + if (message.isNullOrBlank()) { + message_input.requestFocus() + return + } + + message_input.setText("") + addMessage(userName!!, message) + + // perform the sending message attempt. + socket.emit("new message", message) + } + + private fun startSignIn() { + userName = null + val intent = Intent(activity, LoginActivity::class.java) + startActivityForResult(intent, REQUEST_LOGIN) + } + + private fun leave() { + userName = null + socket.disconnect() + socket.connect() + startSignIn() + } + + private fun scrollToBottom() { + messages.scrollToPosition(adapter.itemCount - 1) + } + + private val onConnect = Emitter.Listener { + activity.runOnUiThread { + if ((!isConnected)) { + if (null != userName) + socket.emit("add user", userName) + Toast.makeText(activity.applicationContext, + R.string.connect, Toast.LENGTH_LONG).show() + isConnected = true + } + } + } + + private val onDisconnect = Emitter.Listener { + activity.runOnUiThread { + Log.i(TAG, "diconnected") + isConnected = false + Toast.makeText(activity.applicationContext, + R.string.disconnect, Toast.LENGTH_LONG).show() + } + } + + private val onConnectError = Emitter.Listener { + activity.runOnUiThread { + Log.e(TAG, "Error connecting") + Toast.makeText(activity.applicationContext, + R.string.error_connect, Toast.LENGTH_LONG).show() + } + } + + private val onNewMessage = Emitter.Listener { args -> + activity.runOnUiThread(Runnable { + val data = args[0] as JSONObject + val username: String + val message: String + try { + username = data.getString("username") + message = data.getString("message") + } catch (e: JSONException) { + Log.e(TAG, e.message) + return@Runnable + } + + removeTyping(username) + addMessage(username, message) + }) + } + + private val onUserJoined = Emitter.Listener { args -> + activity.runOnUiThread(Runnable { + val data = args[0] as JSONObject + val username: String + val numUsers: Int + try { + username = data.getString("username") + numUsers = data.getInt("numUsers") + } catch (e: JSONException) { + Log.e(TAG, e.message) + return@Runnable + } + + addLog(resources.getString(R.string.message_user_joined, username)) + addParticipantsLog(numUsers) + }) + } + + private val onUserLeft = Emitter.Listener { args -> + activity.runOnUiThread(Runnable { + val data = args[0] as JSONObject + val username: String + val numUsers: Int + try { + username = data.getString("username") + numUsers = data.getInt("numUsers") + } catch (e: JSONException) { + Log.e(TAG, e.message) + return@Runnable + } + + addLog(resources.getString(R.string.message_user_left, username)) + addParticipantsLog(numUsers) + removeTyping(username) + }) + } + + private val onTyping = Emitter.Listener { args -> + activity.runOnUiThread(Runnable { + val data = args[0] as JSONObject + val username: String + try { + username = data.getString("username") + } catch (e: JSONException) { + Log.e(TAG, e.message) + return@Runnable + } + + addTyping(username) + }) + } + + private val onStopTyping = Emitter.Listener { args -> + activity.runOnUiThread(Runnable { + val data = args[0] as JSONObject + val username: String + try { + username = data.getString("username") + } catch (e: JSONException) { + Log.e(TAG, e.message) + return@Runnable + } + + removeTyping(username) + }) + } + + private val onTypingTimeout = Runnable { + if (!typing) return@Runnable + + typing = false + socket.emit("stop typing") + } + +} + diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/Message.java b/app/src/main/java/com/github/nkzawa/socketio/androidchat/Message.java deleted file mode 100644 index ac46996..0000000 --- a/app/src/main/java/com/github/nkzawa/socketio/androidchat/Message.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.nkzawa.socketio.androidchat; - -public class Message { - - public static final int TYPE_MESSAGE = 0; - public static final int TYPE_LOG = 1; - public static final int TYPE_ACTION = 2; - - private int mType; - private String mMessage; - private String mUsername; - - private Message() {} - - public int getType() { - return mType; - }; - - public String getMessage() { - return mMessage; - }; - - public String getUsername() { - return mUsername; - }; - - - public static class Builder { - private final int mType; - private String mUsername; - private String mMessage; - - public Builder(int type) { - mType = type; - } - - public Builder username(String username) { - mUsername = username; - return this; - } - - public Builder message(String message) { - mMessage = message; - return this; - } - - public Message build() { - Message message = new Message(); - message.mType = mType; - message.mUsername = mUsername; - message.mMessage = mMessage; - return message; - } - } -} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/Message.kt b/app/src/main/java/com/github/nkzawa/socketio/androidchat/Message.kt new file mode 100644 index 0000000..4b3225c --- /dev/null +++ b/app/src/main/java/com/github/nkzawa/socketio/androidchat/Message.kt @@ -0,0 +1,11 @@ +package com.github.nkzawa.socketio.androidchat + +data class Message(var type: Int = 0, + var message: String? = null, + var username: String? = null) { + companion object { + const val TYPE_MESSAGE = 0 + const val TYPE_LOG = 1 + const val TYPE_ACTION = 2 + } +} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MessageAdapter.java b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MessageAdapter.java deleted file mode 100644 index 7521a39..0000000 --- a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MessageAdapter.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.github.nkzawa.socketio.androidchat; - -import android.content.Context; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import java.util.List; - - -public class MessageAdapter extends RecyclerView.Adapter { - - private List mMessages; - private int[] mUsernameColors; - - public MessageAdapter(Context context, List messages) { - mMessages = messages; - mUsernameColors = context.getResources().getIntArray(R.array.username_colors); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - int layout = -1; - switch (viewType) { - case Message.TYPE_MESSAGE: - layout = R.layout.item_message; - break; - case Message.TYPE_LOG: - layout = R.layout.item_log; - break; - case Message.TYPE_ACTION: - layout = R.layout.item_action; - break; - } - View v = LayoutInflater - .from(parent.getContext()) - .inflate(layout, parent, false); - return new ViewHolder(v); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, int position) { - Message message = mMessages.get(position); - viewHolder.setMessage(message.getMessage()); - viewHolder.setUsername(message.getUsername()); - } - - @Override - public int getItemCount() { - return mMessages.size(); - } - - @Override - public int getItemViewType(int position) { - return mMessages.get(position).getType(); - } - - public class ViewHolder extends RecyclerView.ViewHolder { - private TextView mUsernameView; - private TextView mMessageView; - - public ViewHolder(View itemView) { - super(itemView); - - mUsernameView = (TextView) itemView.findViewById(R.id.username); - mMessageView = (TextView) itemView.findViewById(R.id.message); - } - - public void setUsername(String username) { - if (null == mUsernameView) return; - mUsernameView.setText(username); - mUsernameView.setTextColor(getUsernameColor(username)); - } - - public void setMessage(String message) { - if (null == mMessageView) return; - mMessageView.setText(message); - } - - private int getUsernameColor(String username) { - int hash = 7; - for (int i = 0, len = username.length(); i < len; i++) { - hash = username.codePointAt(i) + (hash << 5) - hash; - } - int index = Math.abs(hash % mUsernameColors.length); - return mUsernameColors[index]; - } - } -} diff --git a/app/src/main/java/com/github/nkzawa/socketio/androidchat/MessageAdapter.kt b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MessageAdapter.kt new file mode 100644 index 0000000..adf2c95 --- /dev/null +++ b/app/src/main/java/com/github/nkzawa/socketio/androidchat/MessageAdapter.kt @@ -0,0 +1,69 @@ +package com.github.nkzawa.socketio.androidchat + +import android.content.Context +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView + +class MessageAdapter(context: Context, private val messageList: List) + : RecyclerView.Adapter() { + + private val usernameColors: IntArray = context.resources.getIntArray(R.array.username_colors) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val layout = when (viewType) { + Message.TYPE_MESSAGE -> R.layout.item_message + Message.TYPE_LOG -> R.layout.item_log + Message.TYPE_ACTION -> R.layout.item_action + else -> -1 + } + val v = LayoutInflater.from(parent.context).inflate(layout, parent, false) + return ViewHolder(v) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val message = messageList[position] + viewHolder.setMessage(message.message ?: "") + viewHolder.setUsername(message.username ?: "") + } + + override fun getItemCount(): Int { + return messageList.size + } + + override fun getItemViewType(position: Int): Int { + return messageList[position].type + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val usernameView: TextView? + private val messageView: TextView? + + init { + usernameView = itemView.findViewById(R.id.username) as TextView + messageView = itemView.findViewById(R.id.message) as TextView + } + + fun setUsername(username: String) { + usernameView?.run { + text = username + setTextColor(getUsernameColor(username)) + } + } + + fun setMessage(message: String) { + messageView?.text = message + } + + private fun getUsernameColor(username: String): Int { + var hash = 7 + for (i in 0..username.length - 1) { + hash = username.codePointAt(i) + (hash shl 5) - hash + } + val index = Math.abs(hash % usernameColors.size) + return usernameColors[index] + } + } +} diff --git a/build.gradle b/build.gradle index 16f9930..2a08df3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,16 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.pluginVersion = '3.0.0-alpha6' + ext.kotlinVersion = '1.1.3-2' + repositories { + maven { url 'https://maven.google.com' } jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0-beta4' + classpath "com.android.tools.build:gradle:$pluginVersion" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,5 +20,6 @@ buildscript { allprojects { repositories { jcenter() + maven { url 'https://maven.google.com' } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2a61779..7bcdc9d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 20 09:34:25 PST 2017 +#Sat Aug 12 18:31:02 KST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-milestone-1-all.zip