iPhone Reserve Bot 教學 4 - 接收 SMS
前言
來到 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
新增以下:
<receiver
android:name=".ReceiveSmsBroadcastReceiver"
android:enabled="true"
android:exported="true"
>
<intent-filter android:priority="500">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
而 ReceiveSmsBroadcastReceiver
即是長成這樣子:
public class ReceiveSmsBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "ReceiveSmsBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (null != intent) {
Bundle bundle = intent.getExtras();
Log.d(TAG, "Received SMS intent");
if (null != bundle) {
Object[] pdus = (Object[]) bundle.get("pdus");
SmsMessage[] smsMessage = new SmsMessage[pdus.length];
String [] allMessageContent = new String[pdus.length];
for (int i = 0; i < pdus.length; i++) {
smsMessage[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
allMessageContent[i] = smsMessage[i].getMessageBody();
for(String message:allMessageContent){
if(message != null) {
Log.d(TAG, "Got SMS: " + message);
}
}
}
}
}
}
}
測試一下,應該收到任何 SMS 後,logcat 也會顯示 Got SMS: <SMS Content>
的。
抽取 Reservation code
但我們不是想要所有的 SMS,只要特定的那個,所以要檢查內容是否 apple 給我們的編號。最簡單的是用 String.indexOf()
檢查有沒有 你的註冊代碼為 XXXXXXXX
。若String.indexOf() > -1
便代表是我們需要的 SMS ,然後抽出 SMS code 便可。
String smsPattern = "你的註冊代碼為 (Your registration code is) ";
int idx = message.indexOf(smsPattern);
if(idx > -1){
String smsCode = message.substring(idx + smsPattern.length());
Log.d(TAG, "Matched SMS code: " + smsCode);
}
可是用此放法實在不夠 elegant。來,讓我們用 regular Expression 吧。不知道什麼是 Regular Expression 的建議學一學,基本的也已經很有用。
先定義 SMS code pattern
String smsPattern = "你的註冊代碼為 \\(Your registration code is\\) ([a-zA-Z0-9]+)";
然後再這樣去抽編號出來
Pattern pattern = Pattern.compile(smsPattern);
Matcher matcher = pattern.matcher(message);
if (matcher.find()) {
String smsRespondCode = matcher.group(1);
Log.d(TAG, "Matched SMS code: " + smsRespondCode);
broadcastMessageToActivity(context, smsRespondCode);
}
Regex 難度在於如何寫 Pattern,但學好的話以後做事便方便多了。
找到 code 後便可以通知 MainActivity
去更新 EditText#
了
private void broadcastMessageToActivity(Context context, String msg) {
Intent in = new Intent(MainActivity.BROADCAST_RECEIVE_SMS);
in.putExtra(MainActivity.KEY_RECEIVE_SMS_RESULT, msg);
LocalBroadcastManager.getInstance(context).sendBroadcast(in);
}
當然 MainActivity
那邊的 localBroadcastReceiver
也要更新一下:
BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent != null){
if(BROADCAST_SEND_SMS.equals(intent.getAction())){
boolean result = intent.getBooleanExtra(KEY_SEND_SMS_RESULT, false);
if(result){
addLog("Send SMS successfully");
}
else{
addLog("Failed to send SMS");
}
}
else if(BROADCAST_RECEIVE_SMS.equals(intent.getAction())){
String smsCode = intent.getStringExtra(KEY_RECEIVE_SMS_RESULT);
if(smsCode != null){
addLog("got reservation code: " + smsCode);
// submit reservation code
}
}
}
}
};
這個 localBroadcastReceiver
其實是應該分兩個 class 來對應不同的 action
的,這樣才是一個好 OOP,不過我懶,所以合在一起。
有了這 localBroadcastReceiver
便可以繼續 bot 的旅程了。
遞交預訂編碼
弄妥後,便可遞交 SMS 編碼,需要的資料如下:
做法跟之前的差不多,也是用 http Post。在 ReserveWorker
新增 submitSmsCode()
:
//submit SMS code
public String submitSmsCode(String phoneNum, String smsRespondCode) throws Exception {
Map<String, String> params = new HashMap<String, String>();
params.put("phoneNumber", phoneNum);
params.put("reservationCode", smsRespondCode);
params.put("p_ie", "???");
params.put("_flowExecutionKey", "???");
params.put("_eventId", "next");
FormEncodingBuilder builder = new FormEncodingBuilder();
for (String key : params.keySet()) {
builder.add(key, params.get(key));
}
RequestBody formBody = builder.build();
Request request = new Request.Builder()
.url("https://reserve-hk.apple.com/HK/en_HK/reserve/iPhone?execution=e1s2")
.post(formBody)
.build();
Response response = okHttpClient.newCall(request).execute();
String url = response.request().url().toString();
String returnResponse = url;
return returnResponse;
}
不知 _flowExecutionKey
和 p_ie
在哪裏來? 還記得之前拿 SMS request code 的 respond 嗎?
{
"firstTime" : true,
"IRSV141417879720141024" : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAR<--TRIMED-->",
"keyword" : "data:image/png;base64,iVBORw0KGgoAAAANSU2u1cfWQ<--TRIMED-->",
"_flowExecutionKey" : "e1s2",
"p_ie" : "90166040-b3b6-4551-8d94-8f430f5150c0"
}
就是這個了。我們更新之前的 retrieveSmsCodePage()
去儲存 _flowExecutionKey
和 p_ie
拿來用
//get SMS code
public String retrieveSmsCodePage() throws Exception {
//........
//......
try {
JSONObject jsonObject = new JSONObject(body);
loginPageQueryString.put(P_IE, jsonObject.getString(key));
loginPageQueryString.put(FLOW_EXECUTION_KEY, jsonObject.getString(FLOW_EXECUTION_KEY));
//........
//......
然後 submitSmsCode()
便可以用它們了:
params.put("p_ie", loginPageQueryString.get(P_IE));
params.put("_flowExecutionKey", loginPageQueryString.get(FLOW_EXECUTION_KEY));
當然要在 Button
執行它:
private void submitSmsReservationCode(final String smsReservationCode){
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
String result = null;
try{
result = reserveWorker.submitSmsCode(PHONE_NUMBER, smsReservationCode);
}
catch(Exception e){
e.printStackTrace();
}
return result;
}
@Override
protected void onPostExecute(String s) {
//check submission result
}
}.execute();
}
不過這裏有一個問題,無論 reservation code 正確與否,url
也不像之前的有所改變,那麼如何知道結果呢?
如果你有用 firebug 檢查 request,應該看到成功失敗的話有此一 request:
https://reserve-hk.apple.com/HK/en_HK/reserve/iPhone?execution=e1s3
你可試試輸入成功的 code 和失敗的,看看結果。
看到嗎?分別在於拿回來的 json 是否有 errors
而已。
而拿這 URL
跟 retrieveSmsCodePage()
的 request 對比一下,除了 execution
外,也是一樣的!從此得知 execution
是會在每次遞交後 + 1 的。當然,我們可以加幾個方法來拿取這 URL 結果,但這樣的話我們永遠只是低 level 的 programmer! 要稍為升升 level,自然是要簡化它,不讓它有這麼多重覆的 coding。
在 ReserveWorker
新增 getCommonAjax()
:
public String getCommonAjax() throws Exception {
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));
Request request = new Request.Builder()
.url(url)
.build();
Response response = okHttpClient.newCall(request).execute();
String body = response.body().string();
return body;
}
然後 retrieveSmsCodePage()
便可簡化為
public String retrieveSmsCodePage() throws Exception {
String body = getCommonAjax();
.....
}
不過別忘記 loginPageQueryString.get(FLOW_EXECUTION_KEY)
正正是在 retrieveSmsCodePage()
後 initialized 的,不過經我們反覆測試,肯定在拿 SMS code 時,execution
一定是 e1s2
,所以可以先 hard code 進去:
public String retrieveSmsCodePage() throws Exception {
loginPageQueryString.put(FLOW_EXECUTION_KEY, "e1s2");
String body = getCommonAjax();
.....
}
先運行一次看看確保拿 SMS code 沒問題,然後便可繼續檢查遞交 reservation code 了。
在 MainActivity
新增 getSubmitResult()
:
private void getSubmitResult(){
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
String result = null;
try{
result = reserveWorker.getCommonAjax();
}
catch(Exception e){
e.printStackTrace();
}
return result;
}
@Override
protected void onPostExecute(String s) {
//parse the JSON
}
}.execute();
}
在這個 onPostExecute()
裏我們便可以檢查 JSON 有沒有 errors
便知是否失敗。若沒有 errors
的話便是 login 的資料,可以繼續進行。
@Override
protected void onPostExecute(String jsonStr) {
//parse the JSON
try {
JSONObject jsonObject = new JSONObject(jsonStr);
JSONArray errors = jsonObject.getJSONArray("errors");
if(errors.length() > 0){
for(int i=0; i < errors.length(); i++){
addLog("Errors: " + errors.getString(i) );
}
}
else{
//we have reached page 3!
}
} catch (JSONException jsonException) {
//NO ERROR, should be proceed
} catch (NullPointerException e) {
addLog("Null pointer. Please start again");
}
}
這樣我們終於到達 page 3 檢查存貨的頁面了,離最終步驟只差一步!
待續
今次講解了接收 SMS 的方法,都是用 BroadcastReceiver
去接收再用 local broadcast 通知 MainActivity
的。之後的步驟便簡單多了,只是重覆的用 okhttpClient
request 資料再分析 json 資料而已,相信大家自行寫下去絕無問題。當然,好頭好尾,我也會寫到最後的步驟的。
今次的 code 可在以下網址找到
下回是最終回了。
Apple iPhone Reserve Bot 教學 - 首頁