基于xmpp openfire smack开发之Android客户端开发[3]

在上两篇文章中,我们依次介绍openfire部署以及smack常用API的使用,这一节中我们着力介绍如何基于asmack开发一个Android的客户端,本篇的重点在实践,讲解和原理环节,大家可以参考前两篇的文章

基于xmpp openfire smack开发之openfire介绍和部署[1]

基于xmpp openfire smack开发之smack类库介绍和使用[2]

 

1.源码结构介绍

基于xmpp openfire smack开发之Android客户端开发[3]

activity包下存放一些android页面交互相关的控制程序,还有一个些公共帮助类

db包为sqlite的工具类封装,这里做了一些自定义的改造,稍微仿Spring的JdbcTemplate结构,使用起来更加方便一点

manager包留下主要是一些管理组件,包括联系人管理,消息管理,提醒管理,离线消息管理,用户管理,xmpp连接管理

model包中都是一些对象模型,传输介质

service中存放一些android后台的核心服务,主要包括聊天服务,联系人服务,系统消息服务,重连接服务

task包中存放一些耗时的异步操作

util中存放一些常用的工具类

view中一些和android的UI相关的显示控件


基于xmpp openfire smack开发之Android客户端开发[3]

anim中存放一些动画元素的配置

layout是布局页面

menu是地步菜单布局页面

values中存放一些字符,颜色,样式,参数的配置信息

其中strings.xml中,保存的缺省配置为gtalk的服务器信息,大家如果有谷歌gtalk的账号可以直接登录,否则需要更改这里的配置才可以使用其他的xmpp服务器

[html] view plaincopy
  1. <!-- 缺省的服务器配置 -->   
  2.   <integer name="xmpp_port">5222</integer>   
  3.   <string name="xmpp_host">talk.google.com</string>   
  4.   <string name="xmpp_service_name">gmail.com</string>  
  5.   <bool name="is_remember">true</bool>  
  6.   <bool name="is_autologin">false</bool>  
  7.   <bool name="is_novisible">false</bool>   

AndroidManifest.xml为android功能清单的配置文件,我们这里开放的权限并不多

[html] view plaincopy
  1.     <!-- 访问Internet -->  
  2. <uses-permission android:name="android.permission.INTERNET" />  
  3. <!--- 访问网络状态 -->  
  4.     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />  
  5.     <!-- 往SDCard写入数据权限 -->  
  6.     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>  
  7.    <span style="WHITE-SPACE: pre">  </span><!-- 在SDCard中创建与删除文件权限 -->  
  8.    <span style="WHITE-SPACE: pre">  </span><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>  
  9.    <span style="WHITE-SPACE: pre">  </span><!-- 往SDCard写入数据权限 -->  
  10.    <span style="WHITE-SPACE: pre">  </span><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>  

2.核心类介绍

1.ActivitySupport类
[java] view plaincopy
  1. package csdn.shimiso.eim.activity;  
  2.   
  3. import android.app.Activity;  
  4. import android.app.AlertDialog;  
  5. import android.app.Notification;  
  6. import android.app.NotificationManager;  
  7. import android.app.PendingIntent;  
  8. import android.app.ProgressDialog;  
  9. import android.content.Context;  
  10. import android.content.DialogInterface;  
  11. import android.content.Intent;  
  12. import android.content.SharedPreferences;  
  13. import android.location.LocationManager;  
  14. import android.net.ConnectivityManager;  
  15. import android.net.NetworkInfo;  
  16. import android.os.Bundle;  
  17. import android.os.Environment;  
  18. import android.provider.Settings;  
  19. import android.view.inputmethod.InputMethodManager;  
  20. import android.widget.Toast;  
  21. import csdn.shimiso.eim.R;  
  22. import csdn.shimiso.eim.comm.Constant;  
  23. import csdn.shimiso.eim.model.LoginConfig;  
  24. import csdn.shimiso.eim.service.IMChatService;  
  25. import csdn.shimiso.eim.service.IMContactService;  
  26. import csdn.shimiso.eim.service.IMSystemMsgService;  
  27. import csdn.shimiso.eim.service.ReConnectService;  
  28.   
  29. /** 
  30.  * Actity 工具支持类 
  31.  *  
  32.  * @author shimiso 
  33.  *  
  34.  */  
  35. public class ActivitySupport extends Activity implements IActivitySupport {  
  36.   
  37.     protected Context context = null;  
  38.     protected SharedPreferences preferences;  
  39.     protected EimApplication eimApplication;  
  40.     protected ProgressDialog pg = null;  
  41.     protected NotificationManager notificationManager;  
  42.   
  43.     @Override  
  44.     protected void onCreate(Bundle savedInstanceState) {  
  45.         super.onCreate(savedInstanceState);  
  46.         context = this;  
  47.         preferences = getSharedPreferences(Constant.LOGIN_SET, 0);  
  48.         notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);  
  49.         pg = new ProgressDialog(context);  
  50.         eimApplication = (EimApplication) getApplication();  
  51.         eimApplication.addActivity(this);  
  52.     }  
  53.   
  54.     @Override  
  55.     protected void onStart() {  
  56.         super.onStart();  
  57.     }  
  58.   
  59.     @Override  
  60.     protected void onResume() {  
  61.         super.onResume();  
  62.     }  
  63.   
  64.     @Override  
  65.     protected void onPause() {  
  66.         super.onPause();  
  67.     }  
  68.   
  69.     @Override  
  70.     protected void onStop() {  
  71.         super.onStop();  
  72.     }  
  73.   
  74.     @Override  
  75.     public void onDestroy() {  
  76.         super.onDestroy();  
  77.     }  
  78.   
  79.     @Override  
  80.     public ProgressDialog getProgressDialog() {  
  81.         return pg;  
  82.     }  
  83.   
  84.     @Override  
  85.     public void startService() {  
  86.         // 好友联系人服务  
  87.         Intent server = new Intent(context, IMContactService.class);  
  88.         context.startService(server);  
  89.         // 聊天服务  
  90.         Intent chatServer = new Intent(context, IMChatService.class);  
  91.         context.startService(chatServer);  
  92.         // 自动恢复连接服务  
  93.         Intent reConnectService = new Intent(context, ReConnectService.class);  
  94.         context.startService(reConnectService);  
  95.         // 系统消息连接服务  
  96.         Intent imSystemMsgService = new Intent(context,  
  97.                 IMSystemMsgService.class);  
  98.         context.startService(imSystemMsgService);  
  99.     }  
  100.   
  101.     /** 
  102.      *  
  103.      * 销毁服务. 
  104.      *  
  105.      * @author shimiso 
  106.      * @update 2012-5-16 下午12:16:08 
  107.      */  
  108.     @Override  
  109.     public void stopService() {  
  110.         // 好友联系人服务  
  111.         Intent server = new Intent(context, IMContactService.class);  
  112.         context.stopService(server);  
  113.         // 聊天服务  
  114.         Intent chatServer = new Intent(context, IMChatService.class);  
  115.         context.stopService(chatServer);  
  116.   
  117.         // 自动恢复连接服务  
  118.         Intent reConnectService = new Intent(context, ReConnectService.class);  
  119.         context.stopService(reConnectService);  
  120.   
  121.         // 系统消息连接服务  
  122.         Intent imSystemMsgService = new Intent(context,  
  123.                 IMSystemMsgService.class);  
  124.         context.stopService(imSystemMsgService);  
  125.     }  
  126.   
  127.     @Override  
  128.     public void isExit() {  
  129.         new AlertDialog.Builder(context).setTitle("确定退出吗?")  
  130.                 .setNeutralButton("确定"new DialogInterface.OnClickListener() {  
  131.                     @Override  
  132.                     public void onClick(DialogInterface dialog, int which) {  
  133.                         stopService();  
  134.                         eimApplication.exit();  
  135.                     }  
  136.                 })  
  137.                 .setNegativeButton("取消"new DialogInterface.OnClickListener() {  
  138.                     @Override  
  139.                     public void onClick(DialogInterface dialog, int which) {  
  140.                         dialog.cancel();  
  141.                     }  
  142.                 }).show();  
  143.     }  
  144.   
  145.     @Override  
  146.     public boolean hasInternetConnected() {  
  147.         ConnectivityManager manager = (ConnectivityManager) context  
  148.                 .getSystemService(context.CONNECTIVITY_SERVICE);  
  149.         if (manager != null) {  
  150.             NetworkInfo network = manager.getActiveNetworkInfo();  
  151.             if (network != null && network.isConnectedOrConnecting()) {  
  152.                 return true;  
  153.             }  
  154.         }  
  155.         return false;  
  156.     }  
  157.   
  158.     @Override  
  159.     public boolean validateInternet() {  
  160.         ConnectivityManager manager = (ConnectivityManager) context  
  161.                 .getSystemService(context.CONNECTIVITY_SERVICE);  
  162.         if (manager == null) {  
  163.             openWirelessSet();  
  164.             return false;  
  165.         } else {  
  166.             NetworkInfo[] info = manager.getAllNetworkInfo();  
  167.             if (info != null) {  
  168.                 for (int i = 0; i < info.length; i++) {  
  169.                     if (info[i].getState() == NetworkInfo.State.CONNECTED) {  
  170.                         return true;  
  171.                     }  
  172.                 }  
  173.             }  
  174.         }  
  175.         openWirelessSet();  
  176.         return false;  
  177.     }  
  178.   
  179.     @Override  
  180.     public boolean hasLocationGPS() {  
  181.         LocationManager manager = (LocationManager) context  
  182.                 .getSystemService(context.LOCATION_SERVICE);  
  183.         if (manager  
  184.                 .isProviderEnabled(android.location.LocationManager.GPS_PROVIDER)) {  
  185.             return true;  
  186.         } else {  
  187.             return false;  
  188.         }  
  189.     }  
  190.   
  191.     @Override  
  192.     public boolean hasLocationNetWork() {  
  193.         LocationManager manager = (LocationManager) context  
  194.                 .getSystemService(context.LOCATION_SERVICE);  
  195.         if (manager  
  196.                 .isProviderEnabled(android.location.LocationManager.NETWORK_PROVIDER)) {  
  197.             return true;  
  198.         } else {  
  199.             return false;  
  200.         }  
  201.     }  
  202.   
  203.     @Override  
  204.     public void checkMemoryCard() {  
  205.         if (!Environment.MEDIA_MOUNTED.equals(Environment  
  206.                 .getExternalStorageState())) {  
  207.             new AlertDialog.Builder(context)  
  208.                     .setTitle(R.string.prompt)  
  209.                     .setMessage("请检查内存卡")  
  210.                     .setPositiveButton(R.string.menu_settings,  
  211.                             new DialogInterface.OnClickListener() {  
  212.                                 @Override  
  213.                                 public void onClick(DialogInterface dialog,  
  214.                                         int which) {  
  215.                                     dialog.cancel();  
  216.                                     Intent intent = new Intent(  
  217.                                             Settings.ACTION_SETTINGS);  
  218.                                     context.startActivity(intent);  
  219.                                 }  
  220.                             })  
  221.                     .setNegativeButton("退出",  
  222.                             new DialogInterface.OnClickListener() {  
  223.                                 @Override  
  224.                                 public void onClick(DialogInterface dialog,  
  225.                                         int which) {  
  226.                                     dialog.cancel();  
  227.                                     eimApplication.exit();  
  228.                                 }  
  229.                             }).create().show();  
  230.         }  
  231.     }  
  232.   
  233.     public void openWirelessSet() {  
  234.         AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);  
  235.         dialogBuilder  
  236.                 .setTitle(R.string.prompt)  
  237.                 .setMessage(context.getString(R.string.check_connection))  
  238.                 .setPositiveButton(R.string.menu_settings,  
  239.                         new DialogInterface.OnClickListener() {  
  240.                             @Override  
  241.                             public void onClick(DialogInterface dialog,  
  242.                                     int which) {  
  243.                                 dialog.cancel();  
  244.                                 Intent intent = new Intent(  
  245.                                         Settings.ACTION_WIRELESS_SETTINGS);  
  246.                                 context.startActivity(intent);  
  247.                             }  
  248.                         })  
  249.                 .setNegativeButton(R.string.close,  
  250.                         new DialogInterface.OnClickListener() {  
  251.                             @Override  
  252.                             public void onClick(DialogInterface dialog,  
  253.                                     int whichButton) {  
  254.                                 dialog.cancel();  
  255.                             }  
  256.                         });  
  257.         dialogBuilder.show();  
  258.     }  
  259.   
  260.     /** 
  261.      *  
  262.      * 显示toast 
  263.      *  
  264.      * @param text 
  265.      * @param longint 
  266.      * @author shimiso 
  267.      * @update 2012-6-28 下午3:46:18 
  268.      */  
  269.     public void showToast(String text, int longint) {  
  270.         Toast.makeText(context, text, longint).show();  
  271.     }  
  272.   
  273.     @Override  
  274.     public void showToast(String text) {  
  275.         Toast.makeText(context, text, Toast.LENGTH_SHORT).show();  
  276.     }  
  277.   
  278.     /** 
  279.      *  
  280.      * 关闭键盘事件 
  281.      *  
  282.      * @author shimiso 
  283.      * @update 2012-7-4 下午2:34:34 
  284.      */  
  285.     public void closeInput() {  
  286.         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);  
  287.         if (inputMethodManager != null && this.getCurrentFocus() != null) {  
  288.             inputMethodManager.hideSoftInputFromWindow(this.getCurrentFocus()  
  289.                     .getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);  
  290.         }  
  291.     }  
  292.   
  293.     /** 
  294.      *  
  295.      * 发出Notification的method. 
  296.      *  
  297.      * @param iconId 
  298.      *            图标 
  299.      * @param contentTitle 
  300.      *            标题 
  301.      * @param contentText 
  302.      *            你内容 
  303.      * @param activity 
  304.      * @author shimiso 
  305.      * @update 2012-5-14 下午12:01:55 
  306.      */  
  307.     public void setNotiType(int iconId, String contentTitle,  
  308.             String contentText, Class activity, String from) {  
  309.         /* 
  310.          * 创建新的Intent,作为点击Notification留言条时, 会运行的Activity 
  311.          */  
  312.         Intent notifyIntent = new Intent(this, activity);  
  313.         notifyIntent.putExtra("to", from);  
  314.         // notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
  315.   
  316.         /* 创建PendingIntent作为设置递延运行的Activity */  
  317.         PendingIntent appIntent = PendingIntent.getActivity(this0,  
  318.                 notifyIntent, 0);  
  319.   
  320.         /* 创建Notication,并设置相关参数 */  
  321.         Notification myNoti = new Notification();  
  322.         // 点击自动消失  
  323.         myNoti.flags = Notification.FLAG_AUTO_CANCEL;  
  324.         /* 设置statusbar显示的icon */  
  325.         myNoti.icon = iconId;  
  326.         /* 设置statusbar显示的文字信息 */  
  327.         myNoti.tickerText = contentTitle;  
  328.         /* 设置notification发生时同时发出默认声音 */  
  329.         myNoti.defaults = Notification.DEFAULT_SOUND;  
  330.         /* 设置Notification留言条的参数 */  
  331.         myNoti.setLatestEventInfo(this, contentTitle, contentText, appIntent);  
  332.         /* 送出Notification */  
  333.         notificationManager.notify(0, myNoti);  
  334.     }  
  335.   
  336.     @Override  
  337.     public Context getContext() {  
  338.         return context;  
  339.     }  
  340.   
  341.     @Override  
  342.     public SharedPreferences getLoginUserSharedPre() {  
  343.         return preferences;  
  344.     }  
  345.   
  346.     @Override  
  347.     public void saveLoginConfig(LoginConfig loginConfig) {  
  348.         preferences.edit()  
  349.                 .putString(Constant.XMPP_HOST, loginConfig.getXmppHost())  
  350.                 .commit();  
  351.         preferences.edit()  
  352.                 .putInt(Constant.XMPP_PORT, loginConfig.getXmppPort()).commit();  
  353.         preferences  
  354.                 .edit()  
  355.                 .putString(Constant.XMPP_SEIVICE_NAME,  
  356.                         loginConfig.getXmppServiceName()).commit();  
  357.         preferences.edit()  
  358.                 .putString(Constant.USERNAME, loginConfig.getUsername())  
  359.                 .commit();  
  360.         preferences.edit()  
  361.                 .putString(Constant.PASSWORD, loginConfig.getPassword())  
  362.                 .commit();  
  363.         preferences.edit()  
  364.                 .putBoolean(Constant.IS_AUTOLOGIN, loginConfig.isAutoLogin())  
  365.                 .commit();  
  366.         preferences.edit()  
  367.                 .putBoolean(Constant.IS_NOVISIBLE, loginConfig.isNovisible())  
  368.                 .commit();  
  369.         preferences.edit()  
  370.                 .putBoolean(Constant.IS_REMEMBER, loginConfig.isRemember())  
  371.                 .commit();  
  372.         preferences.edit()  
  373.                 .putBoolean(Constant.IS_ONLINE, loginConfig.isOnline())  
  374.                 .commit();  
  375.         preferences.edit()  
  376.                 .putBoolean(Constant.IS_FIRSTSTART, loginConfig.isFirstStart())  
  377.                 .commit();  
  378.     }  
  379.   
  380.     @Override  
  381.     public LoginConfig getLoginConfig() {  
  382.         LoginConfig loginConfig = new LoginConfig();  
  383.         String a = preferences.getString(Constant.XMPP_HOST, null);  
  384.         String b = getResources().getString(R.string.xmpp_host);  
  385.         loginConfig.setXmppHost(preferences.getString(Constant.XMPP_HOST,  
  386.                 getResources().getString(R.string.xmpp_host)));  
  387.         loginConfig.setXmppPort(preferences.getInt(Constant.XMPP_PORT,  
  388.                 getResources().getInteger(R.integer.xmpp_port)));  
  389.         loginConfig.setUsername(preferences.getString(Constant.USERNAME, null));  
  390.         loginConfig.setPassword(preferences.getString(Constant.PASSWORD, null));  
  391.         loginConfig.setXmppServiceName(preferences.getString(  
  392.                 Constant.XMPP_SEIVICE_NAME,  
  393.                 getResources().getString(R.string.xmpp_service_name)));  
  394.         loginConfig.setAutoLogin(preferences.getBoolean(Constant.IS_AUTOLOGIN,  
  395.                 getResources().getBoolean(R.bool.is_autologin)));  
  396.         loginConfig.setNovisible(preferences.getBoolean(Constant.IS_NOVISIBLE,  
  397.                 getResources().getBoolean(R.bool.is_novisible)));  
  398.         loginConfig.setRemember(preferences.getBoolean(Constant.IS_REMEMBER,  
  399.                 getResources().getBoolean(R.bool.is_remember)));  
  400.         loginConfig.setFirstStart(preferences.getBoolean(  
  401.                 Constant.IS_FIRSTSTART, true));  
  402.         return loginConfig;  
  403.     }  
  404.   
  405.     @Override  
  406.     public boolean getUserOnlineState() {  
  407.         // preferences = getSharedPreferences(Constant.LOGIN_SET,0);  
  408.         return preferences.getBoolean(Constant.IS_ONLINE, true);  
  409.     }  
  410.   
  411.     @Override  
  412.     public void setUserOnlineState(boolean isOnline) {  
  413.         // preferences = getSharedPreferences(Constant.LOGIN_SET,0);  
  414.         preferences.edit().putBoolean(Constant.IS_ONLINE, isOnline).commit();  
  415.   
  416.     }  
  417.   
  418.     @Override  
  419.     public EimApplication getEimApplication() {  
  420.         return eimApplication;  
  421.     }  
  422. }  

大家写android程序会发现,不同的activity之间经常需要调用一些公共的资源,这里的资源不仅包括android自身的,还有我们自己的管理服务类,甚至相互之间传递一些参数,这里我仿照struts2的设计,提炼出一个ActivitySupport类,同时抽取一个接口,让所有的Activity都集成这个类,因为有了接口,我们便可以采用回调模式,非常方便的传递数据和使用公共的资源,这种好处相信大家使用之后都能有深刻的体会,通过接口回调传递参数和相互调用的方式无疑是最优雅的,spring和hibernate源码中曾经大量使用这种结构。

2.SQLiteTemplate类
[java] view plaincopy
  1. package csdn.shimiso.eim.db;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. import android.content.ContentValues;  
  7. import android.database.Cursor;  
  8. import android.database.sqlite.SQLiteDatabase;  
  9.   
  10. /** 
  11.  * SQLite数据库模板工具类 
  12.  *  
  13.  * 该类提供了数据库操作常用的增删改查,以及各种复杂条件匹配,分页,排序等操作 
  14.  *  
  15.  * @see SQLiteDatabase 
  16.  */  
  17. public class SQLiteTemplate {  
  18.     /** 
  19.      * Default Primary key 
  20.      */  
  21.     protected String mPrimaryKey = "_id";  
  22.   
  23.     /** 
  24.      * DBManager 
  25.      */  
  26.     private DBManager dBManager;  
  27.     /** 
  28.      * 是否为一个事务 
  29.      */  
  30.     private boolean isTransaction = false;  
  31.     /** 
  32.      * 数据库连接 
  33.      */  
  34.     private SQLiteDatabase dataBase = null;  
  35.   
  36.     private SQLiteTemplate() {  
  37.     }  
  38.   
  39.     private SQLiteTemplate(DBManager dBManager, boolean isTransaction) {  
  40.         this.dBManager = dBManager;  
  41.         this.isTransaction = isTransaction;  
  42.     }  
  43.   
  44.     /** 
  45.      * isTransaction 是否属于一个事务 注:一旦isTransaction设为true 
  46.      * 所有的SQLiteTemplate方法都不会自动关闭资源,需在事务成功后手动关闭 
  47.      *  
  48.      * @return 
  49.      */  
  50.     public static SQLiteTemplate getInstance(DBManager dBManager,  
  51.             boolean isTransaction) {  
  52.         return new SQLiteTemplate(dBManager, isTransaction);  
  53.     }  
  54.   
  55.     /** 
  56.      * 执行一条sql语句 
  57.      *  
  58.      * @param name 
  59.      * @param tel 
  60.      */  
  61.     public void execSQL(String sql) {  
  62.         try {  
  63.             dataBase = dBManager.openDatabase();  
  64.             dataBase.execSQL(sql);  
  65.         } catch (Exception e) {  
  66.             e.printStackTrace();  
  67.         } finally {  
  68.             if (!isTransaction) {  
  69.                 closeDatabase(null);  
  70.             }  
  71.         }  
  72.     }  
  73.   
  74.     /** 
  75.      * 执行一条sql语句 
  76.      *  
  77.      * @param name 
  78.      * @param tel 
  79.      */  
  80.     public void execSQL(String sql, Object[] bindArgs) {  
  81.         try {  
  82.             dataBase = dBManager.openDatabase();  
  83.             dataBase.execSQL(sql, bindArgs);  
  84.         } catch (Exception e) {  
  85.             e.printStackTrace();  
  86.         } finally {  
  87.             if (!isTransaction) {  
  88.                 closeDatabase(null);  
  89.             }  
  90.         }  
  91.     }  
  92.   
  93.     /** 
  94.      * 向数据库表中插入一条数据 
  95.      *  
  96.      * @param table 
  97.      *            表名 
  98.      * @param content 
  99.      *            字段值 
  100.      */  
  101.     public long insert(String table, ContentValues content) {  
  102.         try {  
  103.             dataBase = dBManager.openDatabase();  
  104.             // insert方法第一参数:数据库表名,第二个参数如果CONTENT为空时则向表中插入一个NULL,第三个参数为插入的内容  
  105.             return dataBase.insert(table, null, content);  
  106.         } catch (Exception e) {  
  107.             e.printStackTrace();  
  108.         } finally {  
  109.             if (!isTransaction) {  
  110.                 closeDatabase(null);  
  111.             }  
  112.         }  
  113.         return 0;  
  114.     }  
  115.   
  116.     /** 
  117.      * 批量删除指定主键数据 
  118.      *  
  119.      * @param ids 
  120.      */  
  121.     public void deleteByIds(String table, Object... primaryKeys) {  
  122.         try {  
  123.             if (primaryKeys.length > 0) {  
  124.                 StringBuilder sb = new StringBuilder();  
  125.                 for (@SuppressWarnings("unused")  
  126.                 Object id : primaryKeys) {  
  127.                     sb.append("?").append(",");  
  128.                 }  
  129.                 sb.deleteCharAt(sb.length() - 1);  
  130.                 dataBase = dBManager.openDatabase();  
  131.                 dataBase.execSQL("delete from " + table + " where "  
  132.                         + mPrimaryKey + " in(" + sb + ")",  
  133.                         (Object[]) primaryKeys);  
  134.             }  
  135.         } catch (Exception e) {  
  136.             e.printStackTrace();  
  137.         } finally {  
  138.             if (!isTransaction) {  
  139.                 closeDatabase(null);  
  140.             }  
  141.         }  
  142.     }  
  143.   
  144.     /** 
  145.      * 根据某一个字段和值删除一行数据, 如 name="jack" 
  146.      *  
  147.      * @param table 
  148.      * @param field 
  149.      * @param value 
  150.      * @return 返回值大于0表示删除成功 
  151.      */  
  152.     public int deleteByField(String table, String field, String value) {  
  153.         try {  
  154.             dataBase = dBManager.openDatabase();  
  155.             return dataBase.delete(table, field + "=?"new String[] { value });  
  156.         } catch (Exception e) {  
  157.             e.printStackTrace();  
  158.         } finally {  
  159.             if (!isTransaction) {  
  160.                 closeDatabase(null);  
  161.             }  
  162.         }  
  163.         return 0;  
  164.     }  
  165.   
  166.     /** 
  167.      * 根据条件删除数据 
  168.      *  
  169.      * @param table 
  170.      *            表名 
  171.      * @param whereClause 
  172.      *            查询语句 参数采用? 
  173.      * @param whereArgs 
  174.      *            参数值 
  175.      * @return 返回值大于0表示删除成功 
  176.      */  
  177.     public int deleteByCondition(String table, String whereClause,  
  178.             String[] whereArgs) {  
  179.         try {  
  180.             dataBase = dBManager.openDatabase();  
  181.             return dataBase.delete(table, whereClause, whereArgs);  
  182.         } catch (Exception e) {  
  183.             e.printStackTrace();  
  184.         } finally {  
  185.             if (!isTransaction) {  
  186.                 closeDatabase(null);  
  187.             }  
  188.         }  
  189.         return 0;  
  190.     }  
  191.   
  192.     /** 
  193.      * 根据主键删除一行数据 
  194.      *  
  195.      * @param table 
  196.      * @param id 
  197.      * @return 返回值大于0表示删除成功 
  198.      */  
  199.     public int deleteById(String table, String id) {  
  200.         try {  
  201.             dataBase = dBManager.openDatabase();  
  202.             return deleteByField(table, mPrimaryKey, id);  
  203.         } catch (Exception e) {  
  204.             e.printStackTrace();  
  205.         } finally {  
  206.             if (!isTransaction) {  
  207.                 closeDatabase(null);  
  208.             }  
  209.         }  
  210.         return 0;  
  211.     }  
  212.   
  213.     /** 
  214.      * 根据主键更新一行数据 
  215.      *  
  216.      * @param table 
  217.      * @param id 
  218.      * @param values 
  219.      * @return 返回值大于0表示更新成功 
  220.      */  
  221.     public int updateById(String table, String id, ContentValues values) {  
  222.         try {  
  223.             dataBase = dBManager.openDatabase();  
  224.             return dataBase.update(table, values, mPrimaryKey + "=?",  
  225.                     new String[] { id });  
  226.         } catch (Exception e) {  
  227.             e.printStackTrace();  
  228.         } finally {  
  229.             if (!isTransaction) {  
  230.                 closeDatabase(null);  
  231.             }  
  232.         }  
  233.         return 0;  
  234.     }  
  235.   
  236.     /** 
  237.      * 更新数据 
  238.      *  
  239.      * @param table 
  240.      * @param values 
  241.      * @param whereClause 
  242.      * @param whereArgs 
  243.      * @return 返回值大于0表示更新成功 
  244.      */  
  245.     public int update(String table, ContentValues values, String whereClause,  
  246.             String[] whereArgs) {  
  247.         try {  
  248.             dataBase = dBManager.openDatabase();  
  249.             return dataBase.update(table, values, whereClause, whereArgs);  
  250.         } catch (Exception e) {  
  251.             e.printStackTrace();  
  252.         } finally {  
  253.             if (!isTransaction) {  
  254.                 closeDatabase(null);  
  255.             }  
  256.         }  
  257.         return 0;  
  258.     }  
  259.   
  260.     /** 
  261.      * 根据主键查看某条数据是否存在 
  262.      *  
  263.      * @param table 
  264.      * @param id 
  265.      * @return 
  266.      */  
  267.     public Boolean isExistsById(String table, String id) {  
  268.         try {  
  269.             dataBase = dBManager.openDatabase();  
  270.             return isExistsByField(table, mPrimaryKey, id);  
  271.         } catch (Exception e) {  
  272.             e.printStackTrace();  
  273.         } finally {  
  274.             if (!isTransaction) {  
  275.                 closeDatabase(null);  
  276.             }  
  277.         }  
  278.         return null;  
  279.     }  
  280.   
  281.     /** 
  282.      * 根据某字段/值查看某条数据是否存在 
  283.      *  
  284.      * @param status 
  285.      * @return 
  286.      */  
  287.     public Boolean isExistsByField(String table, String field, String value) {  
  288.         StringBuilder sql = new StringBuilder();  
  289.         sql.append("SELECT COUNT(*) FROM ").append(table).append(" WHERE ")  
  290.                 .append(field).append(" =?");  
  291.         try {  
  292.             dataBase = dBManager.openDatabase();  
  293.             return isExistsBySQL(sql.toString(), new String[] { value });  
  294.         } catch (Exception e) {  
  295.             e.printStackTrace();  
  296.         } finally {  
  297.             if (!isTransaction) {  
  298.                 closeDatabase(null);  
  299.             }  
  300.         }  
  301.         return null;  
  302.     }  
  303.   
  304.     /** 
  305.      * 使用SQL语句查看某条数据是否存在 
  306.      *  
  307.      * @param sql 
  308.      * @param selectionArgs 
  309.      * @return 
  310.      */  
  311.     public Boolean isExistsBySQL(String sql, String[] selectionArgs) {  
  312.         Cursor cursor = null;  
  313.         try {  
  314.             dataBase = dBManager.openDatabase();  
  315.             cursor = dataBase.rawQuery(sql, selectionArgs);  
  316.             if (cursor.moveToFirst()) {  
  317.                 return (cursor.getInt(0) > 0);  
  318.             } else {  
  319.                 return false;  
  320.             }  
  321.         } catch (Exception e) {  
  322.             e.printStackTrace();  
  323.         } finally {  
  324.             if (!isTransaction) {  
  325.                 closeDatabase(cursor);  
  326.             }  
  327.         }  
  328.         return null;  
  329.     }  
  330.   
  331.     /** 
  332.      * 查询一条数据 
  333.      *  
  334.      * @param rowMapper 
  335.      * @param sql 
  336.      * @param args 
  337.      * @return 
  338.      */  
  339.     public <T> T queryForObject(RowMapper<T> rowMapper, String sql,  
  340.             String[] args) {  
  341.         Cursor cursor = null;  
  342.         T object = null;  
  343.         try {  
  344.             dataBase = dBManager.openDatabase();  
  345.             cursor = dataBase.rawQuery(sql, args);  
  346.             if (cursor.moveToFirst()) {  
  347.                 object = rowMapper.mapRow(cursor, cursor.getCount());  
  348.             }  
  349.         } finally {  
  350.             if (!isTransaction) {  
  351.                 closeDatabase(cursor);  
  352.             }  
  353.         }  
  354.         return object;  
  355.   
  356.     }  
  357.   
  358.     /** 
  359.      * 查询 
  360.      *  
  361.      * @param rowMapper 
  362.      * @param sql 
  363.      * @param startResult 
  364.      *            开始索引 注:第一条记录索引为0 
  365.      * @param maxResult 
  366.      *            步长 
  367.      * @return 
  368.      */  
  369.     public <T> List<T> queryForList(RowMapper<T> rowMapper, String sql,  
  370.             String[] selectionArgs) {  
  371.         Cursor cursor = null;  
  372.         List<T> list = null;  
  373.         try {  
  374.             dataBase = dBManager.openDatabase();  
  375.             cursor = dataBase.rawQuery(sql, selectionArgs);  
  376.             list = new ArrayList<T>();  
  377.             while (cursor.moveToNext()) {  
  378.                 list.add(rowMapper.mapRow(cursor, cursor.getPosition()));  
  379.             }  
  380.         } finally {  
  381.             if (!isTransaction) {  
  382.                 closeDatabase(cursor);  
  383.             }  
  384.         }  
  385.         return list;  
  386.     }  
  387.   
  388.     /** 
  389.      * 分页查询 
  390.      *  
  391.      * @param rowMapper 
  392.      * @param sql 
  393.      * @param startResult 
  394.      *            开始索引 注:第一条记录索引为0 
  395.      * @param maxResult 
  396.      *            步长 
  397.      * @return 
  398.      */  
  399.     public <T> List<T> queryForList(RowMapper<T> rowMapper, String sql,  
  400.             int startResult, int maxResult) {  
  401.         Cursor cursor = null;  
  402.         List<T> list = null;  
  403.         try {  
  404.             dataBase = dBManager.openDatabase();  
  405.             cursor = dataBase.rawQuery(sql + " limit ?,?"new String[] {  
  406.                     String.valueOf(startResult), String.valueOf(maxResult) });  
  407.             list = new ArrayList<T>();  
  408.             while (cursor.moveToNext()) {  
  409.                 list.add(rowMapper.mapRow(cursor, cursor.getPosition()));  
  410.             }  
  411.         } finally {  
  412.             if (!isTransaction) {  
  413.                 closeDatabase(cursor);  
  414.             }  
  415.         }  
  416.         return list;  
  417.     }  
  418.   
  419.     /** 
  420.      * 获取记录数 
  421.      *  
  422.      * @return 
  423.      */  
  424.     public Integer getCount(String sql, String[] args) {  
  425.         Cursor cursor = null;  
  426.         try {  
  427.             dataBase = dBManager.openDatabase();  
  428.             cursor = dataBase.rawQuery("select count(*) from (" + sql + ")",  
  429.                     args);  
  430.             if (cursor.moveToNext()) {  
  431.                 return cursor.getInt(0);  
  432.             }  
  433.         } catch (Exception e) {  
  434.             e.printStackTrace();  
  435.         } finally {  
  436.             if (!isTransaction) {  
  437.                 closeDatabase(cursor);  
  438.             }  
  439.         }  
  440.         return 0;  
  441.     }  
  442.   
  443.     /** 
  444.      * 分页查询 
  445.      *  
  446.      * @param rowMapper 
  447.      * @param table 
  448.      *            检索的表 
  449.      * @param columns 
  450.      *            由需要返回列的列名所组成的字符串数组,传入null会返回所有的列。 
  451.      * @param selection 
  452.      *            查询条件子句,相当于select语句where关键字后面的部分,在条件子句允许使用占位符"?" 
  453.      * @param selectionArgs 
  454.      *            对应于selection语句中占位符的值,值在数组中的位置与占位符在语句中的位置必须一致,否则就会有异常 
  455.      * @param groupBy 
  456.      *            对结果集进行分组的group by语句(不包括GROUP BY关键字)。传入null将不对结果集进行分组 
  457.      * @param having 
  458.      *            对查询后的结果集进行过滤,传入null则不过滤 
  459.      * @param orderBy 
  460.      *            对结果集进行排序的order by语句(不包括ORDER BY关键字)。传入null将对结果集使用默认的排序 
  461.      * @param limit 
  462.      *            指定偏移量和获取的记录数,相当于select语句limit关键字后面的部分,如果为null则返回所有行 
  463.      * @return 
  464.      */  
  465.     public <T> List<T> queryForList(RowMapper<T> rowMapper, String table,  
  466.             String[] columns, String selection, String[] selectionArgs,  
  467.             String groupBy, String having, String orderBy, String limit) {  
  468.         List<T> list = null;  
  469.         Cursor cursor = null;  
  470.         try {  
  471.             dataBase = dBManager.openDatabase();  
  472.             cursor = dataBase.query(table, columns, selection, selectionArgs,  
  473.                     groupBy, having, orderBy, limit);  
  474.             list = new ArrayList<T>();  
  475.             while (cursor.moveToNext()) {  
  476.                 list.add(rowMapper.mapRow(cursor, cursor.getPosition()));  
  477.             }  
  478.         } finally {  
  479.             if (!isTransaction) {  
  480.                 closeDatabase(cursor);  
  481.             }  
  482.         }  
  483.         return list;  
  484.     }  
  485.   
  486.     /** 
  487.      * Get Primary Key 
  488.      *  
  489.      * @return 
  490.      */  
  491.     public String getPrimaryKey() {  
  492.         return mPrimaryKey;  
  493.     }  
  494.   
  495.     /** 
  496.      * Set Primary Key 
  497.      *  
  498.      * @param primaryKey 
  499.      */  
  500.     public void setPrimaryKey(String primaryKey) {  
  501.         this.mPrimaryKey = primaryKey;  
  502.     }  
  503.   
  504.     /** 
  505.      *  
  506.      * @author shimiso 
  507.      *  
  508.      * @param <T> 
  509.      */  
  510.     public interface RowMapper<T> {  
  511.         /** 
  512.          *  
  513.          * @param cursor 
  514.          *            游标 
  515.          * @param index 
  516.          *            下标索引 
  517.          * @return 
  518.          */  
  519.         public T mapRow(Cursor cursor, int index);  
  520.     }  
  521.   
  522.     /** 
  523.      * 关闭数据库 
  524.      */  
  525.     public void closeDatabase(Cursor cursor) {  
  526.         if (null != dataBase) {  
  527.             dataBase.close();  
  528.         }  
  529.         if (null != cursor) {  
  530.             cursor.close();  
  531.         }  
  532.     }  
  533. }  

我们希望在android操作数据库是优雅的一种方式,这里不必关注事务,也不用担心分页,更不用为了封装传递对象烦恼,总之一切就像面向对象那样,简单,模板类的出现正是解决这个问题,虽然它看上去可能不是那么完美有待提高,这里我封装了很多sqlite常用的工具,大家可以借鉴使用。

3.XmppConnectionManager管理类
[java] view plaincopy
  1. package csdn.shimiso.eim.manager;  
  2.   
  3. import org.jivesoftware.smack.Connection;  
  4. import org.jivesoftware.smack.ConnectionConfiguration;  
  5. import org.jivesoftware.smack.Roster;  
  6. import org.jivesoftware.smack.XMPPConnection;  
  7. import org.jivesoftware.smack.provider.ProviderManager;  
  8. import org.jivesoftware.smackx.GroupChatInvitation;  
  9. import org.jivesoftware.smackx.PrivateDataManager;  
  10. import org.jivesoftware.smackx.packet.ChatStateExtension;  
  11. import org.jivesoftware.smackx.packet.LastActivity;  
  12. import org.jivesoftware.smackx.packet.OfflineMessageInfo;  
  13. import org.jivesoftware.smackx.packet.OfflineMessageRequest;  
  14. import org.jivesoftware.smackx.packet.SharedGroupsInfo;  
  15. import org.jivesoftware.smackx.provider.DataFormProvider;  
  16. import org.jivesoftware.smackx.provider.DelayInformationProvider;  
  17. import org.jivesoftware.smackx.provider.DiscoverInfoProvider;  
  18. import org.jivesoftware.smackx.provider.DiscoverItemsProvider;  
  19. import org.jivesoftware.smackx.provider.MUCAdminProvider;  
  20. import org.jivesoftware.smackx.provider.MUCOwnerProvider;  
  21. import org.jivesoftware.smackx.provider.MUCUserProvider;  
  22. import org.jivesoftware.smackx.provider.MessageEventProvider;  
  23. import org.jivesoftware.smackx.provider.MultipleAddressesProvider;  
  24. import org.jivesoftware.smackx.provider.RosterExchangeProvider;  
  25. import org.jivesoftware.smackx.provider.StreamInitiationProvider;  
  26. import org.jivesoftware.smackx.provider.VCardProvider;  
  27. import org.jivesoftware.smackx.provider.XHTMLExtensionProvider;  
  28. import org.jivesoftware.smackx.search.UserSearch;  
  29.   
  30. import csdn.shimiso.eim.model.LoginConfig;  
  31.   
  32. /** 
  33.  *  
  34.  * XMPP服务器连接工具类. 
  35.  *  
  36.  * @author shimiso 
  37.  */  
  38. public class XmppConnectionManager {  
  39.     private XMPPConnection connection;  
  40.     private static ConnectionConfiguration connectionConfig;  
  41.     private static XmppConnectionManager xmppConnectionManager;  
  42.   
  43.     private XmppConnectionManager() {  
  44.   
  45.     }  
  46.   
  47.     public static XmppConnectionManager getInstance() {  
  48.         if (xmppConnectionManager == null) {  
  49.             xmppConnectionManager = new XmppConnectionManager();  
  50.         }  
  51.         return xmppConnectionManager;  
  52.     }  
  53.   
  54.     // init  
  55.     public XMPPConnection init(LoginConfig loginConfig) {  
  56.         Connection.DEBUG_ENABLED = false;  
  57.         ProviderManager pm = ProviderManager.getInstance();  
  58.         configure(pm);  
  59.   
  60.         connectionConfig = new ConnectionConfiguration(  
  61.                 loginConfig.getXmppHost(), loginConfig.getXmppPort(),  
  62.                 loginConfig.getXmppServiceName());  
  63.         connectionConfig.setSASLAuthenticationEnabled(false);// 不使用SASL验证,设置为false  
  64.         connectionConfig  
  65.                 .setSecurityMode(ConnectionConfiguration.SecurityMode.enabled);  
  66.         // 允许自动连接  
  67.         connectionConfig.setReconnectionAllowed(false);  
  68.         // 允许登陆成功后更新在线状态  
  69.         connectionConfig.setSendPresence(true);  
  70.         // 收到好友邀请后manual表示需要经过同意,accept_all表示不经同意自动为好友  
  71.         Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual);  
  72.         connection = new XMPPConnection(connectionConfig);  
  73.         return connection;  
  74.     }  
  75.   
  76.     /** 
  77.      *  
  78.      * 返回一个有效的xmpp连接,如果无效则返回空. 
  79.      *  
  80.      * @return 
  81.      * @author shimiso 
  82.      * @update 2012-7-4 下午6:54:31 
  83.      */  
  84.     public XMPPConnection getConnection() {  
  85.         if (connection == null) {  
  86.             throw new RuntimeException("请先初始化XMPPConnection连接");  
  87.         }  
  88.         return connection;  
  89.     }  
  90.   
  91.     /** 
  92.      *  
  93.      * 销毁xmpp连接. 
  94.      *  
  95.      * @author shimiso 
  96.      * @update 2012-7-4 下午6:55:03 
  97.      */  
  98.     public void disconnect() {  
  99.         if (connection != null) {  
  100.             connection.disconnect();  
  101.         }  
  102.     }  
  103.   
  104.     public void configure(ProviderManager pm) {  
  105.   
  106.         // Private Data Storage  
  107.         pm.addIQProvider("query""jabber:iq:private",  
  108.                 new PrivateDataManager.PrivateDataIQProvider());  
  109.   
  110.         // Time  
  111.         try {  
  112.             pm.addIQProvider("query""jabber:iq:time",  
  113.                     Class.forName("org.jivesoftware.smackx.packet.Time"));  
  114.         } catch (ClassNotFoundException e) {  
  115.         }  
  116.   
  117.         // XHTML  
  118.         pm.addExtensionProvider("html""http://jabber.org/protocol/xhtml-im",  
  119.                 new XHTMLExtensionProvider());  
  120.   
  121.         // Roster Exchange  
  122.         pm.addExtensionProvider("x""jabber:x:roster",  
  123.                 new RosterExchangeProvider());  
  124.         // Message Events  
  125.         pm.addExtensionProvider("x""jabber:x:event",  
  126.                 new MessageEventProvider());  
  127.         // Chat State  
  128.         pm.addExtensionProvider("active",  
  129.                 "http://jabber.org/protocol/chatstates",  
  130.                 new ChatStateExtension.Provider());  
  131.         pm.addExtensionProvider("composing",  
  132.                 "http://jabber.org/protocol/chatstates",  
  133.                 new ChatStateExtension.Provider());  
  134.         pm.addExtensionProvider("paused",  
  135.                 "http://jabber.org/protocol/chatstates",  
  136.                 new ChatStateExtension.Provider());  
  137.         pm.addExtensionProvider("inactive",  
  138.                 "http://jabber.org/protocol/chatstates",  
  139.                 new ChatStateExtension.Provider());  
  140.         pm.addExtensionProvider("gone",  
  141.                 "http://jabber.org/protocol/chatstates",  
  142.                 new ChatStateExtension.Provider());  
  143.   
  144.         // FileTransfer  
  145.         pm.addIQProvider("si""http://jabber.org/protocol/si",  
  146.                 new StreamInitiationProvider());  
  147.   
  148.         // Group Chat Invitations  
  149.         pm.addExtensionProvider("x""jabber:x:conference",  
  150.                 new GroupChatInvitation.Provider());  
  151.         // Service Discovery # Items  
  152.         pm.addIQProvider("query""http://jabber.org/protocol/disco#items",  
  153.                 new DiscoverItemsProvider());  
  154.         // Service Discovery # Info  
  155.         pm.addIQProvider("query""http://jabber.org/protocol/disco#info",  
  156.                 new DiscoverInfoProvider());  
  157.         // Data Forms  
  158.         pm.addExtensionProvider("x""jabber:x:data"new DataFormProvider());  
  159.         // MUC User  
  160.         pm.addExtensionProvider("x""http://jabber.org/protocol/muc#user",  
  161.                 new MUCUserProvider());  
  162.         // MUC Admin  
  163.         pm.addIQProvider("query""http://jabber.org/protocol/muc#admin",  
  164.                 new MUCAdminProvider());  
  165.         // MUC Owner  
  166.         pm.addIQProvider("query""http://jabber.org/protocol/muc#owner",  
  167.                 new MUCOwnerProvider());  
  168.         // Delayed Delivery  
  169.         pm.addExtensionProvider("x""jabber:x:delay",  
  170.                 new DelayInformationProvider());  
  171.         // Version  
  172.         try {  
  173.             pm.addIQProvider("query""jabber:iq:version",  
  174.                     Class.forName("org.jivesoftware.smackx.packet.Version"));  
  175.         } catch (ClassNotFoundException e) {  
  176.         }  
  177.         // VCard  
  178.         pm.addIQProvider("vCard""vcard-temp"new VCardProvider());  
  179.         // Offline Message Requests  
  180.         pm.addIQProvider("offline""http://jabber.org/protocol/offline",  
  181.                 new OfflineMessageRequest.Provider());  
  182.         // Offline Message Indicator  
  183.         pm.addExtensionProvider("offline",  
  184.                 "http://jabber.org/protocol/offline",  
  185.                 new OfflineMessageInfo.Provider());  
  186.         // Last Activity  
  187.         pm.addIQProvider("query""jabber:iq:last"new LastActivity.Provider());  
  188.         // User Search  
  189.         pm.addIQProvider("query""jabber:iq:search"new UserSearch.Provider());  
  190.         // SharedGroupsInfo  
  191.         pm.addIQProvider("sharedgroup",  
  192.                 "http://www.jivesoftware.org/protocol/sharedgroup",  
  193.                 new SharedGroupsInfo.Provider());  
  194.         // JEP-33: Extended Stanza Addressing  
  195.         pm.addExtensionProvider("addresses",  
  196.                 "http://jabber.org/protocol/address",  
  197.                 new MultipleAddressesProvider());  
  198.   
  199.     }  
  200. }  

这个类是xmpp连接的管理类,如果大家使用smack的api对这个应该不会陌生,asmack对xmpp连接的管理,与smack的差别不大,但是部分细微区别也有,我们在使用中如果遇到问题,还要多加注意,我们这里将其设计成单例,毕竟重复创建连接是个非常消耗的过程。

3.演示效果

 

基于xmpp openfire smack开发之Android客户端开发[3] 基于xmpp openfire smack开发之Android客户端开发[3] 基于xmpp openfire smack开发之Android客户端开发[3] 

很像QQ吧,没错,这是2012年版本qq的安卓界面,只是界面元素一样,实现方式大不相同,下面简单列一下这个客户端实现的功能:
1.聊天
2.离线消息
3.添加,删除好友
4.添加,移动好友分组
5.设置昵称
6.监控好友状态
7.网络断开系统自动重连接
8.收到添加好友请求消息处理
9.收到系统广播消息处理
10.查看历史聊天记录
11.消息弹出提醒,和小气泡
....
因为时间关系不是很完美,主要用于学习研究,欢迎大家给我提bug和改进意见。

4.源码下载

分数比较大,不是为了坑大家,是怕有伸手党出现,拿了源码出去招摇撞骗,请尊重作者原创!

http://download.csdn.net/detail/shimiso/6224163

上一篇:Windows 下安装 Node.js


下一篇:Swift中文教程(一)--欢迎来到Swift的世界