首先了解一下即时通信的概念。通过消息通道 传输消息对象,一个账号发往另外一账号,只要账号在线,可以即时获取到消息,这就是最简单的即使通讯。消息通道可由TCP/IP UDP实现。通俗讲就是把一个人要发送给另外一个人的消息对象(文字,音视频,文件)通过消息通道(C/S实时通信)进行传输的服务。即时通讯应该包括四种形式,在线直传、在线代理、离线代理、离线扩展。在线直传指不经过服务器,直接实现点对点传输。在线代理指消息经过服务器,在服务器实现中转,最后到达目标账号。离线代理指消息经过服务器中转到达目标账号,对方不在线时消息暂存服务器的数据库,在其上线再传发。离线扩展指将暂存消息以其它形式,例如邮件、短信等转发给目标账号。
此外,我们还需要认识一下计算机网络相关的概念。经典的计算机网络四层模型中,TCP和UDP是传输层协议,包含着消息通信内容。ip为网络层协议,是一种网络地址。TCP/IP,即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何在它们之间传输的标准。Socket,又称“套接字”, 在应用层和传输层之间的一个抽象层,用于描述 IP 地址和端口,是一个通信连的句柄,应用程序通常通过“套接字”向网络发送请求或者应答网络请求,它就是网络通信过程中端点的抽象表示。它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。XMPP(可扩展消息处理现场协议)是基于可扩展标记语言(XML)的协议,应用于即时通讯场景的应用层协议,底层通过Socket实现。它用于即时消息(IM)以及在线现场探测。它在促进服务器之间的准即时操作。这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息, 即使其操作系统和浏览器不同。这样实现即时通讯就有两种方案,一是从套接字入手,直接利用socket提供的接口进行数据的传送。二是借助开源工具(服务器openfire),用XMPPConnection创建连接。
XMPP是实现即时通讯使用较为普遍的做法。XMPP中,各项工作都是通过在一个 XMPP 流上发送和接收 XMPP 节来完成的。核心 XMPP 工具集由三种基本节组成,这三种节分别为<presence>、出席<message>、<iq>。XMPP 流由两份 XML 文档组成,通信的每个方向均有一份文档。这份文档有一个根元素<stream:stream>,这个根元素的子元素由可路由的节以及与流相关的顶级子元素构成。xmpp协议同样包括客户端和服务器。客户端基于 Android 平台进行开发。负责初始化通信过程,进行即时通信时,由客户端负责向服务器发起创建连接请求。系统通过 GPRS 无线网络与Internet 网络建立连接,通过服务器实现与 Android 客户端的即时通信脚。服务器端则采用 Openfire 作为服务器。 允许多个客户端同时登录并且并发的连接到一个服务器上。服务器对每个客户端的连接进行认证,对认证通过的客户端创建会话,客户端与服务器端之间的通信就在该会话的上下文中进行。使用了 asmark 开源框架实现的即时通讯功能.该框架基于开源的 XMPP 即时通信协议,采用 C/S 体系结构,通过 GPRS 无线网络用TCP 协议连接到服务器,以架设开源的 Openfn'e 服务器作为即时通讯平台。xmpp消息通道的创建:
先配置通道信息进行连接
ConnectionConfiguration configuration = new ConnectionConfiguration(HOST, PORT),
设置Debug信息和安全模式
configuration.setDebuggerEnabled(true);
configuration.setSecurityMode(SecurityMode.disabled),
最后才是建立连接
conn.connect();
在ContentObserver的实现类中观察消息变化。XMPPConnection.getRoster()获取联系人列表对象。用xmpp协议编写通讯协议的大致思路可以如下。进入登陆界面,通过xmppconnection的login方法实现登陆,登陆成功进入主界面。主界面包含两个Fragment,分别用来显示联系人和聊天记录。创建联系人和短信的数据观察者,在联系人、短信服务中分别设定监听RosterListener()、ChatManagerListener(),接受联系人和短信信息,同时将相关信息添加到内容提供者中。在内容提供者中设定一个内容观察者,当数据发生变化时通知界面更新。
本文的重点是利用Socket的接口实现即时通讯,因为绝大多数即时通讯的底层都是通过Socket实现的。其基本的业务逻辑可描述如下。用户进入登陆界面后,提交账号密码 经服务端确定,返回相关参数用于确定连接成功。进入聊天界面或好友界面。点击联系人或聊天记录的条目,进入聊天界面。当移动端再次向服务器发送消息时,由服务器转发消息内容给目标账号。同时更新界面显示。这样就完成即时通讯的基本功能。当然,也可以添加一个后台服务,当用户推出程序时,在后台接受消息。不难看出,对于即时通讯来讲,有三个关注点:消息通道、消息内容、消息对象。因此,主要逻辑也是围绕这三个点展开。消息通道实现传输消息对象的发送和接收。为Socket(String host, int port)传入服务其地址和端口号,即可创建连接。消息内容的格式应该与服务器保持一致。接受数据时,获取输入流并用DataInputStream包装,通过输入流读取server发来的数据。发送数据时,获取输出流并用DataOutputStream包装,通过输出流往server发送数据。消息内容中应该包括发送者、接受者信息、数据类型等。消息对象就是消息的发送者和消息的接受者。接下来在代码中进行详细的讲解。
创建一个消息的基类,实现xml文件和字符串的转换,用到Xsream第三方jar包。这样当创建消息类时,继承该方法,就可以直接在类中实现数据的转换。
/*** Created by huang on 2016/12/3. */ public class ProtacolObjc implementsSerializable { publicString toXml() { XStream stream = newXStream(); //将根节点转换为类名 stream.alias(this.getClass().getSimpleName(), this.getClass()); return stream.toXML(this); } publicObject fromXml(String xml) { XStream x = newXStream(); x.alias(this.getClass().getSimpleName(), this.getClass()); returnx.fromXML(xml); } //创建Gson数据和字符串之间转换的方法,适应多种数据 publicString toGson() { Gson gson = newGson(); returntoGson(); } publicObject fromGson(String result) { Gson gson = newGson(); return gson.fromJson(result, this.getClass()); } }
创建线程工具,指定方法运行在子线程和主线程中。由于网络操作需要在子线程中,界面更新需要在主线程中,创建线程工具可以方便选择线程。
importandroid.os.Handler; /*** Created by huang on 2016/12/5. */ public classThreadUtils { private static Handler handler = newHandler(); public static voidrunUIThread(Runnable r){ handler.post(r); } public static voidrunINThread(Runnable r){ newThread(r).start(); } }
创建消息的工具类,包括消息内容、消息类型、消息本省等。由于服务器返回的内容中包含消息的包名信息所以消息本身的包名应该于服务其保持一直。
/*** Created by huang on 2016/12/3. * 消息内容 */ public class QQMessage extendsProtacolObjc { public String type = QQmessageType.MSG_TYPE_CHAT_P2P;//类型的数据 chat login public long from = 0;//发送者 account public String fromNick = "";//昵称 public int fromAvatar = 1;//头像 public long to = 0; //接收者 account public String content = ""; //消息的内容 约不? public String sendTime = getTime(); //发送时间 publicString getTime() { Date date = newDate(System.currentTimeMillis()); java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("mm-DD HH:mm:ss"); returnformat.format(date); } publicString getTime(Long time) { Date date = newDate(time); java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("mm-DD HH:mm:ss"); returnformat.format(date); } } /*** Created by huang on 2016/12/3. * 消息类型 */ public classQQmessageType { 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";//失败 } importcom.example.huang.imsocket.bean.ProtacolObjc; /**消息本身 包括 账号、头像和昵称 * */ public class QQBuddy extendsProtacolObjc { public longaccount; publicString nick; public intavatar; } /*** Created by huang on 2016/12/3. */ public class QQBuddyList extendsProtacolObjc { public ArrayList<QQBuddy> buddyList = new ArrayList<>(); }
关于socket的创建连接和发送消息、接受消息。
importandroid.util.Log; importcom.example.huang.imsocket.bean.QQMessage; importjava.io.DataInputStream; importjava.io.DataOutputStream; importjava.io.IOException; importjava.net.Socket; importjava.util.ArrayList; importjava.util.List; /*** Created by huang on 2016/12/3. * 连接 服务器 */ public class QQConnection extendsThread { private static final String TAG = "QQConnection"; privateSocket client; privateDataOutputStream write; privateDataInputStream 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 voidaddOnQQmwssagereceiveLisener(OnQQmwssagereceiveLisener lisener) { mOnQQmwssagereceiveLisener.add(lisener); } public voidremoveOnQQmwssagereceiveLisener(OnQQmwssagereceiveLisener lisener) { mOnQQmwssagereceiveLisener.remove(lisener); } public interfaceOnQQmwssagereceiveLisener { public voidonReiceive(QQMessage qq); } @Override public voidrun() { super.run(); while(flag) { try{ String utf =read.readUTF(); QQMessage message = newQQMessage(); QQMessage msg =(QQMessage) message.fromXml(utf); if (msg != null) { for(OnQQmwssagereceiveLisener lisner : mOnQQmwssagereceiveLisener) lisner.onReiceive(msg); } } catch(IOException e) { e.printStackTrace(); } } } public voidconnect() { try{ if (client == null) { client = newSocket(HOST, POST); write = newDataOutputStream(client.getOutputStream()); read = newDataInputStream(client.getInputStream()); flag = true; this.start(); Log.e(TAG, "connect: "+(write==null)+"---"+ (read == null)); } } catch(Exception e) { e.printStackTrace(); } } public voiddisconnect() { 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) throwsIOException { write.writeUTF(xml); write.flush(); } public void send(QQMessage qq) throwsIOException { 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秒钟进入登陆界面。一般来见,闪屏界面可以加载数据、获取版本号、更新版本等操作。这里没有做的那么复杂。
importcom.example.huang.imsocket.R; public class SplashActivity extendsAppCompatActivity { @Override protected voidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSupportActionBar().hide(); //隐藏标栏 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); //全屏显示 setContentView(R.layout.activity_splash); new Handler().postDelayed(newRunnable() { @Override public voidrun() { 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="账号:"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="输入账号" /> </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>
登陆界面,创建和服务器的连接,向服务器发送登陆信息,接受服务器返回的信息。
importandroid.app.Activity; importandroid.content.Intent; importandroid.os.Bundle; importandroid.util.Log; importandroid.view.View; importandroid.widget.EditText; importandroid.widget.Toast; importcom.example.huang.imsocket.R; importcom.example.huang.imsocket.bean.Myapp; importcom.example.huang.imsocket.bean.QQBuddyList; importcom.example.huang.imsocket.bean.QQMessage; importcom.example.huang.imsocket.bean.QQmessageType; importcom.example.huang.imsocket.core.QQConnection; importcom.example.huang.imsocket.service.IMService; importcom.example.huang.imsocket.util.ThreadUtils; importjava.io.IOException; /*** Created by huang on 2016/12/3. */ public class LoginActivity extendsActivity { private static final String TAG = "LoginActivity"; privateEditText et_accoun; privateEditText et_pwd; privateString accoun; privateQQConnection conn; private QQConnection.OnQQmwssagereceiveLisener lisener = newQQConnection.OnQQmwssagereceiveLisener() { @Override public void onReiceive(finalQQMessage qq) { final QQBuddyList list = newQQBuddyList(); final QQBuddyList list2 =(QQBuddyList) list.fromXml(qq.content); if(QQmessageType.MSG_TYPE_BUDDY_LIST.equals(qq.type)) { ThreadUtils.runUIThread(newRunnable() { @Override public voidrun() { 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(newRunnable() { @Override public voidrun() { Toast.makeText(getBaseContext(), "登陆失败", Toast.LENGTH_SHORT).show(); } }); } } }; @Override protected voidonCreate(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(newRunnable() { @Override public voidrun() { try{ conn = newQQConnection(); conn.addOnQQmwssagereceiveLisener(lisener); conn.connect(); } catch(Exception e) { e.printStackTrace(); } } }); } public voidsendmessage(View view) { accoun =et_accoun.getText().toString().trim(); final String password =et_pwd.getText().toString().trim(); Log.i(TAG, "sendmessage: " + accoun + "#" +password); ThreadUtils.runINThread(newRunnable() { @Override public voidrun() { QQMessage message = newQQMessage(); 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 voidonDestroy() { 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="联系人列表"android:textColor="#6d00"android:textSize="23dp" /> <ListView android:id="@+id/lv_contact"android:layout_width="match_parent"android:layout_height="match_parent"></ListView> </LinearLayout>
好友列表及时收到从哪个服务其发挥的好友更新信息,点击条目跳到聊天界面。
importandroid.app.Activity; importandroid.content.Intent; importandroid.graphics.Color; importandroid.os.Bundle; importandroid.view.View; importandroid.view.ViewGroup; importandroid.widget.AdapterView; importandroid.widget.ArrayAdapter; importandroid.widget.ImageView; importandroid.widget.ListView; importandroid.widget.TextView; importandroid.widget.Toast; importcom.example.huang.imsocket.R; importcom.example.huang.imsocket.bean.Myapp; importcom.example.huang.imsocket.bean.QQBuddyList; importcom.example.huang.imsocket.bean.QQMessage; importcom.example.huang.imsocket.bean.QQmessageType; importcom.example.huang.imsocket.core.QQConnection; importcom.example.huang.imsocket.util.ThreadUtils; importjava.util.ArrayList; importbutterknife.Bind; importbutterknife.ButterKnife; importcn.itcast.server.bean.QQBuddy; /*** Created by huang on 2016/12/5. */ public class contactActivity extendsActivity { private static final String TAG = "contactActivity"; @Bind(R.id.tv_title) TextView tv_title; @Bind(R.id.lv_contact) ListView lv_contact; privateQQBuddyList list; private ArrayList<QQBuddy> BuddyList = new ArrayList<>(); private ArrayAdapter adapter = null; private QQConnection.OnQQmwssagereceiveLisener listener = newQQConnection.OnQQmwssagereceiveLisener() { @Override public voidonReiceive(QQMessage qq) { if(QQmessageType.MSG_TYPE_BUDDY_LIST.equals(qq.type)) { QQBuddyList qqlist = newQQBuddyList(); QQBuddyList qqm =(QQBuddyList) qqlist.fromXml(qq.content); BuddyList.clear(); BuddyList.addAll(qqm.buddyList); ThreadUtils.runUIThread(newRunnable() { @Override public voidrun() { saveAndNotify(); } }); } } }; @Override protected voidonCreate(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 voidonDestroy() { super.onDestroy(); Myapp.me.removeOnQQmwssagereceiveLisener(listener); } private voidsaveAndNotify() { if (BuddyList.size() < 1) { return; } if (adapter == null) { adapter = new ArrayAdapter<QQBuddy>(getBaseContext(), 0, BuddyList) { @Override public View getView(intposition, View convertView, ViewGroup parent) { viewHolder holder; if (convertView == null) { convertView = View.inflate(getContext(), R.layout.item_contacts, null); holder = newviewHolder(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); } returnconvertView; } }; lv_contact.setAdapter(adapter); lv_contact.setOnItemClickListener(newAdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, longid) { 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 classviewHolder { @Bind(R.id.iv_contact) ImageView iv_contact; @Bind(R.id.tv_nick) TextView tv_nick; @Bind(R.id.tv_account) TextView tv_account; publicviewHolder(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="发送" /> </LinearLayout> </LinearLayout>
聊天界面中消息接收和消息发送都需要及时更新列表。
importandroid.app.Activity; importandroid.content.Intent; importandroid.os.Bundle; importandroid.text.TextUtils; importandroid.view.View; importandroid.view.ViewGroup; importandroid.widget.ArrayAdapter; importandroid.widget.EditText; importandroid.widget.ListView; importandroid.widget.TextView; importandroid.widget.Toast; importcom.example.huang.imsocket.R; importcom.example.huang.imsocket.bean.Myapp; importcom.example.huang.imsocket.bean.QQMessage; importcom.example.huang.imsocket.bean.QQmessageType; importcom.example.huang.imsocket.core.QQConnection; importcom.example.huang.imsocket.util.ThreadUtils; importjava.io.IOException; importjava.util.ArrayList; importbutterknife.Bind; importbutterknife.ButterKnife; importbutterknife.OnClick; /*** Created by huang on 2016/12/3. */ public class ChatActivity extendsActivity { 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<>(); privateString account; @OnClick(R.id.btn_send) public voidsend(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 = newQQMessage(); 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(newRunnable() { @Override public voidrun() { try{ Myapp.me.send(qq); } catch(IOException e) { e.printStackTrace(); } } }); } private QQConnection.OnQQmwssagereceiveLisener listener = newQQConnection.OnQQmwssagereceiveLisener() { @Override public void onReiceive(finalQQMessage qq) { if(QQmessageType.MSG_TYPE_CHAT_P2P.equals(qq.type)) { ThreadUtils.runUIThread(newRunnable() { @Override public voidrun() { list.add(qq); setAdapteORNotify(); } }); } } }; @Override protected voidonCreate(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 voidonDestroy() { super.onDestroy(); Myapp.me.removeOnQQmwssagereceiveLisener(listener); } private voidsetAdapteORNotify() { if (list.size() < 1) { return; } if (adapter == null) { adapter = new ArrayAdapter<QQMessage>(this, 0, list) { @Override public intgetViewTypeCount() { return 2; } @Override public int getItemViewType(intposition) { QQMessage msg =list.get(position); long fromId =Long.parseLong(Myapp.username); if (fromId ==msg.from) { return 0; } return 1; } @Override public View getView(intposition, View convertView, ViewGroup parent) { int type =getItemViewType(position); if (type == 0) { viewHolder holder1 = null; if (convertView == null) { holder1 = newviewHolder(); 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); returnconvertView; } else if (type == 1) { viewHolder holder2 = null; if (convertView == null) { holder2 = newviewHolder(); 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); returnconvertView; } returnconvertView; } }; lv_chat.setAdapter(adapter); } else{ adapter.notifyDataSetChanged(); } if (lv_chat.getCount() > 0) { lv_chat.setSelection(lv_chat.getCount() - 1); } } classviewHolder { TextView tv_send_time; TextView tv_send; TextView tv_receive_time; TextView tv_receive; } }
最后可以添加一个服务当程序退出时,接受消息。
importandroid.app.Service; importandroid.content.Intent; importandroid.os.IBinder; importandroid.widget.Toast; importcom.example.huang.imsocket.bean.Myapp; importcom.example.huang.imsocket.bean.QQMessage; importcom.example.huang.imsocket.core.QQConnection; importcom.example.huang.imsocket.util.ThreadUtils; /*** Created by huang on 2016/12/7. */ public class IMService extendsService { private QQConnection.OnQQmwssagereceiveLisener lisener = newQQConnection.OnQQmwssagereceiveLisener() { @Override public void onReiceive(finalQQMessage qq) { ThreadUtils.runUIThread(newRunnable() { @Override public voidrun() { Toast.makeText(getBaseContext(), "收到好友消息: " +qq.content, Toast.LENGTH_SHORT).show(); } }); } }; @Override publicIBinder onBind(Intent intent) { return null; } @Override public voidonCreate() { super.onCreate(); Toast.makeText(getBaseContext(), "服务开启", Toast.LENGTH_SHORT).show(); Myapp.me.addOnQQmwssagereceiveLisener(lisener); } @Override public voidonDestroy() { Myapp.me.removeOnQQmwssagereceiveLisener(lisener); super.onDestroy(); } }
Activity和Service节点配置,以及相应的权限。
<?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>