[心得分享] iPhone Reserve Bot 教學 4 - 接收 SMS

寫寫下無乜心機寫,好懶。可以的話去番原 blog post 睇,謝謝。

前幾 part 教學

======================================

前言

來到 Part 4,今次是收 SMS。又來回顧一下完整的步驟完整的步驟:


  • 在第一頁
  •     網頁會下載 驗證碼 captcha
  •     用戶在輸入 apple ID 、密碼和 驗證碼 captcha,按遞交
  •     在第二頁會用 ajax 下載顯示 SMS 的碼
  •     用戶用手機將 SMS 碼以 SMS 形式寄到 Apple 電話,等待回覆
  •     Apple 回覆 SMS code
  •     用戶到第二頁輸入發送 SMS 的手機號碼和 SMS 回覆碼,遞交
  •     在第三頁網頁會自動下載你的個人資訊
  •     用戶選擇 Apple Store,網頁會下載 Apple Store 的 timeslot 資料
  •     用戶選擇 iPhone Model 、大小和 Contract type 後,網頁會下載存貨資料
  •     如有存貨,用戶可輸入姓名、電話、身份證明號碼,遞交
  •     預訂成功/失敗


今次我們做第 6 至 8 步。

接收 SMS

接收 SMS 的概念跟發送 SMS 差不多,都是用 BroadcastReceiver 接收 global broadcast 後,再用 local broadcast 通知 MainActivity。

所以我們又要一 BroadcastReceiver 收取 Android OS 接收 SMS 的 intent,在 AndroidManifest.xml 新增以下:
  1. <receiver
  2.   android:name=".ReceiveSmsBroadcastReceiver"
  3.   android:enabled="true"
  4.   android:exported="true"
  5. >
  6.   <intent-filter android:priority="500">
  7.        <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
  8.   </intent-filter>
  9. </receiver>
複製代碼
而 ReceiveSmsBroadcastReceiver 即是長成這樣子:
  1. public class ReceiveSmsBroadcastReceiver extends BroadcastReceiver {
  2.     private static final String TAG = "ReceiveSmsBroadcastReceiver";
  3.     @Override
  4.     public void onReceive(Context context, Intent intent) {

  5.         if (null != intent) {
  6.             Bundle bundle = intent.getExtras();
  7.             Log.d(TAG, "Received SMS intent");

  8.             if (null != bundle) {
  9.                 Object[] pdus = (Object[]) bundle.get("pdus");
  10.                 SmsMessage[] smsMessage = new SmsMessage[pdus.length];
  11.                 String [] allMessageContent = new String[pdus.length];

  12.                 for (int i = 0; i < pdus.length; i++) {
  13.                     smsMessage[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
  14.                     allMessageContent[i] = smsMessage[i].getMessageBody();
  15.                     for(String message:allMessageContent){
  16.                         if(message != null) {
  17.                              Log.d(TAG, "Got SMS: " + message);
  18.                         }
  19.                     }
  20.                 }
  21.             }
  22.         }
  23.     }
  24. }
複製代碼
測試一下,應該收到任何 SMS 後,logcat 也會顯示 Got SMS: <SMS Content> 的。
抽取 Reservation code

但我們不是想要所有的 SMS,只要特定的那個,所以要檢查內容是否 apple 給我們的編號。最簡單的是用 String.indexOf() 檢查有沒有 你的註冊代碼為 XXXXXXXX。若String.indexOf() > -1 便代表是我們需要的 SMS ,然後抽出 SMS code 便可。
  1. String smsPattern = "你的註冊代碼為 (Your registration code is) ";
  2. int idx = message.indexOf(smsPattern);
  3. if(idx > -1){
  4.   String smsCode = message.substring(idx + smsPattern.length());
  5.   Log.d(TAG, "Matched SMS code: " + smsCode);
  6. }
複製代碼
可是用此放法實在不夠 elegant。來,讓我們用 regular Expression 吧。不知道什麼是 Regular Expression 的建議學一學,基本的也已經很有用。

先定義 SMS code pattern
  1. String smsPattern = "你的註冊代碼為 \\(Your registration code is\\) ([a-zA-Z0-9]+)";
複製代碼
然後再這樣去抽編號出來
  1. Pattern pattern = Pattern.compile(smsPattern);
  2. Matcher matcher = pattern.matcher(message);
  3. if (matcher.find()) {
  4.   String smsRespondCode = matcher.group(1);
  5.   Log.d(TAG, "Matched SMS code: " + smsRespondCode);

  6.    broadcastMessageToActivity(context, smsRespondCode);
  7. }
複製代碼
Regex 難度在於如何寫 Pattern,但學好的話以後做事便方便多了。

找到 code 後便可以通知 MainActivity 去更新 EditText# 了
  1. private void broadcastMessageToActivity(Context context, String msg) {
  2.     Intent in = new Intent(MainActivity.BROADCAST_RECEIVE_SMS);
  3.     in.putExtra(MainActivity.KEY_RECEIVE_SMS_RESULT, msg);
  4.     LocalBroadcastManager.getInstance(context).sendBroadcast(in);
  5. }
複製代碼
當然 MainActivity 那邊的 localBroadcastReceiver 也要更新一下:
  1. BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() {
  2.     @Override
  3.     public void onReceive(Context context, Intent intent) {
  4.         if(intent != null){
  5.             if(BROADCAST_SEND_SMS.equals(intent.getAction())){
  6.                 boolean result = intent.getBooleanExtra(KEY_SEND_SMS_RESULT, false);
  7.                 if(result){
  8.                     addLog("Send SMS successfully");
  9.                 }
  10.                 else{
  11.                     addLog("Failed to send SMS");
  12.                 }
  13.             }
  14.             else if(BROADCAST_RECEIVE_SMS.equals(intent.getAction())){
  15.                 String smsCode = intent.getStringExtra(KEY_RECEIVE_SMS_RESULT);
  16.                 if(smsCode != null){
  17.                     addLog("got reservation code: " + smsCode);
  18.                     // submit reservation code
  19.                 }
  20.             }
  21.         }
  22.     }
  23. };
複製代碼
這個 localBroadcastReceiver 其實是應該分兩個 class 來對應不同的 action 的,這樣才是一個好 OOP,不過我懶,所以合在一起。

有了這 localBroadcastReceiver 便可以繼續 bot 的旅程了。

遞交預訂編碼

弄妥後,便可遞交 SMS 編碼,需要的資料如下:

[圖片]

做法跟之前的差不多,也是用 http Post。在 ReserveWorker 新增 submitSmsCode():
  1. //submit SMS code
  2. public String submitSmsCode(String phoneNum, String smsRespondCode) throws Exception {
  3.     Map<String, String> params = new HashMap<String, String>();
  4.     params.put("phoneNumber", phoneNum);
  5.     params.put("reservationCode", smsRespondCode);
  6.     params.put("p_ie", "???");
  7.     params.put("_flowExecutionKey", "???");
  8.     params.put("_eventId", "next");

  9.     FormEncodingBuilder builder = new FormEncodingBuilder();
  10.     for (String key : params.keySet()) {
  11.         builder.add(key, params.get(key));
  12.     }
  13.     RequestBody formBody = builder.build();
  14.     Request request = new Request.Builder()
  15.             .url("https://reserve-hk.apple.com/HK/en_HK/reserve/iPhone?execution=e1s2")
  16.             .post(formBody)
  17.             .build();
  18.     Response response = okHttpClient.newCall(request).execute();

  19.     String url = response.request().url().toString();

  20.     String returnResponse = url;

  21.     return returnResponse;
  22. }
複製代碼
不知 _flowExecutionKey 和 p_ie 在哪裏來? 還記得之前拿 SMS request code 的 respond 嗎?
  1. {
  2.   "firstTime" : true,
  3.   "IRSV141417879720141024" : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAR<--TRIMED-->",
  4.   "keyword" : "data:image/png;base64,iVBORw0KGgoAAAANSU2u1cfWQ<--TRIMED-->",
  5.   "_flowExecutionKey" : "e1s2",
  6.   "p_ie" : "90166040-b3b6-4551-8d94-8f430f5150c0"
  7. }
複製代碼
就是這個了。我們更新之前的 retrieveSmsCodePage() 去儲存 _flowExecutionKey 和 p_ie 拿來用
  1. //get SMS code
  2. public String retrieveSmsCodePage() throws Exception {
  3.     //........
  4.     //......
  5.     try {
  6.         JSONObject jsonObject = new JSONObject(body);
  7.         loginPageQueryString.put(P_IE, jsonObject.getString(key));
  8.         loginPageQueryString.put(FLOW_EXECUTION_KEY, jsonObject.getString(FLOW_EXECUTION_KEY));

  9.     //........
  10.     //......
複製代碼
然後 submitSmsCode() 便可以用它們了:
  1. params.put("p_ie", loginPageQueryString.get(P_IE));
  2. params.put("_flowExecutionKey", loginPageQueryString.get(FLOW_EXECUTION_KEY));
複製代碼
當然要在 Button 執行它:
  1. private void submitSmsReservationCode(final String smsReservationCode){
  2.     new AsyncTask<Void, Void, String>() {
  3.         @Override
  4.         protected String doInBackground(Void... params)  {
  5.             String result = null;
  6.             try{
  7.                 result = reserveWorker.submitSmsCode(PHONE_NUMBER, smsReservationCode);
  8.             }
  9.             catch(Exception e){
  10.                 e.printStackTrace();
  11.             }
  12.             return result;
  13.         }

  14.         @Override
  15.         protected void onPostExecute(String s) {
  16.             //check submission result
  17.         }
  18.     }.execute();
  19. }
複製代碼
不過這裏有一個問題,無論 reservation code 正確與否,url 也不像之前的有所改變,那麼如何知道結果呢?

如果你有用 firebug 檢查 request,應該看到成功失敗的話有此一 request:



你可試試輸入成功的 code 和失敗的,看看結果。

看到嗎?分別在於拿回來的 json 是否有 errors 而已。

而拿這 URL 跟 retrieveSmsCodePage() 的 request 對比一下,除了 execution外,也是一樣的!從此得知 execution 是會在每次遞交後 + 1 的。當然,我們可以加幾個方法來拿取這 URL 結果,但這樣的話我們永遠只是低 level 的 programmer! 要稍為升升 level,自然是要簡化它,不讓它有這麼多重覆的 coding。

在 ReserveWorker 新增 getCommonAjax():
  1. public String getCommonAjax() throws Exception {
  2.     String url = String.format("https://reserve-hk.apple.com/HK/en_HK/reserve/iPhone?execution=%1$s&ajaxSource=true&_eventId=context", loginPageQueryString.get(FLOW_EXECUTION_KEY));
  3.     Request request = new Request.Builder()
  4.             .url(url)
  5.             .build();
  6.     Response response = okHttpClient.newCall(request).execute();

  7.     String body = response.body().string();
  8.     return body;
  9. }
複製代碼
然後 retrieveSmsCodePage() 便可簡化為
  1. public String retrieveSmsCodePage() throws Exception {
  2.     String body = getCommonAjax();
  3.     .....
  4. }
複製代碼
不過別忘記 loginPageQueryString.get(FLOW_EXECUTION_KEY) 正正是在 retrieveSmsCodePage() 後 initialized 的,不過經我們反覆測試,肯定在拿 SMS code 時,execution 一定是 e1s2,所以可以先 hard code 進去:
  1. public String retrieveSmsCodePage() throws Exception {
  2.     loginPageQueryString.put(FLOW_EXECUTION_KEY, "e1s2");
  3.     String body = getCommonAjax();
  4.     .....
  5. }
複製代碼
先運行一次看看確保拿 SMS code 沒問題,然後便可繼續檢查遞交 reservation code 了。

在 MainActivity 新增 getSubmitResult():
  1. private void getSubmitResult(){
  2.     new AsyncTask<Void, Void, String>() {
  3.         @Override
  4.         protected String doInBackground(Void... params)  {
  5.             String result = null;
  6.             try{
  7.                 result = reserveWorker.getCommonAjax();
  8.             }
  9.             catch(Exception e){
  10.                 e.printStackTrace();
  11.             }
  12.             return result;
  13.         }

  14.         @Override
  15.         protected void onPostExecute(String s) {
  16.             //parse the JSON
  17.         }
  18.     }.execute();
  19. }
複製代碼
在這個 onPostExecute() 裏我們便可以檢查 JSON 有沒有 errors 便知是否失敗。若沒有 errors 的話便是 login 的資料,可以繼續進行。
  1. @Override
  2. protected void onPostExecute(String jsonStr) {
  3.     //parse the JSON

  4.     try {
  5.         JSONObject jsonObject = new JSONObject(jsonStr);
  6.         JSONArray errors = jsonObject.getJSONArray("errors");
  7.         if(errors.length() > 0){
  8.             for(int i=0; i < errors.length(); i++){
  9.                 addLog("Errors: " + errors.getString(i) );
  10.             }
  11.         }
  12.         else{
  13.             //we have reached page 3!
  14.         }
  15.     } catch (JSONException jsonException) {
  16.         //NO ERROR, should be proceed
  17.     } catch (NullPointerException e) {
  18.         addLog("Null pointer.  Please start again");
  19.     }
  20. }
複製代碼
這樣我們終於到達 page 3 檢查存貨的頁面了,離最終步驟只差一步!

待續

今次講解了接收 SMS 的方法,都是用 BroadcastReceiver 去接收再用 local broadcast 通知 MainActivity 的。之後的步驟便簡單多了,只是重覆的用 okhttpClient request 資料再分柝 json 資料而已,相信大家自行寫下去絕無問題。當然,好頭好尾,我也會寫到最後的步驟的。

今次的 code 可在以下網址找到



下回是最終回了。

============================================
可以的話去番原 blog post 睇,謝謝。

前幾 part 教學

多謝CHING

TOP

   支持一下。

TOP

睇唔明都要SUPPORT下

TOP

support

TOP

THANKS

TOP