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

 

1.源码结构介绍


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

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

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

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

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

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

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

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



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.演示效果

 

   

很像QQ吧,没错,这是2012年版本qq的安卓界面,只是界面元素一样,实现方式大不相同,下面简单列一下这个客户端实现的功能:

1.聊天

2.离线消息

3.添加,删除好友

4.添加,移动好友分组

5.设置昵称

6.监控好友状态

7.网络断开系统自动重连接

8.收到添加好友请求消息处理

9.收到系统广播消息处理

10.查看历史聊天记录

11.消息弹出提醒,和小气泡

....

因为时间关系不是很完美,主要用于学习研究,欢迎大家给我提bug和改进意见。


4.源码下载

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



参阅文献
Openfirehttp://www.igniterealtime.org/
push-notificationhttp://www.push-notification.org/
Claros chathttp://www.claros.org/
androidpnsourceforgehttp://sourceforge.net/projects/androidpn/
android消息推送解决方案http://www.cnblogs.com/hanyonglu/archive/2012/03/04/2378971.html
xmpp协议实现原理介绍 http://www.cnblogs.com/hanyonglu/archive/2012/03/04/2378956.html


转载于:https://my.oschina.net/u/733847/blog/379518

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值