25 ตุลาคม 2555

[Android Code] การ Chat ผ่าน WLAN ด้วย TCP Socket


เรื่องนี้เดิมเจ้าของบล็อกเคยเขียนไว้รอบนึงล่ะ


แต่อันนั้นเป็นตัวอย่างที่เจ้าของบล็อกตั้งใจ
แยกส่วนของผู้รับและผู้ส่งออกจากกัน
เพื่อให้ผู้ที่หลงเข้ามาอ่านได้ศึกษาเข้าใจง่าย

แต่ในการใช้งานจริง ก็ควรทำหน้าแชทให้สามารถรับส่งได้เลย
ซึ่งจริงๆเจ้าของบล็อกทำเสร็จไว้นานล่ะ แต่ขี้เกียจเลยดองไว้นาน
วันนี้ถึงเวลาจะใช้งานมันจริงๆ ก็เลยเอามาลงบทความไปเลย


สำหรับตัวอย่างนี้ก็คล้ายๆของเดิม รับและส่งข้อมูลผ่าน WLAN
แต่เอามาแก้ไขใหม่ได้ใช้งานได้จริง และดูเหมือนแอพแชททั่วไป


Main.java
package app.akexorcist.tcpsocket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.app.Activity;
import android.content.SharedPreferences;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;

public class Main extends Activity {
    public static final int TCP_SERVER_PORT = 21111;
   
    TextView txtIP, txtStatus;
    ListView listChat;
    Button btnSend;
    EditText etxtIP, etxtMessage;
    
    ArrayList<String> arr_list;
    List<Integer> arr_gravity;
    
    InService inTask;
    
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        arr_list = new ArrayList<String>();
        arr_gravity = new ArrayList<Integer>();
        listChat = (ListView)findViewById(R.id.listChat);
        etxtIP = (EditText)findViewById(R.id.etxtIP);
        etxtMessage = (EditText)findViewById(R.id.etxtMessage);
        txtIP = (TextView)findViewById(R.id.txtIP);
        txtIP.setText("Your IP : " + getIP());
        txtStatus = (TextView)findViewById(R.id.txtStatus);

        SharedPreferences settings = getSharedPreferences("Pref", 0);
        String ip = settings.getString("IP", "192.168.1.1");
        etxtIP.setText(ip);
        
        inTask = new InService(getApplicationContext()
                    , TCP_SERVER_PORT, listChat, arr_list
                    , arr_gravity);
        inTask.execute();
        
        btnSend = (Button) findViewById(R.id.btnSend);
        btnSend.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                if(etxtMessage.getText().toString().length() > 0) {

                    txtStatus.setText("Sending...");
                    sendMessage(etxtIP.getText().toString()
                            , etxtMessage.getText().toString());
                    etxtMessage.setText("");
                }
            }
        });
    }
    
    public void sendMessage(String ip, String message) {
        final String IP_ADDRESS = ip;
        final String MESSAGE = message;

        Runnable runSend = new Runnable() {
            public void run() {
                try {
                    Socket s = new Socket(IP_ADDRESS, TCP_SERVER_PORT);
                    s.setSoTimeout(5000);
                    BufferedReader in = new BufferedReader
                            (new InputStreamReader(s.getInputStream()));
                    BufferedWriter out = new BufferedWriter
                            (new OutputStreamWriter(s.getOutputStream()));
                    String outgoingMsg = MESSAGE 
                            + System.getProperty("line.separator"); 
                    out.write(outgoingMsg);
                    out.flush();
                    Log.i("TcpClient", "sent: " + outgoingMsg);
                    String inMsg = in.readLine()
                            + System.getProperty("line.separator");
                    
                    Handler refresh = new Handler(Looper.getMainLooper());
                    refresh.post(new Runnable() {
                        public void run() {
                            arr_gravity.add(Gravity.LEFT);
                            arr_list.add("Me : " + MESSAGE);
                            listChat.setAdapter(new CustomListViewBlack
                                    (getApplicationContext()
                                    , android.R.layout.simple_list_item_1
                                    , arr_list, arr_gravity));
                            listChat.setSelection(listChat.getCount());
                            txtStatus.setText("Message has been sent.");
                            etxtMessage.setText("");
                        }
                    });
                    
                    Log.i("Message Response", inMsg);
                    s.close();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                    setText("No device on this IP address.");
                } catch (Exception e) {
                    e.printStackTrace();
                    setText("Connection failed. Please try again.");
                }
            }

            public void setText(String str) {
                final String string = str;
                Handler refresh = new Handler(Looper.getMainLooper());
                refresh.post(new Runnable() {
                    public void run() {
                        txtStatus.setText(string);
                    }
                });    
            }
        };
        new Thread(runSend).start();
    }
    
    public String getIP() {
        WifiManager wifiManager = 
                (WifiManager) getSystemService(WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        int ipAddress = wifiInfo.getIpAddress();
        String ip = (ipAddress & 0xFF) + "." +
                ((ipAddress >> 8 ) & 0xFF) + "." +
                ((ipAddress >> 16 ) & 0xFF) + "." +
                ((ipAddress >> 24 ) & 0xFF );
        if(ip.equals("0.0.0.0"))
            ip = "Please connect WIFI";
        return ip;
    }
    
    public void onPause() {
        super.onPause();
        SharedPreferences settings = getSharedPreferences("Pref", 0);
        SharedPreferences.Editor editor = settings.edit();
        editor.putString("IP", etxtIP.getText().toString());
        editor.commit();
        inTask.killTask();
    }
}

จากเดิมที่เจ้าของบล็อกทำหน้าแรกเป็นปุ่ม Sender กับ Receiver
ตอนนี้ก็ทำหน้าแรกเป็นหน้ารับข้อมูลและส่งข้อมูลเลย
คำสั่งก็จะคล้ายๆของเดิม คือมีคลาส sendMessage สำหรับส่งข้อมูล
และมีการเรียกใช้คลาส InService ใน onCreate สำหรับรับข้อมูล

ที่เพิ่มเข้ามาก็คือ inTask.killTask ใน onPause ซึ่งอันนี้
เจ้าของบล็อกได้แก้จากเดิมที่เป็น System.exit(0)
ถ้าใครติดตามบทความเก่าของเรื่องนี้ก็จะจำได้ว่า
เจ้าของบล็อกได้ติดปัญหาปิด AsyncTask เวลาออกแอพฯ
ทำให้เจ้าของบล็อกใช้คำสั่ง System.exit(0) แก้ขัดไปก่อน
ซึ่งในตอนนี้แก้ปัญหานี้ได้ล่ะ ด้วยคำสั่ง inTask.killTask
ส่วนการทำงานคำสั่งนี้จะอธิบายใน InService อีกที

main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#FFFFFF"
    android:orientation="vertical" 
    android:focusable="true"
    android:focusableInTouchMode="true" >
    <TextView
        android:id="@+id/txtIP"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#EFEFEF"
        android:gravity="center"
        android:text="Your IP : 0.0.0.0"
        android:textSize="20dp" />
    <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:orientation="horizontal" >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:text="Target IP"
            android:textSize="20dp" />
        <EditText
            android:id="@+id/etxtIP"
            android:layout_width="180dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="20dp"
            android:layout_weight="1"
            android:hint="Target IP Address"
            android:lines="1"
            android:maxLength="15"
            android:maxLines="1"
            android:singleLine="true" />
    </LinearLayout>

    <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="50dp"
            android:layout_marginTop="60dp" >

            <ListView
                android:id="@+id/listChat"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_margin="20dp" />
    </RelativeLayout>
    <RelativeLayout
        android:id="@+id/relativeLayout1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true" >
        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:text="Say"
            android:textSize="20dp" />
        <EditText
            android:id="@+id/etxtMessage"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_alignParentTop="true"
            android:layout_marginRight="110dp"
            android:layout_toRightOf="@+id/textView1"
            android:ems="10"
            android:hint="Message"
            android:lines="1"
            android:maxLength="30"
            android:maxLines="1"
            android:singleLine="true" />
        <Button
            android:id="@+id/btnSend"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_alignParentTop="true"
            android:layout_marginRight="20dp"
            android:text="Send" />
    </RelativeLayout>
    <TextView
        android:id="@+id/txtStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/relativeLayout1"
        android:layout_alignParentLeft="true"
        android:layout_marginBottom="5dp"
        android:layout_marginRight="20dp"
        android:gravity="right"
        android:text="Say something" />
</RelativeLayout>

สำหรับ Layout ของอันนี้ เจ้าของบล็อก Recommend ให้ศึกษา
เพราะเจ้าของบล็อกได้จัด Layout ให้รองรับกับหน้าจอได้ทุกขนาด
เป็นตัวอย่างการจัดวาง Widget ต่างๆบน Layout อย่างไรให้เหมาะสม
เจ้าของบล็อกถือว่าเป็นอีกเรื่องหนึ่งที่สำคัญไม่น้อยไปกว่าโปรแกรมเลย
โดยหน้าตาของแอพพลิเคชันก็ได้ออกมาประมาณนี้ 



InService.java
package app.akexorcist.tcpsocket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Gravity;
import android.widget.ListView;
import android.widget.Toast;

public class InService extends AsyncTask<Void, Void, Void> {
    ServerSocket ss;
    ListView LISTVIEW;
    ArrayList<String> ARR_LIST;
    List<Integer> ARR_GRAVITY;
    int TCP_SERVER_PORT;
    Context CONTEXT;
    Boolean TASK_STATE = true;
    
    public InService(Context context, int port, ListView lv
            , ArrayList<String> arr_list, List<Integer> arr_gravity ) {
        CONTEXT = context;
        TCP_SERVER_PORT = port;
        LISTVIEW = lv;
        ARR_LIST = arr_list;
        ARR_GRAVITY = arr_gravity;
    }
    
    public void killTask() {
        TASK_STATE = false;
    }
    
    protected Void doInBackground(Void... params) {  
        try {
            ss = new ServerSocket(TCP_SERVER_PORT);
            ss.setSoTimeout(1000);
        } catch (IOException e) {
            e.printStackTrace();
        }
        while(TASK_STATE) {
            try {
                Socket s = ss.accept();
                BufferedReader in = new BufferedReader
                        (new InputStreamReader(s.getInputStream()));
                BufferedWriter out = new BufferedWriter
                        (new OutputStreamWriter(s.getOutputStream()));
                final String incomingMsg = in.readLine() 
                        + System.getProperty("line.separator");
                Log.i("Message Incoming", incomingMsg);
                Handler refresh = new Handler(Looper.getMainLooper());
                refresh.post(new Runnable() {
                    public void run() {
                        ARR_GRAVITY.add(Gravity.RIGHT);
                        ARR_LIST.add("Someone : " 
                                + incomingMsg.replace(System.getProperty
                                ("line.separator"), ""));         
                        LISTVIEW.setAdapter(new CustomListViewBlack(CONTEXT
                                   , android.R.layout.simple_list_item_1
                                   , ARR_LIST, ARR_GRAVITY));
                        LISTVIEW.setSelection(LISTVIEW.getCount());
                        Toast.makeText(CONTEXT, "Message Incoming"
                                , Toast.LENGTH_SHORT).show();
                    }
                });
                String outgoingMsg = "OK" 
                        + System.getProperty("line.separator");
                out.write(outgoingMsg);
                out.flush();

            } catch (IOException e) { }
        }
        try {
            ss.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

สำหรับ InService ที่เป็นคลาสสำหรับรับข้อมูล
อันนี้เจ้าของบล็อกได้แก้ไขเพิ่ม โดยจะเห็นว่ามีฟังก์ชัน killTask
ที่เจ้าของบล็อกได้กล่าวถึงในโค๊ดก่อนหน้านี้
ซึ่งจะเห็นว่าเจ้าของบล็อกประกาศตัวแปร Boolean ขึ้นมา
เพื่อเอาไว้ใช้ออกจากลูป While ใน doInBackground นั่นเอง
เพราะตัวอย่างคราวที่แล้วใช้เป็น While(true) จึงทำให้มันวนตลอด
ไม่สามารถปิดตัวเองได้ จนต้องใช้ System.exit(0) แก้ขัดไปก่อน
โดย TASK_STATE จะเป็น false เมื่อเกิด onPause ใน Main.java 
ก็จะทำให้หลุดออกมาจากลูปของ While ได้
แต่จะเห็นอีกอย่างคือเจ้าของบล็อกย้าย Try/Catch ออกเป็นสองส่วน
สำหรับ ServerSocket ส่วนหนึ่ง และอีกส่วนสำหรับในลูป While
ทั้งนี้ก็เพื่อจะใช้คำสั่ง setSoTimeout ซึ่งเป็นคำสั่งหมดเวลา
ก็คือในขณะที่ Socket รอรับข้อมูล เมื่อผ่านไป 1 วินาที ก็จะหยุดรอ
ซึ่งการหยุดรอก็จะอยู่ที่คำสั่ง

Socket s = ss.accept();

ถ้าไม่กำหนด Timeout เวลาที่ผู้ใช้กดออกจากแอพ
ตัวแปร TASK_STATE เปลี่ยนเป็น false ก็จริง
แต่คำสั่งยังค้างอยู่ที่ ss.accept ก็จะไม่หลุดลูป While อยู่ดี
จึงใช้ Timeout เพื่อให้คำสั่ง ss.accept เกิด Exception ขึ้นมา
ซึ่งพอเกิด IOException ปุป ก็จะให้ Catch รอ ไว้
แต่ก็ไม่มีคำสั่งอะไรใน Catch เพราะให้มันออกจากคำสั่ง ss.accept เฉยๆ
พอ Catch เสร็จ ก็จะวนลูปใน While ต่อ ซึ่งทำให้ตัวแปร TASK_STATE
ถูกเช็คได้ว่าเป็น true หรือ false ก็จะทำให้หลุดออกจากลูป While ได้แล้ว
 และอย่าลืมว่าการหลุดออกจาก While คือเมื่อผู้ใช้ปิดแอพ
ดังนั้นก็ปิดการใช้งาน ServerSocket ด้วย เพื่อไม่ให้เกิดการเรียกซ้ำ
ในครั้งต่อไปที่จะทำให้โปรแกรมเกิดเออเรอร์เพราะยังไม่ได้ปิดของเดิม
ก็ให้ใช้คำสั่ง ss.close เพื่อปิดมันซะ เท่านี้ออกจากแอพ
AsyncTask ตัวนี้ก็จะหยุดทำงานและปิดการใช้งาน SocketServer ได้แล้ว


CustomListViewBlack.java
package app.akexorcist.tcpsocket;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

public class CustomListViewBlack extends ArrayAdapter<String> {
    ArrayList<String> STR; 
    LayoutInflater INFLATER;
    List<Integer> GRAVITY;
    
    public CustomListViewBlack(Context context, int textViewResourceId
            , ArrayList<String> objects, List<Integer> gv) {
        super(context, textViewResourceId, objects);
        INFLATER = (LayoutInflater)context.getSystemService
                (context.LAYOUT_INFLATER_SERVICE);
        STR = objects;
        GRAVITY = gv;
    }
 
    public View getView(int position, View convertView, ViewGroup parent) {
        View row = INFLATER.inflate(R.layout.listview_simple_row
                , parent, false);
        TextView textView = (TextView) row.findViewById(R.id.txt1);
        textView.setGravity(GRAVITY.get(position));
        textView.setTextColor(Color.BLACK);
        textView.setText(STR.get(position));
        return row;
    }
}

อันนี้ก็ไม่มีอะไรแค่ Custom List View ธรรมดา
แต่เวลามีข้อความรับเข้ามา ก็จะให้ข้อความใน Row นั้นๆชิดขวา
และถ้ามีการส่งข้อความออกไปข้อความใน Row นั้นๆก็จะชิดซ้าย
ให้นึกถึงพวกแอพแชทอย่าง Facebook หรือ Line ละกัน
ที่ข้อความที่ผู้ใช้พิมจะอยู่ฝั่งซ้าย และของคู่สนทนาจะอยู่ฝั่งขวา

listview_simple_row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >
    <TextView
        android:id="@+id/txt1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="5dp"
        android:text="TextView"
        android:textSize="20dp" />
</LinearLayout>

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app.akexorcist.tcpsocket"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="15" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Main"
            android:label="@string/title_activity_main" 
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="Sender" 
            android:screenOrientation="portrait"></activity>
        <activity android:name="Receiever" 
            android:screenOrientation="portrait"></activity>
    </application>
</manifest>

เรียบร้อยล่ะ เวลาใช้งานก็ให้พิม IP ของเครื่องเป้าหมาย
โดยจะมี IP ของแต่ละเครื่องบอกที่ข้างบนหน้าจอ แล้วส่งข้อความเลย
เครื่องเป้าหมายก็ต้องเปิดแอพนี้ด้วยนะ ถ้าไม่เปิดส่งไม่ได้
เท่านี้ก็สามารถส่งข้อความไปมาผ่านวงแลนคล้ายๆแอพแชทแล้ว

สำหรับผู้ที่หลงเข้ามาอ่านคนใดต้องการดาวน์โหลดไฟล์ตัวอย่าง
สามารถดาวน์โหลดได้จากที่นี่เลย TCP Chat [Google Drive]

สำหรับใครที่จะเอาไปประยุกต์ให้ส่งผ่านอินเตอร์เน็ตได้จริง
ก็ควรหาเซิฟเวอร์ซักตัวเป็นตัวกลางในการรับข้อมูลจากที่หนึ่ง
แล้วส่งไปยังเป้าหมายที่ต้องการ โดยอิงจาก IP ของอีกฝั่งที่เชื่อมต่อเข้ามา
แต่จริงๆแล้วเจ้าของบล็อกบอกได้เลยว่า มันทำอะไรมากกว่าแชทเยอะ


ดองไว้นาน เอามาลงซะที




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium