詳解android環(huán)境下的即時(shí)通訊
首先了解一下即時(shí)通信的概念。通過消息通道 傳輸消息對(duì)象,一個(gè)賬號(hào)發(fā)往另外一賬號(hào),只要賬號(hào)在線,可以即時(shí)獲取到消息,這就是最簡單的即使通訊。消息通道可由TCP/IP UDP實(shí)現(xiàn)。通俗講就是把一個(gè)人要發(fā)送給另外一個(gè)人的消息對(duì)象(文字,音視頻,文件)通過消息通道(C/S實(shí)時(shí)通信)進(jìn)行傳輸?shù)姆?wù)。即時(shí)通訊應(yīng)該包括四種形式,在線直傳、在線代理、離線代理、離線擴(kuò)展。在線直傳指不經(jīng)過服務(wù)器,直接實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)傳輸。在線代理指消息經(jīng)過服務(wù)器,在服務(wù)器實(shí)現(xiàn)中轉(zhuǎn),最后到達(dá)目標(biāo)賬號(hào)。離線代理指消息經(jīng)過服務(wù)器中轉(zhuǎn)到達(dá)目標(biāo)賬號(hào),對(duì)方不在線時(shí)消息暫存服務(wù)器的數(shù)據(jù)庫,在其上線再傳發(fā)。離線擴(kuò)展指將暫存消息以其它形式,例如郵件、短信等轉(zhuǎn)發(fā)給目標(biāo)賬號(hào)。
此外,我們還需要認(rèn)識(shí)一下計(jì)算機(jī)網(wǎng)絡(luò)相關(guān)的概念。經(jīng)典的計(jì)算機(jī)網(wǎng)絡(luò)四層模型中,TCP和UDP是傳輸層協(xié)議,包含著消息通信內(nèi)容。ip為網(wǎng)絡(luò)層協(xié)議,是一種網(wǎng)絡(luò)地址。TCP/IP,即傳輸控制協(xié)議/網(wǎng)間協(xié)議,定義了主機(jī)如何連入因特網(wǎng)及數(shù)據(jù)如何在它們之間傳輸?shù)臉?biāo)準(zhǔn)。Socket,又稱“套接字”, 在應(yīng)用層和傳輸層之間的一個(gè)抽象層,用于描述 IP 地址和端口,是一個(gè)通信連的句柄,應(yīng)用程序通常通過“套接字”向網(wǎng)絡(luò)發(fā)送請(qǐng)求或者應(yīng)答網(wǎng)絡(luò)請(qǐng)求,它就是網(wǎng)絡(luò)通信過程中端點(diǎn)的抽象表示。它把TCP/IP層復(fù)雜的操作抽象為幾個(gè)簡單的接口供應(yīng)用層調(diào)用已實(shí)現(xiàn)進(jìn)程在網(wǎng)絡(luò)中通信。XMPP(可擴(kuò)展消息處理現(xiàn)場協(xié)議)是基于可擴(kuò)展標(biāo)記語言(XML)的協(xié)議,應(yīng)用于即時(shí)通訊場景的應(yīng)用層協(xié)議,底層通過Socket實(shí)現(xiàn)。它用于即時(shí)消息(IM)以及在線現(xiàn)場探測。它在促進(jìn)服務(wù)器之間的準(zhǔn)即時(shí)操作。這個(gè)協(xié)議可能最終允許因特網(wǎng)用戶向因特網(wǎng)上的其他任何人發(fā)送即時(shí)消息, 即使其操作系統(tǒng)和瀏覽器不同。這樣實(shí)現(xiàn)即時(shí)通訊就有兩種方案,一是從套接字入手,直接利用socket提供的接口進(jìn)行數(shù)據(jù)的傳送。二是借助開源工具(服務(wù)器openfire),用XMPPConnection創(chuàng)建連接。
XMPP是實(shí)現(xiàn)即時(shí)通訊使用較為普遍的做法。XMPP中,各項(xiàng)工作都是通過在一個(gè) XMPP 流上發(fā)送和接收 XMPP 節(jié)來完成的。核心 XMPP 工具集由三種基本節(jié)組成,這三種節(jié)分別為<presence>、出席<message>、<iq>。XMPP 流由兩份 XML 文檔組成,通信的每個(gè)方向均有一份文檔。這份文檔有一個(gè)根元素<stream:stream>,這個(gè)根元素的子元素由可路由的節(jié)以及與流相關(guān)的頂級(jí)子元素構(gòu)成。xmpp協(xié)議同樣包括客戶端和服務(wù)器。客戶端基于 Android 平臺(tái)進(jìn)行開發(fā)。負(fù)責(zé)初始化通信過程,進(jìn)行即時(shí)通信時(shí),由客戶端負(fù)責(zé)向服務(wù)器發(fā)起創(chuàng)建連接請(qǐng)求。系統(tǒng)通過 GPRS 無線網(wǎng)絡(luò)與Internet 網(wǎng)絡(luò)建立連接,通過服務(wù)器實(shí)現(xiàn)與 Android 客戶端的即時(shí)通信腳。服務(wù)器端則采用 Openfire 作為服務(wù)器。 允許多個(gè)客戶端同時(shí)登錄并且并發(fā)的連接到一個(gè)服務(wù)器上。服務(wù)器對(duì)每個(gè)客戶端的連接進(jìn)行認(rèn)證,對(duì)認(rèn)證通過的客戶端創(chuàng)建會(huì)話,客戶端與服務(wù)器端之間的通信就在該會(huì)話的上下文中進(jìn)行。使用了 asmark 開源框架實(shí)現(xiàn)的即時(shí)通訊功能.該框架基于開源的 XMPP 即時(shí)通信協(xié)議,采用 C/S 體系結(jié)構(gòu),通過 GPRS 無線網(wǎng)絡(luò)用TCP 協(xié)議連接到服務(wù)器,以架設(shè)開源的 Openfn'e 服務(wù)器作為即時(shí)通訊平臺(tái)。xmpp消息通道的創(chuàng)建:
先配置通道信息進(jìn)行連接
ConnectionConfiguration configuration = new ConnectionConfiguration(HOST, PORT)
設(shè)置Debug信息和安全模式
configuration.setDebuggerEnabled(true); configuration.setSecurityMode(SecurityMode.disabled)
最后才是建立連接
conn.connect();
在ContentObserver的實(shí)現(xiàn)類中觀察消息變化。XMPPConnection.getRoster()獲取聯(lián)系人列表對(duì)象。用xmpp協(xié)議編寫通訊協(xié)議的大致思路可以如下。進(jìn)入登陸界面,通過xmppconnection的login方法實(shí)現(xiàn)登陸,登陸成功進(jìn)入主界面。主界面包含兩個(gè)Fragment,分別用來顯示聯(lián)系人和聊天記錄。創(chuàng)建聯(lián)系人和短信的數(shù)據(jù)觀察者,在聯(lián)系人、短信服務(wù)中分別設(shè)定監(jiān)聽RosterListener()、ChatManagerListener(),接受聯(lián)系人和短信信息,同時(shí)將相關(guān)信息添加到內(nèi)容提供者中。在內(nèi)容提供者中設(shè)定一個(gè)內(nèi)容觀察者,當(dāng)數(shù)據(jù)發(fā)生變化時(shí)通知界面更新。
本文的重點(diǎn)是利用Socket的接口實(shí)現(xiàn)即時(shí)通訊,因?yàn)榻^大多數(shù)即時(shí)通訊的底層都是通過Socket實(shí)現(xiàn)的。其基本的業(yè)務(wù)邏輯可描述如下。用戶進(jìn)入登陸界面后,提交賬號(hào)密碼 經(jīng)服務(wù)端確定,返回相關(guān)參數(shù)用于確定連接成功。進(jìn)入聊天界面或好友界面。點(diǎn)擊聯(lián)系人或聊天記錄的條目,進(jìn)入聊天界面。當(dāng)移動(dòng)端再次向服務(wù)器發(fā)送消息時(shí),由服務(wù)器轉(zhuǎn)發(fā)消息內(nèi)容給目標(biāo)賬號(hào)。同時(shí)更新界面顯示。這樣就完成即時(shí)通訊的基本功能。當(dāng)然,也可以添加一個(gè)后臺(tái)服務(wù),當(dāng)用戶推出程序時(shí),在后臺(tái)接受消息。不難看出,對(duì)于即時(shí)通訊來講,有三個(gè)關(guān)注點(diǎn):消息通道、消息內(nèi)容、消息對(duì)象。因此,主要邏輯也是圍繞這三個(gè)點(diǎn)展開。消息通道實(shí)現(xiàn)傳輸消息對(duì)象的發(fā)送和接收。為Socket(String host, int port)傳入服務(wù)其地址和端口號(hào),即可創(chuàng)建連接。消息內(nèi)容的格式應(yīng)該與服務(wù)器保持一致。接受數(shù)據(jù)時(shí),獲取輸入流并用DataInputStream包裝,通過輸入流讀取server發(fā)來的數(shù)據(jù)。發(fā)送數(shù)據(jù)時(shí),獲取輸出流并用DataOutputStream包裝,通過輸出流往server發(fā)送數(shù)據(jù)。消息內(nèi)容中應(yīng)該包括發(fā)送者、接受者信息、數(shù)據(jù)類型等。消息對(duì)象就是消息的發(fā)送者和消息的接受者。接下來在代碼中進(jìn)行詳細(xì)的講解。
創(chuàng)建一個(gè)消息的基類,實(shí)現(xiàn)xml文件和字符串的轉(zhuǎn)換,用到Xsream第三方j(luò)ar包。這樣當(dāng)創(chuàng)建消息類時(shí),繼承該方法,就可以直接在類中實(shí)現(xiàn)數(shù)據(jù)的轉(zhuǎn)換。
/** * Created by huang on 2016/12/3. */ public class ProtacolObjc implements Serializable { public String toXml() { XStream stream = new XStream(); //將根節(jié)點(diǎn)轉(zhuǎn)換為類名 stream.alias(this.getClass().getSimpleName(), this.getClass()); return stream.toXML(this); } public Object fromXml(String xml) { XStream x = new XStream(); x.alias(this.getClass().getSimpleName(), this.getClass()); return x.fromXML(xml); } //創(chuàng)建Gson數(shù)據(jù)和字符串之間轉(zhuǎn)換的方法,適應(yīng)多種數(shù)據(jù) public String toGson() { Gson gson = new Gson(); return toGson(); } public Object fromGson(String result) { Gson gson = new Gson(); return gson.fromJson(result, this.getClass()); } }
創(chuàng)建線程工具,指定方法運(yùn)行在子線程和主線程中。由于網(wǎng)絡(luò)操作需要在子線程中,界面更新需要在主線程中,創(chuàng)建線程工具可以方便選擇線程。
import android.os.Handler; /** * Created by huang on 2016/12/5. */ public class ThreadUtils { private static Handler handler = new Handler(); public static void runUIThread(Runnable r){ handler.post(r); } public static void runINThread(Runnable r){ new Thread(r).start(); } }
創(chuàng)建消息的工具類,包括消息內(nèi)容、消息類型、消息本省等。由于服務(wù)器返回的內(nèi)容中包含消息的包名信息所以消息本身的包名應(yīng)該于服務(wù)其保持一直。
/** * Created by huang on 2016/12/3. * 消息內(nèi)容 */ public class QQMessage extends ProtacolObjc { public String type = QQmessageType.MSG_TYPE_CHAT_P2P;// 類型的數(shù)據(jù) chat login public long from = 0;// 發(fā)送者 account public String fromNick = "";// 昵稱 public int fromAvatar = 1;// 頭像 public long to = 0; // 接收者 account public String content = ""; // 消息的內(nèi)容 約不? public String sendTime = getTime(); // 發(fā)送時(shí)間 public String getTime() { Date date = new Date(System.currentTimeMillis()); java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("mm-DD HH:mm:ss"); return format.format(date); } public String getTime(Long time) { Date date = new Date(time); java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("mm-DD HH:mm:ss"); return format.format(date); } } /** * Created by huang on 2016/12/3. * 消息類型 */ public class QQmessageType { public static final String MSG_TYPE_REGISTER = "register";// 注冊 public static final String MSG_TYPE_LOGIN = "login";// 登錄 public static final String MSG_TYPE_LOGIN_OUT = "loginout";// 登出 public static final String MSG_TYPE_CHAT_P2P = "chatp2p";// 聊天 public static final String MSG_TYPE_CHAT_ROOM = "chatroom";// 群聊 public static final String MSG_TYPE_OFFLINE = "offline";// 下線 public static final String MSG_TYPE_SUCCESS = "success";//成功 public static final String MSG_TYPE_BUDDY_LIST = "buddylist";// 好友 public static final String MSG_TYPE_FAILURE = "failure";// 失敗 } import com.example.huang.imsocket.bean.ProtacolObjc; /* *消息本身 包括 賬號(hào)、頭像和昵稱 * */ public class QQBuddy extends ProtacolObjc { public long account; public String nick; public int avatar; } /** * Created by huang on 2016/12/3. */ public class QQBuddyList extends ProtacolObjc { public ArrayList<QQBuddy> buddyList = new ArrayList<>(); }
關(guān)于socket的創(chuàng)建連接和發(fā)送消息、接受消息。
import android.util.Log; import com.example.huang.imsocket.bean.QQMessage; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; import java.util.ArrayList; import java.util.List; /** * Created by huang on 2016/12/3. * 連接 服務(wù)器 */ public class QQConnection extends Thread { private static final String TAG = "QQConnection"; private Socket client; private DataOutputStream write; private DataInputStream read; public static final String HOST = "192.168.23.48"; public static final int POST = 5225; private boolean flag = true; private List<OnQQmwssagereceiveLisener> mOnQQmwssagereceiveLisener = new ArrayList<>(); public void addOnQQmwssagereceiveLisener(OnQQmwssagereceiveLisener lisener) { mOnQQmwssagereceiveLisener.add(lisener); } public void removeOnQQmwssagereceiveLisener(OnQQmwssagereceiveLisener lisener) { mOnQQmwssagereceiveLisener.remove(lisener); } public interface OnQQmwssagereceiveLisener { public void onReiceive(QQMessage qq); } @Override public void run() { super.run(); while (flag) { try { String utf = read.readUTF(); QQMessage message = new QQMessage(); QQMessage msg = (QQMessage) message.fromXml(utf); if (msg != null) { for (OnQQmwssagereceiveLisener lisner : mOnQQmwssagereceiveLisener) lisner.onReiceive(msg); } } catch (IOException e) { e.printStackTrace(); } } } public void connect() { try { if (client == null) { client = new Socket(HOST, POST); write = new DataOutputStream(client.getOutputStream()); read = new DataInputStream(client.getInputStream()); flag = true; this.start(); Log.e(TAG, "connect: "+(write==null)+"---"+ (read == null)); } } catch (Exception e) { e.printStackTrace(); } } public void disconnect() { if (client != null) { flag = false; this.stop(); try { read.close(); } catch (IOException e) { e.printStackTrace(); } try { write.close(); } catch (IOException e) { e.printStackTrace(); } try { client.close(); } catch (IOException e) { e.printStackTrace(); } } } public void send(String xml) throws IOException { write.writeUTF(xml); write.flush(); } public void send(QQMessage qq) throws IOException { write.writeUTF(qq.toXml()); write.flush(); } }
閃屏界面的布局
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_splash" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@mipmap/splash_bg"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:src="@mipmap/conversation_bg_logo" /> </RelativeLayout>
閃屏界面,保持4秒鐘進(jìn)入登陸界面。一般來見,閃屏界面可以加載數(shù)據(jù)、獲取版本號(hào)、更新版本等操作。這里沒有做的那么復(fù)雜。
import com.example.huang.imsocket.R; public class SplashActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSupportActionBar().hide(); //隱藏標(biāo)欄 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); //全屏顯示 setContentView(R.layout.activity_splash); new Handler().postDelayed(new Runnable() { @Override public void run() { startActivity(new Intent(SplashActivity.this, LoginActivity.class)); finish(); } }, 4000); } }
登陸界面的布局
<?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="match_parent" android:background="#aabbdd" android:gravity="center" android:orientation="vertical"> <TableLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/conversation_bg_logo" /> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="8dp" android:gravity="center_horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="賬號(hào):" android:textColor="#000" /> <EditText android:id="@+id/et_accoun" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="3" android:gravity="center" android:hint="輸入賬號(hào)" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="4dp" android:gravity="center_horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="密碼:" android:textColor="#000" /> <EditText android:id="@+id/et_pwd" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="3" android:gravity="center" android:hint="輸入密碼" /> </TableRow> <Button android:id="@+id/btn_login" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="80dp" android:layout_marginRight="80dp" android:layout_marginTop="8dp" android:onClick="sendmessage" android:text="登錄" /> </TableLayout> </LinearLayout>
登陸界面,創(chuàng)建和服務(wù)器的連接,向服務(wù)器發(fā)送登陸信息,接受服務(wù)器返回的信息。
import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.Toast; import com.example.huang.imsocket.R; import com.example.huang.imsocket.bean.Myapp; import com.example.huang.imsocket.bean.QQBuddyList; import com.example.huang.imsocket.bean.QQMessage; import com.example.huang.imsocket.bean.QQmessageType; import com.example.huang.imsocket.core.QQConnection; import com.example.huang.imsocket.service.IMService; import com.example.huang.imsocket.util.ThreadUtils; import java.io.IOException; /** * Created by huang on 2016/12/3. */ public class LoginActivity extends Activity { private static final String TAG = "LoginActivity"; private EditText et_accoun; private EditText et_pwd; private String accoun; private QQConnection conn; private QQConnection.OnQQmwssagereceiveLisener lisener = new QQConnection.OnQQmwssagereceiveLisener() { @Override public void onReiceive(final QQMessage qq) { final QQBuddyList list = new QQBuddyList(); final QQBuddyList list2 = (QQBuddyList) list.fromXml(qq.content); if (QQmessageType.MSG_TYPE_BUDDY_LIST.equals(qq.type)) { ThreadUtils.runUIThread(new Runnable() { @Override public void run() { Toast.makeText(getBaseContext(), "成功", Toast.LENGTH_SHORT).show(); Myapp.me = conn; Myapp.username = accoun; Myapp.account = accoun + "@qq.com"; Intent intent = new Intent(LoginActivity.this, contactActivity.class); intent.putExtra("list", list2); startActivity(intent); Intent data = new Intent(LoginActivity.this, IMService.class); startService(data); finish(); } }); } else { ThreadUtils.runUIThread(new Runnable() { @Override public void run() { Toast.makeText(getBaseContext(), "登陸失敗", Toast.LENGTH_SHORT).show(); } }); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); et_accoun = (EditText) findViewById(R.id.et_accoun); et_pwd = (EditText) findViewById(R.id.et_pwd); ThreadUtils.runINThread(new Runnable() { @Override public void run() { try { conn = new QQConnection(); conn.addOnQQmwssagereceiveLisener(lisener); conn.connect(); } catch (Exception e) { e.printStackTrace(); } } }); } public void sendmessage(View view) { accoun = et_accoun.getText().toString().trim(); final String password = et_pwd.getText().toString().trim(); Log.i(TAG, "sendmessage: " + accoun + "#" + password); ThreadUtils.runINThread(new Runnable() { @Override public void run() { QQMessage message = new QQMessage(); message.type = QQmessageType.MSG_TYPE_LOGIN; message.content = accoun + "#" + password; String xml = message.toXml(); if (conn != null) { try { conn.send(xml); } catch (IOException e) { e.printStackTrace(); } } } }); } @Override protected void onDestroy() { super.onDestroy(); conn.removeOnQQmwssagereceiveLisener(lisener); } }
好友列表界面
<?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="match_parent" android:background="#aabbcc" android:orientation="vertical"> <TextView android:id="@+id/tv_title" android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:text="聯(lián)系人列表" android:textColor="#6d00" android:textSize="23dp" /> <ListView android:id="@+id/lv_contact" android:layout_width="match_parent" android:layout_height="match_parent"></ListView> </LinearLayout>
好友列表及時(shí)收到從哪個(gè)服務(wù)其發(fā)揮的好友更新信息,點(diǎn)擊條目跳到聊天界面。
import android.app.Activity; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.example.huang.imsocket.R; import com.example.huang.imsocket.bean.Myapp; import com.example.huang.imsocket.bean.QQBuddyList; import com.example.huang.imsocket.bean.QQMessage; import com.example.huang.imsocket.bean.QQmessageType; import com.example.huang.imsocket.core.QQConnection; import com.example.huang.imsocket.util.ThreadUtils; import java.util.ArrayList; import butterknife.Bind; import butterknife.ButterKnife; import cn.itcast.server.bean.QQBuddy; /** * Created by huang on 2016/12/5. */ public class contactActivity extends Activity { private static final String TAG = "contactActivity"; @Bind(R.id.tv_title) TextView tv_title; @Bind(R.id.lv_contact) ListView lv_contact; private QQBuddyList list; private ArrayList<QQBuddy> BuddyList = new ArrayList<>(); private ArrayAdapter adapter = null; private QQConnection.OnQQmwssagereceiveLisener listener = new QQConnection.OnQQmwssagereceiveLisener() { @Override public void onReiceive(QQMessage qq) { if (QQmessageType.MSG_TYPE_BUDDY_LIST.equals(qq.type)) { QQBuddyList qqlist = new QQBuddyList(); QQBuddyList qqm = (QQBuddyList) qqlist.fromXml(qq.content); BuddyList.clear(); BuddyList.addAll(qqm.buddyList); ThreadUtils.runUIThread(new Runnable() { @Override public void run() { saveAndNotify(); } }); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_contact); ButterKnife.bind(this); Myapp.me.addOnQQmwssagereceiveLisener(listener); Intent intent = getIntent(); list = (QQBuddyList) intent.getSerializableExtra("list"); BuddyList.clear(); BuddyList.addAll(list.buddyList); saveAndNotify(); } @Override protected void onDestroy() { super.onDestroy(); Myapp.me.removeOnQQmwssagereceiveLisener(listener); } private void saveAndNotify() { if (BuddyList.size() < 1) { return; } if (adapter == null) { adapter = new ArrayAdapter<QQBuddy>(getBaseContext(), 0, BuddyList) { @Override public View getView(int position, View convertView, ViewGroup parent) { viewHolder holder; if (convertView == null) { convertView = View.inflate(getContext(), R.layout.item_contacts, null); holder = new viewHolder(convertView); convertView.setTag(holder); } else { holder = (viewHolder) convertView.getTag(); } QQBuddy qqBuddy = BuddyList.get(position); holder.tv_nick.setText(qqBuddy.nick); holder.tv_account.setText(qqBuddy.account + "@qq.com"); if (Myapp.username.equals(qqBuddy.account + "")) { holder.tv_nick.setText("[自己]"); holder.tv_nick.setTextColor(Color.GRAY); } else { holder.tv_nick.setTextColor(Color.RED); } return convertView; } }; lv_contact.setAdapter(adapter); lv_contact.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { QQBuddy qqbuddy = BuddyList.get(position); if (Myapp.username.equals(qqbuddy.account + "")) { Toast.makeText(getBaseContext(), "不能和自己聊天", Toast.LENGTH_SHORT).show(); } else { Intent intent = new Intent(contactActivity.this, ChatActivity.class); intent.putExtra("account", qqbuddy.account + ""); intent.putExtra("nick", qqbuddy.nick + ""); startActivity(intent); } } }); } else { adapter.notifyDataSetChanged(); } } static class viewHolder { @Bind(R.id.iv_contact) ImageView iv_contact; @Bind(R.id.tv_nick) TextView tv_nick; @Bind(R.id.tv_account) TextView tv_account; public viewHolder(View view) { ButterKnife.bind(this, view); } } }
聊天界面
<?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="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="40dp" android:background="#aa119988" android:gravity="center" android:text="和誰誰聊天中........." android:textSize="19dp" /> <ListView android:id="@+id/lv_chat" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <EditText android:id="@+id/et_sms" android:layout_width="0dp" android:layout_height="40dp" android:layout_weight="1" android:hint="輸入聊天" /> <Button android:id="@+id/btn_send" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="發(fā)送" /> </LinearLayout> </LinearLayout>
聊天界面中消息接收和消息發(fā)送都需要及時(shí)更新列表。
import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.example.huang.imsocket.R; import com.example.huang.imsocket.bean.Myapp; import com.example.huang.imsocket.bean.QQMessage; import com.example.huang.imsocket.bean.QQmessageType; import com.example.huang.imsocket.core.QQConnection; import com.example.huang.imsocket.util.ThreadUtils; import java.io.IOException; import java.util.ArrayList; import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; /** * Created by huang on 2016/12/3. */ public class ChatActivity extends Activity { private static final String TAG = "ChatActivity"; @Bind(R.id.tv_name) TextView tv_name; @Bind(R.id.lv_chat) ListView lv_chat; @Bind(R.id.et_sms) EditText et_sms; private ArrayAdapter<QQMessage> adapter = null; private ArrayList<QQMessage> list = new ArrayList<>(); private String account; @OnClick(R.id.btn_send) public void send(View view) { String sendsms = et_sms.getText().toString().trim(); if (TextUtils.isEmpty(sendsms)) { Toast.makeText(this, "消息不能為空", Toast.LENGTH_SHORT).show(); return; } et_sms.setText(""); final QQMessage qq = new QQMessage(); qq.type = QQmessageType.MSG_TYPE_CHAT_P2P; qq.content = sendsms; qq.from = Long.parseLong(Myapp.username); qq.to = Long.parseLong(account); list.add(qq); setAdapteORNotify(); ThreadUtils.runINThread(new Runnable() { @Override public void run() { try { Myapp.me.send(qq); } catch (IOException e) { e.printStackTrace(); } } }); } private QQConnection.OnQQmwssagereceiveLisener listener = new QQConnection.OnQQmwssagereceiveLisener() { @Override public void onReiceive(final QQMessage qq) { if (QQmessageType.MSG_TYPE_CHAT_P2P.equals(qq.type)) { ThreadUtils.runUIThread(new Runnable() { @Override public void run() { list.add(qq); setAdapteORNotify(); } }); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_chat); ButterKnife.bind(this); Myapp.me.addOnQQmwssagereceiveLisener(listener); Intent intent = getIntent(); account = intent.getStringExtra("account"); String nick = intent.getStringExtra("nick"); tv_name.setText("和" + nick + "聊天中......"); setAdapteORNotify(); } @Override protected void onDestroy() { super.onDestroy(); Myapp.me.removeOnQQmwssagereceiveLisener(listener); } private void setAdapteORNotify() { if (list.size() < 1) { return; } if (adapter == null) { adapter = new ArrayAdapter<QQMessage>(this, 0, list) { @Override public int getViewTypeCount() { return 2; } @Override public int getItemViewType(int position) { QQMessage msg = list.get(position); long fromId = Long.parseLong(Myapp.username); if (fromId == msg.from) { return 0; } return 1; } @Override public View getView(int position, View convertView, ViewGroup parent) { int type = getItemViewType(position); if (type == 0) { viewHolder holder1 = null; if (convertView == null) { holder1 = new viewHolder(); convertView = View.inflate(getBaseContext(), R.layout.item_sms_send, null); holder1.tv_send_time = (TextView) convertView.findViewById(R.id.tv_send_time); holder1.tv_send = (TextView) convertView.findViewById(R.id.tv_send); convertView.setTag(holder1); } else { holder1 = (viewHolder) convertView.getTag(); } QQMessage qqMessage = list.get(position); holder1.tv_send_time.setText(qqMessage.sendTime); holder1.tv_send.setText(qqMessage.content); return convertView; } else if (type == 1) { viewHolder holder2 = null; if (convertView == null) { holder2 = new viewHolder(); convertView = View.inflate(getBaseContext(), R.layout.item_sms_receive, null); holder2.tv_receive_time = (TextView) convertView.findViewById(R.id.tv_receive_time); holder2.tv_receive = (TextView) convertView.findViewById(R.id.tv_receive); convertView.setTag(holder2); } else { holder2 = (viewHolder) convertView.getTag(); } QQMessage qqMessage = list.get(position); holder2.tv_receive_time.setText(qqMessage.sendTime); holder2.tv_receive.setText(qqMessage.content); return convertView; } return convertView; } }; lv_chat.setAdapter(adapter); } else { adapter.notifyDataSetChanged(); } if (lv_chat.getCount() > 0) { lv_chat.setSelection(lv_chat.getCount() - 1); } } class viewHolder { TextView tv_send_time; TextView tv_send; TextView tv_receive_time; TextView tv_receive; } }
最后可以添加一個(gè)服務(wù)當(dāng)程序退出時(shí),接受消息。
import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.widget.Toast; import com.example.huang.imsocket.bean.Myapp; import com.example.huang.imsocket.bean.QQMessage; import com.example.huang.imsocket.core.QQConnection; import com.example.huang.imsocket.util.ThreadUtils; /** * Created by huang on 2016/12/7. */ public class IMService extends Service { private QQConnection.OnQQmwssagereceiveLisener lisener = new QQConnection.OnQQmwssagereceiveLisener() { @Override public void onReiceive(final QQMessage qq) { ThreadUtils.runUIThread(new Runnable() { @Override public void run() { Toast.makeText(getBaseContext(), "收到好友消息: " + qq.content, Toast.LENGTH_SHORT).show(); } }); } }; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); Toast.makeText(getBaseContext(), "服務(wù)開啟", Toast.LENGTH_SHORT).show(); Myapp.me.addOnQQmwssagereceiveLisener(lisener); } @Override public void onDestroy() { Myapp.me.removeOnQQmwssagereceiveLisener(lisener); super.onDestroy(); } }
Activity和Service節(jié)點(diǎn)配置,以及相應(yīng)的權(quán)限。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.huang.imsocket"> <uses-permission android:name="android.permission.INTERNET" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.example.huang.imsocket.activity.SplashActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.example.huang.imsocket.activity.LoginActivity" android:theme="@android:style/Theme.NoTitleBar"></activity> <activity android:name="com.example.huang.imsocket.activity.ChatActivity" android:theme="@android:style/Theme.NoTitleBar"></activity> <activity android:name="com.example.huang.imsocket.activity.contactActivity" android:theme="@android:style/Theme.NoTitleBar"></activity> <service android:name=".service.IMService" /> </application> </manifest>
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android 實(shí)現(xiàn)切圓圖作為頭像使用實(shí)例
這篇文章主要介紹了Android 實(shí)現(xiàn)切圓圖作為頭像使用實(shí)例的相關(guān)資料,需要的朋友可以參考下2016-12-12Flutter使用AnimationController實(shí)現(xiàn)控制動(dòng)畫
這篇文章主要想帶大家來嘗試一下Flutter如何使用AnimationController實(shí)現(xiàn)一個(gè)拖拽圖片,然后返回原點(diǎn)的動(dòng)畫,感興趣的可以了解一下2023-05-05Android應(yīng)用開發(fā)中RecyclerView組件使用入門教程
這篇文章主要介紹了Android應(yīng)用開發(fā)中RecyclerView組件使用的入門教程,RecyclerView主要針對(duì)安卓5.0以上的material design開發(fā)提供支持,需要的朋友可以參考下2016-02-02Android菜單操作之創(chuàng)建并響應(yīng)菜單
這篇文章主要介紹了Android菜單操作之創(chuàng)建并響應(yīng)菜單的相關(guān)資料,如何使用代碼創(chuàng)建菜單項(xiàng),給菜單項(xiàng)分組,及各種響應(yīng)菜單事件的方法,需要的朋友可以參考下2016-04-04Android開發(fā)之AlarmManager的用法詳解
這篇文章主要介紹了Android開發(fā)之AlarmManager的用法,是Android應(yīng)用開發(fā)中非常實(shí)用的技能,需要的朋友可以參考下2014-07-07Android adb logcat 命令查看日志詳細(xì)介紹
這篇文章主要介紹了Android adb logcat 命令詳細(xì)介紹的相關(guān)資料,這里對(duì)logcat 命令進(jìn)行了詳細(xì)介紹,并介紹了過濾日志輸出的知識(shí),需要的朋友可以參考下2016-12-12Android Studio3.0升級(jí)后使用注意事項(xiàng)及解決方法
這篇文章主要介紹了Android Studio3.0升級(jí)后使用注意事項(xiàng)及解決方法,需要的朋友參考下吧2017-12-12Android 利用廣播監(jiān)聽usb連接狀態(tài)(變化情況)
這篇文章主要介紹了Android 利用廣播監(jiān)聽usb連接狀態(tài),需要的朋友可以參考下2017-06-06Android仿微博個(gè)人詳情頁滾動(dòng)到頂部的實(shí)例代碼
這篇文章主要介紹了Android仿微博個(gè)人詳情頁滾動(dòng)到頂部的實(shí)例代碼,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒家,需要的朋友可以參考下2019-05-05android 有阻尼下拉刷新列表的實(shí)現(xiàn)方法
下面小編就為大家分享一篇android 有阻尼下拉刷新列表的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,一起跟隨小編過來看看吧2018-01-01