使用Android Accessibility實現免Root自動批量安裝功能

Main-zy發表於2015-12-11

對於國內Android裝置,應用的自動批量安裝/更新一直是一個痛點,在之前,第三方應用商店通常要求裝置Root,然後呼叫系統的PackageManagerService命令列來實現後臺安裝。最近,豌豆莢利用Android Accessibility(輔助功能)在業內率先實現了免Root自動批量安裝功能。

這個功能實現的原理是,在後臺批量下載應用後,呼叫系統的PackageInstaller,獲取安裝介面的按鈕位置,然後通過Accessibility提供的模擬使用者點選功能,代替使用者自動點選下一步,直到安裝結束。

雖然技術看起來不是特別困難,但在實現中還是有不少坑的,豌豆莢工程師向我們分享了該功能的一些技術細節和實踐經驗。

Android Accessibility API介紹與呼叫方法

對於那些由於視力、聽力或其它身體原因導致不能方便使用Android智慧手機的使用者,Android提供了Accessibility功能和服務幫助這些使用者更加簡單地操作裝置,包括文字轉語音、觸覺反饋、手勢操作、軌跡球和手柄操作。開發者可以搭建自己的Accessibility服務,這可以加強應用的可用性,例如聲音提示,物理反饋,和其他可選的操作模式。

隨著Android系統版本的迭代,Accessibility功能也越來越強大,它能實時地獲取當前操作應用的視窗元素資訊,並能夠雙向互動,既能獲取使用者的輸入,也能對視窗元素進行操作,比如點選按鈕。更多的介紹見Android開發者官網的Accessibility頁面

呼叫Android Accessibility API需要三個步驟:申請許可權、註冊 Service、配置 Accessibility Service Info。使用Accessibility API需要的許可權如下:

<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"/>

註冊Service

<service android:name="com.your.AccessibilityImpl.className"
        android:label="@string/acc_auto_install_service_name"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:enabled="@bool/enable_accessibility">
   <intent-filter>
       <action android:name="android.accessibilityservice.AccessibilityService" />
   </intent-filter>
   <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_config" />
</service>

配置Accessibility Service Info

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:description="@string/acc_description" android:accessibilityEventTypes="typeAllMask"
   android:accessibilityFlags="flagDefault"
   android:accessibilityFeedbackType="feedbackGeneric"
   android:notificationTimeout="100"
   android:canRetrieveWindowContent="true"
   android:settingsActivity="com.your.settingActivity"
   android:packageNames="packageName1,packageName2"
/>

需要說明的一點是,在配置配置 Accessibility Service Info時,如果明確的知道目標APP的包名,那一定要使用packageNames屬性進行設定。舉一個例子:

在一些使用虛擬鍵盤的APP中,經常會出現這樣的邏輯

Button button = (Button) findViewById(R.id.button);
String num = (String) button.getText();

在一般情況下,getText方法的返回值是Java.lang.String類的例項,上面這段程式碼可以正確執行。但是在開啟Accessibility Service之後,如果沒有指定packageNames,系統會對所有APP的UI都進行Accessible的處理。在這個例子中的表現就是getText方法的返回值變成了android.text.SpannableString類的例項(Java.lang.Stringandroid.text.SpannableString都實現了java.lang.CharSequence介面),進而造成目標APP崩潰。

所以強烈建議在註冊Accessibility Service時指定目標APP的packageName,以減少手機上其他應用的莫名崩潰(程式碼中有這樣的邏輯的各位,也請默默的改為呼叫toString()方法吧)。

實現AccessibilityService

繼承android.accessibilityservice.AccessibilityService並過載onAccessibilityEventonInterrupt方法:

public class AccessibilityImpl extends AccessibilityService {
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {}
    @Override
    public void onInterrupt() {}
}

以onAccessibilityEvent與onInterrupt為入口實現業務邏輯程式碼。

如何獲取UI元素

onAccessibilityEvent中,使用引數event的getSource方法獲取到的AccessibilityNodeInfo例項,即為觸發這次事件的UI節點。

如果需要獲取當前介面上的其它元素,需要獲取到當前介面UI Tree的根節點後再使用findAccessibilityNodeInfosByText或者findAccessibilityNodeInfosByViewId方法進行獲取。

需要注意的一點是,findAccessibilityNodeInfosByText在獲取UI元素時的判斷邏輯是contains而非equals,在使用時可能要根據具體業務邏輯做進一步的處理。

模擬使用者點選

實現AccessibilityService,並獲取介面上UI元素之後,可以使用下面的程式碼來模擬使用者點選:

nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);

需要注意的是,在觸發事件之前需要確定該UI元素在介面上是否依舊存在。使用該方法還可以模擬使用者的其它操作,甚至是複製貼上這種行為,具體可以參考AccessibilityNodeInfo

豌豆莢負責“自動裝”專案的產品經理程志達、研發工程師李曉鵬還接受了InfoQ記者的採訪,談到了該專案的初衷和一些開發細節。

InfoQ:請介紹一下你們的團隊在豌豆莢負責什麼部分?

程志達:我們屬於豌豆莢的Apps團隊,主要負責應用平臺這部分的業務,還有應用相關的“瀏覽、發現和安裝”,這些都是我們團隊在做。豌豆莢移動開發團隊組織結構並不是統一的,各個產品線都獨立負責自己的模組。

InfoQ:你們開發這個自動裝功能的初衷是什麼?

程志達:為了方便Android手機使用者批量安裝和升級全部應用。很多使用者都有這樣的體會,當手機上有十幾或者幾十個應用時,想全部升級它們就是一件非常麻煩的事情。不僅要等待下載完更新,還要一一確認安裝,每次都要點選幾十上百次“下一步”。而且這樣的噩夢不到一兩週就要重複一次,因為每週都總會有一些應用再次更新。天長日久,強迫症也被逼得沒有脾氣了。通過我們的走訪,豌豆莢員工有很多人的應用更新提醒已經積累了幾十個。即使是Google Play原生的解決方案也不能解決問題,尤其是它不能幫忙更新從網頁等渠道下載的應用,但這種情況在國內市場反而是一種常態。

我們基於Android Accessibility機制,在獲取目標APP的UI元素後模擬使用者動作進行點選,讓之前的“一鍵升級”做到真的只要一次點選,為使用者提供更輕鬆的安裝體驗,就是“自動裝”功能的初衷。

InfoQ:你們開發的這個功能和目前市面上的類似功能相比有什麼優勢?

程志達:在豌豆莢“自動裝”功能出來之前,使用者只能通過root的方式才能自動批量安裝應用。懂得永久root方法的使用者只佔使用者數量很少的一部分,技術門檻太高,大多數使用者無法體驗;臨時root通過系統漏洞進行提權操作,獲取的並不僅僅是可以安裝應用的許可權,因此風險相對較高;隨著Android系統日趨完善,通過系統漏洞進行臨時root的可能性越來越小,難度也越來越高。因此,採取臨時root方式進行應用的解決方案終究會無法使用(目前在v4.3及以上版本的Android系統上很多以前採用臨時root方式的app已將該功能關閉)。而我們的這個功能無論使用者是否root都可以使用。

InfoQ:能介紹一下這個專案的立項情況嗎?

程志達:在以前,我們對Root使用者提供了自動安裝的功能,但是Android 4.3釋出以後,修復了很多漏洞,Root門檻變高,去年年底我們通過調查發現,國內Android的Root使用者其實已經不到14%,因此有必要為非Root使用者也提供自動安裝功能。

所以從去年開始我們就希望做這個功能,當時考慮過各種解決方案,Accessibility這個介面出現之後,我們評估了一下是不是可以用這個功能去實現,當時考慮到因為ROM不同,可能會出現適配問題,因此一度擱置下來。今年第一季度之後,我們又整體的衡量一下,還是希望使用者在豌豆莢上無論是發現新應用還是查詢應用、安裝應用,都給他們一個最好的體驗。從一開始我們就知道將會遇到一些困難,但還是下決心一定要把這個東西實現好,做下來。最後我們花了一個多月的時間把它完成了。

InfoQ:你們在開發這個功能的過程中遇到了哪些困難?

李曉鵬:最主要的困難有兩個,第一是因為Accessibility這個東西以前很少有人研究過,有些人將它用來殺程式清記憶體,但從來沒有人將它用在應用的安裝上面,網路上的資料很少,這次做這個事情我們需要學習這個API,踩坑是第一個困難。第二,因為國內市場Android ROM和系統版本這麼多,適配也是一件非常困難的事情。

InfoQ:請介紹一下你們的開發過程。

李曉鵬:決定用Accessibility之後,我們做了幾件事情。第一是看Android原始碼,因為國內ROM定製化很多,我們希望找到通用的解決方案來為使用者提供這個功能。通過分析Android原始碼,我們找到了一些共性。比如說多個版本的PackageInstaller,相同的按鈕的ID是沒有變化的,而且位置、功能也是沒有變化的,我們最開始想從ID這個方向切入,通過這種手段找到我們需要點選的按鈕來進行操作。但後來發現有一些廠商的按鈕沒有ID,我們根本沒有辦法取到,而且通過ID來取按鈕是Android 4.3以上才支援的。我們平臺上Android 4.3以上的使用者佔80%左右,還是有相當一部分人在使用較老的系統版本,我們覺得不能放棄這部分使用者,希望他們也能用這樣的功能,最後我們採取從介面上通過文字找按鈕的方式來做這個事情,而這就涉及到非常複雜的適配問題了。

另外還有對不同ROM的安裝器流程進行調研。不同的廠商的ROM的應用安裝流程是大不一樣的,有些ROM會幫你解包和分析安全性,有些會讓使用者選擇是否提供某些許可權,都需要我們去應對,這也給適配造成了困難。

InfoQ:請介紹一下你們的適配情況。

李曉鵬:我們一開始做的時候就靠自己,公司內部的同事貢獻機器,做的比較穩定了之後,在論壇發動一部分使用者幫我們做測試,幫我們測試一些市面上比較少的機器,我們目前已經適配了超過3000種機型,覆蓋了市面上主流裝置。

適配遇到的主要問題就是各種ROM對PackageInstaller的修改。我們發現小廠商一般只會把PackageInstaller改皮換顏色之類的,越是有技術實力的廠商,不光改皮,還會進行功能化定製,甚至換掉這個東西,比如聯想自己做了一個安裝器裡替代系統自帶的,適配起來非常困難。

InfoQ:非Root自動安裝功能和Root自動安裝功能如何共存?未來如何進行取捨?

李曉鵬:首先這兩個功能還是會並存的,我們會根據使用者的選擇來使用,現在比如說你的機器是Root過的,第一次安裝的時候,我們會詢問是是否選擇Root的安裝模式,如果是這樣的話,就是靜默安裝了,如果沒有選擇Root,那就是使用系統預設的安裝器,我們會提示使用者開啟自動裝功能。不過Root的使用者是越來越少的,而且臨時Root又不安全,所以我們判斷未來非Root自動安裝功能使用的會更多一些,所以我們的主要精力會放在非Root上。

程志達:Root的功能我們會一直保留的,相當於我們為使用者設計了兩套不同的方案,我們希望普通使用者對手機儘量少的設定或者調整的情況下都能夠使用自動安裝。

自從“自動裝”功能上線以來,我們統計的數字是平均為使用者節省點選24次,隨使用時間的增加,替使用者點選的次數還會增加,相當於這部分本來需要使用者手動點選的事情我們都可以讓它自動完成,節省了更多的時間。

相關文章