BES推送应用实例演示与分析


Page | 1 BES 推送应用实例演示与分析 作者: 杨江 Page | 2 目录 前言 ................................................................................................................................................. 3 第一部分 演示环境配置 ................................................................................................................ 5 第二部分 演示过程 ....................................................................................................................... 6 第三部分 手机端代码导入 Eclipse .............................................................................................. 11 第四部分 核心代码分析 .............................................................................................................. 16 后记 ............................................................................................................................................... 27 常见运行错误处理 ....................................................................................................................... 27 英文缩写说明 ............................................................................................................................... 29 参考资料 ....................................................................................................................................... 29 附录 ............................................................................................................................................... 30 Page | 3 前言 本文通过一个实际的应用案例 ECL (Emergency Contact List ) ,来介绍经典的 BlackBerry 通过 BES(BlackBerry Enterprise Server)数据推送的功能,包括了服务器端 和手机端的源代码。 主要演示的技术点包括:  服务端 Java 程序如何通过调用 BES 服务器推送功能向手机推送数据  手机端 Java 应用如何侦听并接收数据  手机端 Java 应用如何变换图标,以提醒用户有新的数据到达该手机 该代码实例可以运行在模拟器环境(MDS 模拟器 + BlackBerry 手机模拟器),也可以 运行在真实的(BES + BlackBerry 真机)环境。本文主要介绍了在模拟器环境下代码运 行的情况。 注: 为方便开发人员学习和使用 BlackBerry push 技术,黑莓官方网站上提供了一 个样例程序 Emergency Contact List(简称 ECL),包括 Java/ASP.NET/Domino 三种语言版本,支持浏览器 Push 和 Java 应用程序 Push。Java 版本的程序源代码 和运行脚本在网页 www.blackberry.com/go/ecl 上面上可以免费下载。在本文中, 我们将分析 Java 版本 ECL 样例程序。 各语言版本 ECL 程序的比较: ECL Version Database(s) Utilized(数据来源) Server-Side Technology Push Interface BlackBerry Client Implementation Java ECL Microsoft Excel Java EE 命令行 Java Application Browser Application ASP.NET ECL Microsoft Excel Microsoft Access Microsoft SQL Server .NET 命令行 GUI Windows® Java Application Browser Application Page | 4 Service Push Access Protocol (PAP) Domino® ECL IBM Lotus Notes Java EE 命令行 Lotus Notes 界 面 Java Application Browser Application 模拟业务场景: ECL (Emergency Contact List) 实例模拟了以下业务场景: 1. 用户 BlackBerry 手机端安装 ECL J2ME 客户端应用 2. 服务器端为一个 Excel 数据表(代表数据库) 3. 当在 Excel 表中加入新的数据或者修改了数据后,服务器端可以发起一个推 送,将更新后的 Excel 表内容全部推送到 BlackBerry 手机 4. BlackBerry 手机用户注意到手机端应用图标发生变化,表明有新数据到达手 机,即可在手机上打开应用,查看数据 Page | 5 第一部分 演示环境配置 这里描述了在一台电脑上,如何使用模拟器演示该代码实例。 需要安装的软件包括:  BlackBerry Email and MDS Services Simulators 4.1.4  BlackBerry Smartphone Simulators 5.0.0 (也可以使用 4.5, 4.6, 4.7 等版本,但需要通 过 Eclipse 修改载入不同 SDK,详细方法见附录)  Eclipse SDK 3.5.1 (可选,V3.4 也可以,用于研究代码用)  BlackBerry JDE Plug-in for Eclipse Version: 1.1.1.200911111641-15 安装以上软件在一台电脑上,一般采用缺省目录安装即可。 解压缩 ECL_Java.zip 到 c:\ECL_Java 目录下, 下面解释一下主要文件夹内容和其作用:  Readme.txt: 使用说明文档  目录 src: 代码源文件目录,包括手机端代码和服务器端  目录 bin: 可执行文件目录,用于实际演示用 Page | 6 第二部分 演示过程 1. 启动 BlackBerry Email and MDS Services Simulators 4.1.4 2. 启动 BlackBerry Smartphone Simulators 5.0.0 Page | 7 3. 安装 ECLSample.cod 文件到手机模拟器 模拟器: File  Load Java Program  选择 5.0 版本的 ECLSample.cod Page | 8 4. 在 DOS 命令行下输入命令: C:\ECL_Java\bin\Server>run catcher simulatoremai.txt xyzlist.xls 运行后显示 successful push 表示推送成功。 Page | 9 5. 手机端应用图标发生变化,然后进入应用查看信息 6. 修改 Excel 表内容, 到 C:\ECL_Java\bin\Server 目录下修改 xyzlist.xls 文件,如把内容修改一下, 例如将”John Peng” 改成 “Petter Liang” 7. 在 DOS 命令行下输入命令: C:\ECL_Java\bin\Server>run catcher simulatoremai.txt xyzlist.xls Page | 10 8. 手机端应用图标发生变化,然后查看更新后的信息 Page | 11 第三部分 手机端代码导入 Eclipse 1. 打开 Eclipse, 2. File – Import, 选择 General Existing Projects to Workspace Page | 12 Page | 13 Page | 14 Page | 15 Page | 16 第四部分 核心代码分析 BlackBerry Push 架构 在分析样例代码之前,首先让我们从整体上了解 BlackBerry Push 架构。 更加详细的 BlackBerry 推送机制的分析和介绍,请参考黑莓官方网站,以及参考资 料“BES 服务器推送机制分析”。 Page | 17 从示意图中,我们可以看到,在 BlackBerry 应用平台上的数据推送从整体上可以分 为六步,按时间顺序分别为: 1. 应用服务器向 MDS/BES 服务器发送推送请求,该请求为 HTTP POST 请求。 2/3:MDS/BES 服务器做必要的权限和数据监测,告知应用服务器推送请求是否被 接受并将被执行。 4:MDS/BES 服务器通过 BlackBerry Infrastructure 和无线网络把数据推送到手持设 备端。 5: 手持设备收到数据后,向 MDS/BES 服务器反馈。 6:MDS/BES 服务器告知应用服务器其推送数据是否送达手持设备。 ECL 系统架构 从 BlackBerry Push 系统架构上看,从服务器端 Push 数据到手机端,要经过多个服 务器和网络甚至无线基站,整个流程相当复杂。幸运的是,从应用开发的角度,程 序员可以不考虑 Push 的复杂底层实现,而只需简单地发送 Push 请求并监听 Push 请求的处理状态,客户端监听并接受推送数据即可: 1. 服务器程序向 MDS/BES 服务器发送推送请求,所发送的请求为 HTTP POST 请求。 2. 手持设备上的 Java 客户端程序接收 push 过来的数据,做出灯光闪烁/振铃 等提示,甚至修改桌面图标提示用户,用户打开程序界面查看数据。 3. 服务器程序从 MDS/BES 服务器获得推送请求是否完成的状态信息。 Page | 18 Java 版本 ECL 服务器代码分析 核心代码说明,Java 类列表 Java 版本 ECL 服务器程序是一个命令行程序,根据命令行参数把指定 excel 表格中 的联系人列表 contact list 数据推送给指定的手机上。 1. 主程序:是 eclTimedUpdate.java,这是一个包含 main()方法的控制程序,根 据不同参数调用两个 Pusher 类做 Browser Push 和 Customize Push。 2. 核心 Push 代码:由 BrowserChannelPusher.java/CustomAppPusher.java/Pusher.java 三个类构成。 3. 服务器配置信息:MdsProperties.java 等四个类用于读取配置文本文件,让 ECL 服务器主程序获得 MDS/BES 服务器 IP 地址端口号等信息。 4. 数据访问:DataReader.java 通过 JDBC-ODBC 接口读取 Excel 表单数据。 ECL 服务器程序各个 Java 类的功能说明列表如下: Java Class Description eclTimedUpdate.java 包含 main()方法的控制程序,根据不同参数调 用两个 Pusher 类做 Browser Push 和 Customize Push BrowserChannelPusher.java CustomAppPusher.java Pusher.java 核心的服务器 Push 代码,包括 Browser pusher 和 custom 应用 pusher。 Browser pusher 和 custom 应用 pusher 的共同父 类是 Pusher.java MdsProperties.java CatcherProperties.java ChannelProperties.java StronglyTypedPropertiesSet.java MdsProperties.java 读取 MDS 服务器参数,比如 BES 服务器主机名/IP 地址/端口号。 CatcherProperties.java 读取手机端监听端口。 ChannelProperties.java 读取浏览器 Push 相关参 数。 配置文件样本: # The name of the machine that hosts the MDS push server BesHostName = localhost Page | 19 # The port on which the MDS Push service is listening. # (Set equal to the "WebServer.listen.port" property for the MDS.) BesPushPort = 8080 # The port that the device-side catcher app is listening on. DeviceListenPort = 911 DataReader.java SpreadsheetProperties.java DataReader 通过 JDBC-ODBC 接口读取 Excel 表 单数据—紧急情况联系人列表。 eclTimedUpdate.java 代码分析 根据参数构造 Pusher 类(BrowserChannelPusher 或者 CustomAppPusher) 作为整个服务器端推送数据程序的入口,eclTimedUpdate.java 是一个标准的 J2SE 程序。 在 main()方法中首先分析第一个参数。如果参数值是 channel,则构造 pusher 为 BrowserChannelPusher 类;如果参数值是 catcher,则构造 pusher 为 CustomAppPusher 类。 根据参数从文本文件中读取 BlackBerry 手机 PIN 码列表 例如从 simulatoremail.txt 文件中读取到的 PIN 码为黑莓模拟器的 PIN 码 2100000a。 提示:BlackBerry MDS/BES 服务器支持以手机 PIN 码和 email 地址识别要推送的目 标手机。在 ECL 样例程序中是使用手机 PIN 码。 public static void main(String[] args) … // Process command-line arguments if (args.length >= 1) { if (args[0].equalsIgnoreCase("channel")) { // Construct a pusher that sends to a browser channel. pusher = new BrowserChannelPusher(); //Browser push也是一个很有趣的话题 } else if (args[0].equalsIgnoreCase("catcher")) { // Construct a pusher that sends to a custom catcher. pusher = new CustomAppPusher(); } } Page | 20 根据参数从 excel 表格文件中读取 ECL 联系人列表 在这里调用DataReader类访问Excel表,读取每个组和每个人的联系方式,并通过 pusher.addContact(dataFields); 方法把数据传递给pusher,最后调用 pusher.finishedConstruction(); 方法告诉pusher数据全部读取完毕。Pusher会把所有 联系人方式构造成一个xml字符串保存起来以备下一步调用 pusher.sendToHandheld(curRecipient);进行发送。 调用 pusher.sendToHandheld()推送数据到每一部黑莓手机上面 做一个 For 循环,多次调用 pusher.sendToHandheld(curRecipient)通过 BES/MDS 服 务器把数据发送到各个手机上面。 // Fetch the group names from the spreadsheet. Vector groupDescription = dataReader.getGroupList(); // Assemble the data of all contacts (from all groups) that we // will push to handhelds. for (int i = 0; i < groupDescription.size(); i++) { // Define the current group. pusher.beginGroup((String)groupDescription.elementAt(i)); // Add all its members. Vector groupContactList = dataReader.getContactList( (String)groupDescription.elementAt(i)); for (int j = 0; j < groupContactList.size(); j++) { String[] dataFields = dataReader.getContactData( (String)groupContactList.elementAt(j)); pusher.addContact(dataFields); } } // Indicate that we're done building the contacts list. After, // the pusher is ready to send the message multiple times. pusher.finishedConstruction(); if (args.length >= 2) { emailListFile = args[1]; } … … // Get the list of emails to push to. List recipientEmails = getRecipients(emailListFile); Page | 21 Pusher.java 代码分析 Pusher.java 代码实际上包括两部分内容,一部分是准备要推送的数据,一部分是真 正的推送操作。 BeginGroup(),addContact(),finishedConstruction()三个方法是用来准备要推送的数 据。主逻辑程序多次调用这三个方法告诉 Pusher 有哪些联系人数据要推送。 Pusher 会把这些数据拼装成一个 html 或者是一个大文本,并最终推送给手机浏 览器或者手机客户端程序。writeTo(OutputStream)方法用来把前面三个方法 构造出来的数据写到输出流中。 注意:以上 4 个方法和 push 无关,更好的做法应该是把他们分离出去做一个独 立的数据构造类。 Pusher.java代码的核心方法是sendToHandheld(String recipientEmail)。该方法构造标准的http POST请求给MDS/BES服务器,通 过参数DESTINATION指定要把数据push给哪些人(参数值可以是email地址或 者是手机PIN码),参数PORT告知要把数据推送到手机上面哪个端口(例子代码 中是端口911)。 // Push the message we just built to all recipients. for (int i = 0; i < recipientEmails.size(); i++) { String curRecipient = (String)recipientEmails.get(i); try { System.out.print("Contacting BES for " + curRecipient); pusher.sendToHandheld(curRecipient); System.out.println(" - successful push."); } catch (Pusher.MDSConnectionException ex) { // Unable to connect to MDS. The condition is unlikely // to be temporary so abort. System.out.println(" - " + ex.getMessage()); break; } catch (Pusher.MDSResponseException ex) { // Connected OK but MDS responded with error. Log it and // try the next email. System.out.println(" - MDS responded with " + ex.getMessage()); } catch (Exception ex) { // Unexpected error. Log a full stack trace and try the // next email. System.out.println(" - unexpected error:"); ex.printStackTrace(); } } Page | 22 Java 版本 ECL 客户端代码分析 核心代码说明,Java 类列表 客户端代码由三个 Java 类,两个图标文件,BlackBerry 应用描述文件构成。 ECL 客户端程序三个 Java 类的功能说明列表如下: /** * Pushes the already-constructed message to the indicated recipient. May * be used many times, but only in the "sending" state. */ public void sendToHandheld(String recipientEmail) throws IOException, MDSConnectionException, MDSResponseException { HttpURLConnection conn = null; OutputStream out = null; try { // Build the URL to define our connection to the BES. URL url = new URL("http", _props.getBesHostName(), _props.getBesPushPort(), "/push?DESTINATION=" + recipientEmail + "&PORT=" + getDevicePort() + "&REQUESTURI=/"); conn = (HttpURLConnection)url.openConnection(); conn.setDoOutput(true); //to post data conn.setRequestMethod("POST"); establishRequestHeaders(conn); //write the data to the http connection out = conn.getOutputStream(); writeTo(out); // Check the MDS's response so that we report an error if the push // was unsuccessful. int responseCode = conn.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_OK) { String serverMessage = conn.getResponseMessage(); throw new MDSResponseException( "HTTP-" + responseCode + ": " + serverMessage); } } Page | 23 Java Class PushedDataListener 在指定端口 911 上监听 push 过来的数据。收到 后调用 DataStore,修改手机上应用的图标以提 示用户有更新过的联系人列表。 ECLApplication 客户端主程序,通过配置,最终会在客户端启 动两个 Java 进程:后台运行的 PushedDataListener 用来监听 push 过来的数据; 前台 GUI 界面 ECLApplication 和内部类 EclScreen 用于展现收到的 push 数据—联系人列 表。 DataStore 使用 net.rim.device.api.system.PersistentStore 保存最新的ECL联系人员列表数据。 ECL 客户端程序的两个入口点(Alternate Entry Points) ECL 客户端程序是如何自动启动监听程序的?又是如何区分点击 icon 启动 GUI 界 面程序的呢?答案在应用描述文件 BlackBerry_App_Descriptor.xml 和主程序的 main()方法上。 打开 BlackBerry_App_Descriptor.xml,在 Application 标签栏目里面,你会看到 Auto-run on startup 被选中。这样,ECLSample 程序会在手机启动过程中自动地启 动,产生一个后台 Java 进程监听 Push 数据。 在 Alternate Entry Points 栏目中,设置了一个参数 Application argument 为 gui,并 单独设置了程序 Title 和 Icon,因此程序安装后在“下载”目录里面出现一个应用 图标。点击图标将产生一个前台 GUI 进程来读取并显示 Push 过来的数据。 Page | 24 ECLApplication 客户端主程序说明 在ECLApplication的main()方法中,如果没有任何参数传进来,那么就是说程序是在 手机启动的时候被初始化调用的,启动PushedDataListener线程在后台运行,监听 push来的数据。 Page | 25 在 ECLApplication 的 main()方法中,如果发现有参数传进来,那么说是有用户点击 了应用图标,想打开 GUI 界面查看数据。 在 ECLApplication 构造方法中,首先把应用的图标从未读状态,修改为已读状 态;然后调用 DataStore 类读取 PersistentStore 里面的数据,最后弹出 EclScreen 窗口以树状组件显示数据。 DataStore 和 EclScreen 代码因为和 Push 操作无关,这里就不再做代码分析 了。 class ECLApplication extends UiApplication { public static void main(String[] args) { if( args != null && args.length > 0) { //entry point是有参数的,那么打开GUI窗口 ECLApplication theApp = new ECLApplication(); theApp.enterEventDispatcher(); } else { //entry point是没有参数的,那么启动PushedDataListener线程在后台运行,监听push来的数据 PushedDataListener.waitForSingleton().start(); } } class ECLApplication extends UiApplication { private MenuItem _copyItem; /****************************************************************************************** * main() - controls the startup of the application...thread will start in the background * and the GUI starts when user clicks from main menu ******************************************************************************************/ public static void main(String[] args) { if( args != null && args.length > 0) { //entry point是有参数的,那么打开GUI窗口 ECLApplication theApp = new ECLApplication(); theApp.enterEventDispatcher(); } else { //entry point是没有参数的,那么启动PushedDataListener线程在后台运行,监听push来的数据 PushedDataListener.waitForSingleton().start(); } } Page | 26 PushedDataListener 代码说明 Push 监听代码是客户端最核心的代码。Push.java 首先是使用 waitForSingleton()方 法达到手机上只有一个 PushedDataListener 对象在运行的目的,然后构建并启动 ListenerThread 类开始监听。 waitForSingleton()方法把唯一的 PushedDataListener 对象实例保存到 RuntimeStore 对象中,从而实现 singleton 模式。RuntimeStore 在黑莓手机中是程序间共享的存 储空间,或者说所有程序都可以访问其他程序保存在 RuntimeStore 里面的数据。 在手机重新启动后,RuntimeStore 被清空。 Push.javaPushedDataListener 的监听代码放在 ListenerThread 类中。ListenerThread 类扩展 Thread 线程类,调用 (StreamConnectionNotifier)Connector.open(LISTEN_URL)方法和 stream = notify.acceptAndOpen();代码开始监听 911 端口上 push 过来的数据。 class PushedDataListener extends UiApplication { public static final long RTSID_MY_APP = 0x56b19e51d45ff827L; private static final String LISTEN_URL = "http://:911"; //the listen port private ListenerThread myThread; ime public PushedDataListener() { myThread = new ListenerThread(); } public static PushedDataListener waitForSingleton(){ //make sure this is a singleton instance RuntimeStore store = RuntimeStore.getRuntimeStore(); Object o = store.get(RTSID_MY_APP); if (o == null){ store.put(RTSID_MY_APP, new PushedDataListener()); return (PushedDataListener)store.get(RTSID_MY_APP); } else { return (PushedDataListener)o; } } public ECLApplication() { … Bitmap icon=Bitmap.getBitmapResource("icon/read.gif"); net.rim.blackberry.api.homescreen.HomeScreen.updateIcon(icon); //call the datastore class DataStore dataStore = new DataStore(); //opens the persistent store and loads in the group list dataStore.loadGroupListFromStore(); EclScreen screen = new EclScreen(dataStore); pushScreen(screen); } Page | 27 收到数据后调用 DataStore 方法保存数据到持久存储中,工 ECL GUI 进程显示数 据;然后调用 HomeScreen.updateIcon(icon,1)代码更新 ECL 图标为未读提示。 后记: 常见运行错误处理 1. 模拟器无法收到 Push 数据 class ListenerThread extends Thread { public void run() { System.out.println("eclBackGroundThread -- running"); StreamConnectionNotifier notify = null; StreamConnection stream = null; InputStream input = null; try{ sleep(1000); } catch(Exception e){} try { notify = (StreamConnectionNotifier)Connector.open(LISTEN_URL); for(;;) { //NOTE: the following will block until data is received stream = notify.acceptAndOpen(); input = stream.openInputStream(); //Extract the data from the input stream StringBuffer sb = new StringBuffer(); int datum = -1; while ( -1 != (datum = input.read()) ) { sb.append((char)datum); } stream.close(); stream = null; String contactData = sb.toString(); DataStore dataStore = new DataStore(); dataStore.saveData(contactData); Bitmap icon=Bitmap.getBitmapResource("icon/unread.gif"); net.rim.blackberry.api.homescreen.HomeScreen.updateIcon(icon,1); System.err.println("Push message received...("+contactData.length()+" bytes)\n"); System.err.println(contactData); Page | 28 解决办法:使用最新的 Blackberry Plugin for eclipse 1.1 版本,运行项目的时 候选择启动 MDS 模拟器。启动模拟器后,确认模拟器的网络连接是好的, 方法是打开浏 览器访问任意外网网站。 2. 服务器端和客户端程序各自的端口不匹配 解决办法:在 ECL 例子程序中两个端口都是 911,注意查看。如果你参考 ECL 代码编写自己的 Push 程序,那么建议你把服务器和客户端的端口都修 改为非 911 的端口,以避免在手机上参数监听端口冲突。 3. 客户端程序没有 auto start,因而没能启动监听程序 解决办法:在 ECL 例子程序中,编辑 BlackBerry_App_Descriptor.xml,选择 Auto-run on startup,并在代码的 main()方法中相应处理,启动 listener thread 在后台开始监听。 4. 客户端收到的文字中文乱码 解决办法:首先,服务器端代码在提出 Push 请求时候要通过 HttpURLConnection.setRequestProperty(("Content-Type",…)方法告诉 BES/MDS 服务器,它将发送什么样的内容。如果是 Content-Type 是 text/plain,那么 服务器将以 gb2312 编码发送数据;为代码清晰起见,建议设置 Content- Type 为 text/plain; charset=utf-8。 其次,客户端接收数据的时候,建议读取 byte[],然后按照服务器 Content- Type 相同的编码进行转码获得字符串 String。 public void sendToHandheld(String recipientEmail) … { HttpURLConnection conn = null; … establishRequestHeaders(conn); … } protected void establishRequestHeaders(HttpURLConnection conn) { conn.setRequestProperty("Content-Type", "text/plain; charset=utf-8"); } Page | 29 英文缩写说明 BES – BlackBerry Enterprise Server,是 BlackBerry 解决方案的核心服务器之一,架设在企业 内网。 MDS – BlackBerry Mobile Data System,是 BES 服务器的一个重要组件。在 Push 技术中, MDS 接受 Push 请求,然后通过 BES 其他组件把数据推送出去。 参考资料 1. BES 服务器推送机制分析 http://www.searchcio.com.cn/whitepaper_1161.htm 2. What Is - Sample applications demonstrating BlackBerry push technology: Emergency Contact List http://www.blackberry.com/go/ecl stream = notify.acceptAndOpen(); input = stream.openInputStream(); //Extract the data from the input stream byte[] bytes = loadBytesFromStream(input); stream.close(); stream = null; String contactData = new String(bytes, “UTF-8”); Page | 30 附录: 一.如何更换不同版本的 BlackBerry SDK? 由于不同 BlackBerry 手机使用不同版本的 OS, 所以在应用编译的时候,需要使用不同的 BlackBerry SDK。 首先可以在 Eclipse 中获取所有版本的 BlackBerry SDK,目前有的包括 4.5, 4.6 和 5.0 版本: Eclipse  Help  Install New Software  输入 URL: http://www.blackberry.com/go/eclipseUpdate/3.5/java 然后进行更新即可。 二.如何选择不同版本的 BlackBerry SDK? Page | 31 在上图中选择 Edit 然后再次编译即可: Page | 32 或者: 在 JRE System Library 右击鼠标,属性中选择不同的 SDK 即可。 三. ECL sample Browser push 中文问题的解决 现象:xyzlist.xls 数据源里面有中文,通过 MDS 服务器 push 到手机里面的网页,中文是乱 码,显示不正常。 step 1. 编辑 ECL_Java\src\Server\BrowserChannelPusher.java 把 con.setRequestProperty('Content-Type', 'text/html'); 修改为 con.setRequestProperty('Content-Type', 'text/html; charset=gb2312'); Page | 33 step 2. 重新编译生成 ECL_Java\bin\Server\ecl.jar 执行 ECL_Java\src\Server\build.bat step 3. 重新运行 push server/client 测试即可 Page | 34
还剩33页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

BlackBerry

贡献于2010-09-15

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf