星期二, 10月 15, 2013

Code Lab - Communication among Android devices : A chat room


此Android程式練習是為成大資工系-電腦通訊網路課程 所設計的一個Code Lab,目的在介紹Android之間的裝置通訊,以及在Android上面如何實作一個聊天室應用,其中包含在Server端的HttpServerlet以及GCM Api的使用。
此App運用可以上安裝此App的所有使用者彼此間進行聊天。
課程時間:3小時

投影片下載



Check Point 0 : Android 環境設定

安裝設定Android開發環境,以繼續接下來的練習

Step 1 : 下載並安裝Android ADT Bundle



Step 2 : 下載ChatRoom Project ,並進行解壓縮


  • Do not publish this code to Google Play !!

Step 3 : 在新的Workspace 中,匯入ChatRoom Project

  • File> Import > General> Existing Project into Workspace
  • 選擇Project路徑
  • 按下Select All 按鈕
  • 勾選 Copy project into workspace
若匯入後有錯,請檢查Project name右鍵 > Properties > Google > AppEngine > AppEngine SDK路徑是否正確

Step 4 : 設定Project Build Target為Android 4.3


  • Project上按右鍵 > Properties > Android > Project Build Target > Google APIs Level 18


Step 5 : 註冊下載Genymotion Android 模擬器 / 或使用Android 手機

Step 6 :執行Genymotion並建立一個Galaxy Nexus with Google App API17

Step 7 :嘗試在模擬器上執行專案

*中文字如果出現亂碼請將編碼改為UTF-8  (File > Properties > Resource > Text file encoding)

*如果有任何錯誤,請坐下列檢查

  • ADT > Project > clean > clean all
  • Project右鍵 > property > Android > 檢查Build Target 及 Library


Check Point 1 : 下載App Engine SDK

Step 1 : 安裝Eclipse 外掛

  • ADT > Help > install new software > 輸入 http://dl.google.com/eclipse/plugin/4.2
  • 勾選(1)Google App Engine Tools for Android、(2) SDKs / Google App Engine Java SDK 及(3)SDKs / Google Web Toolkit > 按下Next開始安裝 (4) Google plugin for Eclipse
  • https://developers.google.com/appengine/downloads?hl=zh-TW&csw=1
  • 在GAE-ChetRoom按右鍵 > Properties > Google > App Engine > Use App Engine
註: app engine sdk 1.8.6 需要以JDK 7 來編譯,JDK 6會有錯

Step 2: 建立Web Application專案

  • File > new > Web application project > 只勾選Use Google App Engine
  • Project Name 設為TEST
  • Package 設為com.xxx.gae.test
  • 執行新建立的專案,並在瀏覽器開啟http://localhost:8888/,看是否正常顯示
  • 觀察此專案內容結構

Check Point 2 : 在Google Api Console 開啟GCM服務

Step 1 : 連到Google API Console 並建立一Project
Step 2 : 取得API Key
  • API Access > Create Server Key 
  • 填入whitelist ip, 此處保持空白就好
  • 按下Create
  • 將API_KEY記錄下來,之後會用到

Check Point 3 : 在Server上建立App Engine Project,並上傳專案

Step 1 : Create GAE project

  • https://appengine.google.com/ > create project 
  • 輸入一個Application Identifier,此名稱必須沒有被用過。http://[applicaton identifier].appspot.com 為你所申請的server domain name

Step 2 : 將Web Application上傳

  • GAE_ChatRoom project > 右鍵 > Properties > Google > App Engine > Application Id 填入Step 1 所設定的Application Identifier
  • GAE_ChatRoom project > 右鍵 > Google > Deploy to App Engine

Step 3 : 測試

  • 嘗試 http://[applicaton identifier].appspot.com 

Check Point 4 : 建立GcmRegister.java HttpServlet處理註冊

Step 1 : 建立GcmRegister來定義DataStore欄位

  • 建立GcmRegister其中包含regid的String欄位

GcmRegister.java
package com.whilerain.gae.chatroom.db;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
@PersistenceCapable
public class GcmRegister {
 @PrimaryKey
 private String regid; 
 public GcmRegister(String id){
  regid = id;
 }
 public String getRegid() {
  return regid;
 }
 public void setRegid(String regid) {
  this.regid = regid;
 }
}

Step 2 : 在GAE_ChatRoom當中建立PMF類別來處理DataStore的存取作業。

  • 建立PMF.java

PMF.java
package com.whilerain.gae.chatroom.db;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

public final class PMF {
 private static final PersistenceManagerFactory pmfInstance = JDOHelper.getPersistenceManagerFactory("transactions-optional");
 public static PersistenceManagerFactory get() {
  return pmfInstance;
 }
}


Step 3 : 在DoGcmRegister.java當中,儲存所上傳的registration_id

DoGcmRegister.java
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException {
 
 String jsonState = URLDecoder.decode(req.getParameter("statement"));
 PersistenceManager pm=PMF.get().getPersistenceManager();
 JSONObject jsonResp = new JSONObject();
 try {
  JSONObject jsonObject = new JSONObject(jsonState);  
  String regid = jsonObject.getString("regid");  
  GcmRegister reg = new GcmRegister(regid);
  pm.makePersistent(reg); 
   
  jsonResp.put("result", "ok");  
  resp.setContentType("text/json");
  resp.setCharacterEncoding("UTF-8");
  resp.getWriter().println(jsonResp.toString());
 } catch (Exception e) {
  resp.setContentType("text/plain");
  resp.getWriter().println(e.getMessage());
  e.printStackTrace();
 }finally{
  pm.close();
 }
}

Step 4 : 宣告Servlet路徑

  • 在war/WEB-INF/web.xml加入servlet路徑宣告,在<web-app>標籤之間加入

<servlet>
 <servlet-name>GCMREG</servlet-name>
 <servlet-class>com.whilerain.gae.chatroom.DoGcmRegister</servlet-class>
</servlet>
<servlet-mapping>
 <servlet-name>GCMREG</servlet-name>
 <url-pattern>/gcmreg</url-pattern>
</servlet-mapping>

step 5 : 上傳目前版本,並測試之




Check Point 5 : 建立SendMessage.java HttpServlet處理訊息的推送

Step 1 : 在SendMessage.java實作doPost()來處理取得Http Post參數
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 String jsonState = URLDecoder.decode(req.getParameter("statement"));
 PersistenceManager pm = PMF.get().getPersistenceManager();
 JSONObject jsonResp = new JSONObject();
 try {
  JSONObject jsonObject = new JSONObject(jsonState);
  castMessage(pm, jsonObject);
  jsonResp.put("result", "ok");
  resp.setContentType("text/json");
  resp.setCharacterEncoding("UTF-8");
  resp.getWriter().println(jsonResp.toString());
 } catch (Exception e) {
  resp.setContentType("text/plain");
  resp.getWriter().println(e.getMessage());
  e.printStackTrace();
 } finally {
  pm.close();
 }
}

Step 2 : 在war/WEB-INF/web.xml加入servlet路徑宣告,在<web-app>標籤之間加入
        <servlet>
         <servlet-name>SendMessage</servlet-name>
  <servlet-class>com.whilerain.gae.chatroom.SendMessage</servlet-class>
 </servlet>
 <servlet-mapping>
  <servlet-name>SendMessage</servlet-name>
  <url-pattern>/sendmsg</url-pattern>
 </servlet-mapping>
Step 3 : 上傳目前版本

Check Point 6 : 將訊息丟給GCM廣播至裝置

Step 1 : 在GAE_ChatRoom專案加入gcm-server.jar參考 (已加入Project中)

  • 將gcm-server.jar複製到war/WEB-INF/lib
  • 並且在CoWallet-GAE右鍵 > properties > java build path > Library > add JAR 選擇gcm-server.jar
Step 2 : 在GAE_ChatRoom專案加入json-simple-1.1.1.jar (已加入Project中)

  • 將json-simple-1.1.1.jar複製到war/WEB-INF/lib

Step 3 : 在GAE_ChatRoom專案加入設定API_KEY

public static String API_KEY = "AIzaSyBY0AYX9RdPpvsAmnyjuwm9hl1-Xh...";

Step 4 : cast to GCM 程式碼

  • 在SendMessage.java, 加入castMessage()來將所有regid取出,並依序將message丟給GCM
private void castMessage(PersistenceManager pm, JSONObject jsonReq) throws IOException {
 Query queryRegid = pm.newQuery(GcmRegister.class);
 List<GcmRegister> reg_results = (List<GcmRegister>) queryRegid.execute();
 for (GcmRegister gcmReg : reg_results) {
  GCM.cast(pm, gcmReg.getRegid(), jsonReq.toString(), gcmReg.getMember());
 }
}

Step 5 : 在GCM.java當中實作Cast to GCM

public static void cast(PersistenceManager pm, String regid, String content) throws IOException{
 Sender sender = new Sender(API_KEY);
 Message.Builder builder = new Message.Builder();
 builder.addData("timestamp", String.valueOf(System.currentTimeMillis()));
 builder.addData("statement", URLEncoder.encode(content,"UTF-8"));
   
 Message message = builder.build();
 Result result = sender.send(message, regid, 5);

 if (result.getMessageId() != null) {
  String canonicalRegId = result.getCanonicalRegistrationId();
  if (canonicalRegId != null) {
   // same device has more than one registration ID: update
   // database
   GcmRegister reg = pm.getObjectById(GcmRegister.class, regid);
   pm.deletePersistent(reg);
   reg = new GcmRegister(canonicalRegId);
   pm.makePersistent(reg);
   
  }
 } else {
  String error = result.getErrorCodeName();
  if (error.equals(Constants.ERROR_NOT_REGISTERED)) {
   // application has been removed from device - unregister from
   // database
   GcmRegister reg = pm.getObjectById(GcmRegister.class, regid);
   pm.deletePersistent(reg);
  }
 }
}


Check point 7 : Android端取得裝置GCM Registration id,並註冊到Server上

Step 1 : 修改SENDER_ID
  • 將MainActivity.java中的SENDER_ID改為Google API Console的 Project number
String SENDER_ID = "36994572588";
    Step 2 : 修改Server URL
    • 修改URL為GAE的project name
    private static final String URL_GCMREG = "http://your-gae-projectname.appspot.com/gcmreg";
    Step 3 : 在MainActivity.java中收到regid後,使用http post註冊到server上
    • 送出http post到Server上
    private boolean sendRegistrationIdToBackend(String regid) {
     ArrayList<NameValuePair> paramList = new ArrayList<NameValuePair>();
     try {
      JSONObject statement = new JSONObject();
      statement.put("action", "GcmReg");
      statement.put("regid", regid);
      paramList.add(new BasicNameValuePair("statement", statement.toString()));
    
      String result = Util.sentHttpPost(URL_GCMREG, paramList, Util.getHttpClient());
    
      if (result == null) {
       return false;
      } else {
       JSONObject json = new JSONObject(result);
       if (json.get("result").equals("ok")) {
        return true;
       } else {
        return false;
       }
      }
     } catch (Exception e) {
      e.printStackTrace();
      return false;
     }
    }

    Check point 8 : Android傳送訊息到Server上,廣播給其他使用者

    Step 1 : 實作SendMessageTask
    • 在SendMessageTask.java當中,修改URL_SENDMESSAGE參數為你的Server
    private static final String URL_SENDMESSAGE = "http://[project-identifier].appspot.com/sendmsg";
    • 在SendMessageTask.java當中送出Http Post到Server
    @Override
    protected Boolean doInBackground(Void... params) {
     ArrayList<NameValuePair> paramList = new ArrayList<NameValuePair>();
     try{
      JSONObject statement = new JSONObject();
      statement.put("action", "SendMessage");
      statement.put("message", mMessage);
      statement.put("member", mName);
      paramList.add(new BasicNameValuePair("statement", statement.toString()));
      
      String result = Util.sentHttpPost(URL_SENDMESSAGE, paramList, Util.getHttpClient());
      
      if (result == null) {
       return false;
      } else {
       JSONObject json = new JSONObject(result);
       if(json.get("result").equals("ok")){
        return true;
       }else{
        return false;
       }
      }
     }catch(Exception e){
      e.printStackTrace();
      return false;
     }
    }
    Step 2 : 送出SendMessage Http Post要求
    • 在MainActivity中當使用者按下傳送按鈕後,將訊息透過SendMessageTask送出
    protected void doSend(String name, String message) {
     SendMessageTask task = new SendMessageTask(name, message, new OnSendMessageListener() {
      @Override
      public void onPostExecute(Boolean ok) {
       try {
        String msg;
        if (ok) {
         msg = "Message send";
        } else {
         msg = "ohhh! something wrong";
        }
        Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
       } catch (Exception e) {
        e.printStackTrace();
       }
      }
     });
     task.execute();
    }

    Check point 9 : 取得其他使用者的廣播內容

    Step 1 : 接收與處理GCM訊息,並以LocalBroadcast廣播
    • 在GcmIntentService.java中,當收到來自GCM的訊息時,將內容以LocalBroadcast廣播給MainActivity來更新訊息列表內容
    GcmIntentService.java

    ...
    if (intent.getStringExtra("statement") != null) {
     String statement = URLDecoder.decode(intent.getStringExtra("statement"));
     String timestamp = URLDecoder.decode(intent.getStringExtra("timestamp"));
    
     try {
      JSONObject castevent = new JSONObject(statement);
      String event = castevent.getString("action");
      if (event.equalsIgnoreCase("SendMessage")) {   
       String member = castevent.getString("member");
       String message = castevent.getString("message");
       sendLocalBroadcast(member, message);
       Log.i(TAG,"Completed work @ " + SystemClock.elapsedRealtime());
       
       sendNotification("Received: " + extras.toString());
       Log.i(TAG, "Received: " + extras.toString());
      }
     } catch (Exception e) {
      e.printStackTrace();
     }
    }

    Step 2 : 傳送LocalBroadcast
    • 實作sendLocalBroadcast

    private void sendLocalBroadcast(String member, String message) {
     Intent localIntent = new Intent("com.whilerain.chatroom.MESSAGE");
     localIntent.putExtra("member", member);
     localIntent.putExtra("message", message);
     LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
    }

    Step 3 : 實作BroadcastReceiver物件
    • 在MainActivity中實作BroadcastReceiver,接收來自GcmIntentService的LocalBroadcast,並更新訊息列表
    BroadcastReceiver mLocalBroadcastReceiver = new BroadcastReceiver() {
    
     @Override
     public void onReceive(Context context, Intent intent) {
      String member = intent.getStringExtra("member");
      String message = intent.getStringExtra("message");
    
      ChatMessage msg = new ChatMessage(member, message);
      mList.add(msg);
      mListAdapter.notifyDataSetChanged();
     }
    };

    Step 4 : 註冊與取消註冊BroadcastReceiver

    • 在MainActivity.onCreate()中註冊BroadcastReceiver, 

    LocalBroadcastManager.getInstance(this).registerReceiver(mLocalBroadcastReceiver, new IntentFilter("com.whilerain.chatroom.MESSAGE"));
    • 並在MainActivity.onDestory()中取消註冊
    LocalBroadcastManager.getInstance(this).unregisterReceiver(mLocalBroadcastReceiver);

    
    

    張貼留言