[Google Play][HowTo] 檢查 Android App 在各國市集架上的狀態

App 無預警遭到下架的狀況時有所聞,原因可能是政策變更,也可能是官方內部作業的疏失。不論原因如何,為了能在第一時間發現 app 搜尋不到或不在架上的問題,好跟官方聯繫取得協助,有必要將自動監測 app 在各國市集架上狀況的機制建立起來。

不過 Google Play Store 並未提供像 Apple Search API 之類的工具,若要透過程式查詢 app 在架上的狀態,做法會跟檢查 Windows Phone Store App 在各國市集架上的狀態很類似 - 模擬手機上 Play Store 向 Google Play 發出請求的動作,架構上會像是:

howto-check-app-availability/arch.png

繼續閱讀

廣告

[monkeyrunner] `startActivity()` 參數內容要避開某些特殊的字元

照理說,如果要用瀏覽器叫出 Google Maps 顯示 台北 101 的位置,可以這麼做:

startActivity(action='android.intent.action.VIEW', data='http://maps.google.com/?q=25.033611,121.565000&z=19')

不過這個動作大部份的時候會失敗(至於為什麼 “偶爾" 可以,真的讓人不解!),因為某個參數裡出現特殊的字元,就像是這裡 data 參數裡的 &。原因是 monkeyrunner 最後也是將不同的參數串成一個 shell command,但某些字元在 shell 的環境下有特殊的意義,所以造成某些參數值被錯誤解讀。

這個問題可以在 ADB shell 裡看出端倪:

$ am start -a android.intent.action.VIEW -d http://maps.google.com/?q=25.033611,121.565000&z=19
$ Starting: Intent { act=android.intent.action.VIEW dat=http://maps.google.com/?q=25.033611,121.565000 } 1

[1]   Done                    am start -a android.intent.action.VIEW -d http://maps.google.com/?q=25.033611,121.565000
$
$ am start -a android.intent.action.VIEW -d http://maps.google.com/?q=25.033611,121.565000\&z=19
Starting: Intent { act=android.intent.action.VIEW dat=http://maps.google.com/?q=25.033611,121.565000&z=19 } 2
1 很明顯地,整個 am start 尾部的 &z=19 整個被切掉了,因為 & 被解讀為 “放到背景執行"。
2 \& 字元跳脫後,am 就能收到完整的參數。

由於 startActivity() 內部不會做 escaping(細節可以看最後一節),要解這個問題,就必須要先將參數值加工過:

startActivity(action='android.intent.action.VIEW', data=r'http://maps.google.com/?q=25.033611,121.565000\&z=19')

原來 monkeyrunner 最後也是呼叫 Shell Command

MonkeyDevice.startActivity() 開始追起:

com.android.monkeyrunner.MonkeyDevice

69    private IChimpDevice impl;
...
274    public void startActivity(PyObject[] args, String[] kws) {
275        ArgParser ap = JythonUtils.createArgParser(args, kws);
276        Preconditions.checkNotNull(ap);
277
278        String uri = ap.getString(0, null);
279        String action = ap.getString(1, null);
280        String data = ap.getString(2, null);
281        String mimetype = ap.getString(3, null);
282        Collection<String> categories = Collections2.transform(JythonUtils.getList(ap, 4),
283                Functions.toStringFunction());
284        Map<String, Object> extras = JythonUtils.getMap(ap, 5);
285        String component = ap.getString(6, null);
286        int flags = ap.getInt(7, 0);
287
288        impl.startActivity(uri, action, data, mimetype, categories, extras, component, flags); 1
289    }
290
1 轉呼叫 IChimpDevice.startActivity(),目前只有 AdbChimpDevice 實作它。

com.android.chimpchat.adb.AdbChimpDevice

383    public void startActivity(String uri, String action, String data, String mimetype,
384            Collection<String> categories, Map<String, Object> extras, String component,
385            int flags) {
386        List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories,
387                extras, component, flags);
388        shell(Lists.asList("am", "start", 1
389                intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY));
390    }
...
406    private List<String> buildIntentArgString(String uri, String action, String data, String mimetype,
407            Collection<String> categories, Map<String, Object> extras, String component,
408            int flags) {
409        List<String> parts = Lists.newArrayList();
410
411        // from adb docs: 2
412        //<INTENT> specifications include these flags:
413        //    [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>]
414        //    [-c <CATEGORY> [-c <CATEGORY>] ...]
415        //    [-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...]
416        //    [--esn <EXTRA_KEY> ...]
417        //    [--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...]
418        //    [-e|--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...]
419        //    [-n <COMPONENT>] [-f <FLAGS>]
420        //    [<URI>]
421
422        if (!isNullOrEmpty(action)) {
423            parts.add("-a");
424            parts.add(action);
425        }
426
427        if (!isNullOrEmpty(data)) {
428            parts.add("-d");
429            parts.add(data);
430        }
...
479        return parts;
480    }
1 原來 monkeyrunner 內部最後也是轉呼叫 shell command。
2 問題就出在不同的參數最後會被接成一長串 am start ... 的指令。

延伸閱讀

ADB 按鍵輸入

ADB shell command 裡有個 input,可以用來輸入文字或觸發鍵盤的事件。

$ input
usage: input [text|keyevent]
       input text <string>
       input keyevent <event_code>

首先來看 input keyevent <event_code>,例如:

$ input keyevent 3

Home 鍵的 key code 是 3,所以 input keyevent 3 可以模擬使用者按下 Home 鍵的效果。其他常用的 key code 有:

  • KEYCODE_HOME (3)
  • KEYCODE_BACK (4)
  • KEYCODE_SPACE (62)
  • KEYCODE_ENTER (66)
  • KEYCODE_MENU (82)

完整的清單可以參考這裡

接著來看 input text <string>,通常只要先讓文字輸入框取得焦點,然後把要輸入的文字接在 input text 後面即可:

$ input text hello
$
$ input text android&java 1
java: not found
[1]   Done                    input text android
$ input text 'android&java'
$
$ input text hello world 2
$ input text 'hello world'
$
$ input text hello
$ input keyevent 62
$ input text world
$
$ input text 你好 3
/mnt/.lfs: Function not implemented
/mnt/secure/asec: Permission denied
rename(/data/log/dumpstate_sys_error.txt.gz.tmp, /data/log/dumpstate_sys_error.txt.gz): Operation not permitted
Unable to chmod /data/log/dumpstate_sys_error.txt.gz: Operation not permitted
[1]   Killed                  input text 你好
1 & 在 shell 有特殊的意義,所以畫面上只出現 hello& 後面的 java 被視為另外一個指令;包在單引號裡就不會有問題了。
2 只會輸出 hello,空白字元後面的 " world" 全不見了,用單引號框起來或是把 software keyboard 整個收起來都沒用(跟輸入法無關?)。一個可行的解決方法,就是混用 input textinput keyevent,其中 62 是空白字元的 key code。
3 顯然 input text 目前還不支援非英數字元。
Tip 由於 input text 背後是送出 raw keyevent,所以想要透過它來輸入 unicode 是行不通的,不過倒可以試試透過 clipboard 來交換文字

撇開文字內容包含空白、特殊符號或非英數字的限制,就算是第一個 input text hello 這麼簡單的動作,都可能會失敗,原因就出在使用者開始要輸入文字之前,對螢幕點選的動作會叫輸入法的 software keyboard。input text 背後其實是送出一連串的 keyevent(也就是說,改用 input keyevent 也會有相同的問題),感覺 keyevent 會先被吃進 software keyboard 再轉換成對應的文字,因此最後的結果往往不如預期(字首變成大寫、只出現部份文字…)。

AndADBInput/ime_hello.png

Figure 1. 在 Samsung Galaxy S2 上採用 Chinese Keypad 時,input text hello 累積成一串無效的拼音

遇到這種情形,通常只要事先用 Back 鍵將輸入法收回去即可,keyevent 就不會先由輸入法過一手。不過某些 App 在按下 Back 鍵之後,輸入的焦點就會跟著移開,導致輸入的文字無法進到該文字輸入框,這時候只好透過 ime 切換輸入法來避開這個問題(每一種輸入法影響的程度不一)。下面示範在 Samsung Galaxy S2 上的做法:

$ ime list -a -s
com.swype.android.inputmethod/.SwypeInputMethod
com.samsung.inputmethod/.SamsungIME
com.sec.android.inputmethod.axt9/.AxT9IME
$
$ ime set com.samsung.inputmethod/.SamsungIME 1
Input method com.samsung.inputmethod/.SamsungIME selected
$ input text http://www.google.com.tw         2
$ input ABCabc123
$
$ ime set com.sec.android.inputmethod.axt9/.AxT9IME
Input method com.sec.android.inputmethod.axt9/.AxT9IME selected
$ input text http://www.google.com.tw         3
$ input text ABCabc123
1 別懷疑,com.sec.android.inputmethod.axt9/.AxT9IME 是 Samsung Keypad,com.samsung.inputmethod/.SamsungIME 才是會出狀況的 Chinese Keypad。
2 搭配 Chinese Keypad 時,輸入的網址會變成 http://www:google:com(句點全變成冒號了),下一行文字則變成 ABCAbc123(中間那個 a 被轉成大寫 A 了)。
3 切換回 Samsung Keypad 之後,兩串文字都能正常地輸入了。

一路測試下來,似乎沒有一個方法是萬無一失的。這裡總結一下不同的做法:

  • 要輸入 unicode,只能 透過 clipboard 來交換文字,而且會涉及 UI 的操作(長按後選 Paste,而且貼上的動作不一定有支援)。
  • 如果輸入法的 software keyboard 可以用 Back 鍵關閉,而且輸入的焦點不會跑掉,直接用 input event 輸入英數字即可。
  • 如果輸入的焦點會因為關閉輸入法的 software keyboard 跑掉,只好事先切換到不會出狀況的輸入法,不過每台裝置安裝的輸入法不同,是這個方法最大的缺點。

延伸閱讀


參考資料

其他文件