這篇主要是記錄一下在完全沒學過Android的情況下硬拗完的這個APK,拖了很久查了很多資料才勉強寫完,比較垃圾但還是實現功能了。記錄的過程我也盡量把知識點貼出來。
一開始是看了一個大佬的分享貼(testerhome的帖子,但是現在論壇封了,等過后再貼大佬的鏈接),決定改寫這個apk。大佬原先寫的代碼不適用於MIUI系統,稍微改了一下,再增加了跳轉獲取Accessibility權限部分。(這部分代碼也參考了網上的大佬,我有空再找找記得哪個鏈接就貼哪個,畢竟查了太多資料了)
這個版本現在還很稚嫩,過后有空還會完善。
貼上GitHub地址:https://github.com/congyingHHZ/AutoInstall
有需要的朋友可以自己下載源碼
測試設備:小米10
工具:Android Studio 11.0.10
環境:window10
軟件介紹
這個軟件主要是為了解決MIUI系統(todo:嘗試兼容各家定制Android)在APK安裝過程中總是需要手動允許才能繼續安裝流程這個比較煩人的事情。並且也為了以后接入自動化測試,遇到安裝APK可以自動完成。
軟件大致分成2個部分
- 判斷是否有AccessibilityService權限,如果沒有權限則引導跳轉到AccessibilityService權限設置頁面
- 利用AccessibilityService對安裝過程的彈窗進行自動點擊,已完成自動化安裝
代碼
一、利用AccessibilityService對權限彈窗的“允許”等按鈕進行點擊(從核心內容開始說起, AccessibilityUtil.java)
1. 創建一個類繼承AccessibilityService
無障礙服務AccessibilityService可以主動接收到系統的事件,通過對事件的判斷,知道是否有進行安裝操作,並且可以對控件進行點擊等操作。
public class AutoInstallService extends AccessibilityService{
// do something
}
2. 當接收到系統事件后,調用onAccessibilityEvent方法
在這個方法里對收到的系統事件進行判斷,如果判斷為系統正在安裝軟件那么就調用自己寫performInstallation去完成自動點擊操作。
public void onAccessibilityEvent(AccessibilityEvent event) {
/*
* 回調方法,當事件發生時會從這里進入,在這里判斷需要捕獲的內容,
* 可通過下面這句log將所有事件詳情打印出來,分析決定怎么過濾。
*/
log("!!onAccessibilityEvent!!");
//log(event.toString());
AccessibilityNodeInfo noteInfo = event.getSource();
log("===noteInfo!===");
if (event.getSource() == null) {
log("<null> event source");
return;
}
AccessibilityNodeInfo rowNode = getRootInActiveWindow();
log("===rowNode!===");
//log(rowNode.toString());
int eventType = event.getEventType();
log(eventType+"");
log(event.getPackageName().toString());
if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
&& (event.getPackageName().equals(PACKAGE_INSTALLER_MIUI) | event.getPackageName().equals(PACKAGE_INSTALLER_MIUI_adb))) {
boolean r = performInstallation(event);
log("Action Perform: " + r);
}else if(eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED &&
event.getPackageName().equals(PACKAGE_INSTALLER_MIUI)){
log("!!input TYPE_WINDOW_CONTENT_CHANGED !!");
boolean r = performInstallation(event);
log("Action Perform: " + r);
}
}
相關知識點:
- event.getSource()
接收的系統事件類型是AccessibilityEvent,通過getSource()方法可以獲取到事件信息
AccessibilityNodeInfo noteInfo = event.getSource();
- getRootInActiveWindow()
獲取節點信息
AccessibilityNodeInfo rowNode = getRootInActiveWindow();
之后通過節點信息判斷頁面有沒有我們想要的內容,如“允許安裝”這些
3.event.getEventType()
獲取事件的類型,返回值是INT, TYPE_VIEW_CLICKED, TYPE_VIEW_LONG_CLICKED, TYPE_VIEW_SELECTED……
event.getPackageName()
獲取事件產生的應用,也就是這個事件是哪個應用產生的。
int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
&& (event.getPackageName().equals(PACKAGE_INSTALLER_MIUI) | event.getPackageName().equals(PACKAGE_INSTALLER_MIUI_adb))){
...
}else if(eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED &&
event.getPackageName().equals(PACKAGE_INSTALLER_MIUI)){
}
傳進來的AccessibilityEvent,可能是發生了點擊事件、長按事件等等,但是如果安裝軟件彈出來權限框肯定是頁面變化,所以這里判斷even是不是頁面變化的類型(TYPE_WINDOW_STATE_CHANGED,TYPE_WINDOW_CONTENT_CHANGED )才執行接下來的操作。
同時因為系統可能還會有其他軟件導致的頁面變化事件,因此這里還判斷了是不是安裝軟件產生的頁面變化。MIUI中安裝應用的包名是PACKAGE_INSTALLER_MIUI,通過adb安裝應用的包名是PACKAGE_INSTALLER_MIUI_adb。
3. 確定是進入安裝流程后開始執行點擊獲取權限操作,跳轉到performInstallation方法
private boolean performInstallation(AccessibilityEvent event) {
List<AccessibilityNodeInfo> nodeInfoList;
/*
* 有的手機會彈2次,有的只彈一次,在替換安裝時會出現確定按鈕,
* 為了大而全,下面定義了比較多的內容,可按需增減。
*/
log("!!performInstallation!!");
String[] labels = new String[]{"本次允許","允許", "確定", "繼續安裝", "下一步", "完成","安裝"};
for (String label : labels) {
log(label);
nodeInfoList = event.getSource().findAccessibilityNodeInfosByText(label);
if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
boolean performed = performClick(nodeInfoList);
if (performed) return true;
}
}
return false;
}
這部分沒有什么可說的,就是獲取頁面內容,然后循環判斷有沒有存在“允許”之類的字符,這些都要要點擊的節點。
4. 如果頁面有需要點擊節點,也就是彈出的權限框,則跳轉到performClick()執行點擊操作。
@RequiresApi(api = Build.VERSION_CODES.N)
private boolean performClick(List<AccessibilityNodeInfo> nodeInfoList) {
for (AccessibilityNodeInfo node : nodeInfoList) {
/*
* 這里還可以根據node的類名來過濾,大多數是button類,這里也是為了大而全,
* 判斷只要是可點擊的是可用的就點。
*/
if (node.isClickable() && node.isEnabled()) {
return node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
else if(node.getClassName() == "android.widget.Button"){
Log.d(TAG,"clickByNode");
return clickByNode(node);
}
}
return false;
}
這一段代碼邏輯也很簡單就是傳進來上一步在當前頁面發現的所有node,然后遍歷這個list,如果是按鈕可以點擊則點擊該node。
相關知識點:
- node.isClickable() && node.isEnabled()
AccessibilityNode如果是按鈕就具有isClickable()和isEnabled()屬性 - node.getClassName() == "android.widget.Button"
這里比原版代碼多加了一個判斷,因為小米安裝過程彈出權限框的“允許安裝”按鈕(還是“繼續安裝”?不記得了,反正就是有個按鈕點不了)不是isClickable的,導致無法繼續往下走執行點擊。但是這個node的類名是"android.widget.Button",所以補充判斷再進行點擊操作。當然原來isClickable屬性的按鈕點擊方法也用不了了,這里也添加的這種類型node的點擊方法。
5. 執行點擊操作。這部分是新增的,針對node.performAction()無法實現點擊的情況。
public final boolean clickByNode(AccessibilityNodeInfo nodeInfo){
if (nodeInfo == null){
return false;
}
// if (nodeInfo.getClassName() != "android.widget.Button"){
// return false;
// }
Rect rect = new Rect();
nodeInfo.getBoundsInScreen(rect);
int x = (rect.left + rect.right)/2;
int y = (rect.top + rect.bottom)/2;
Point point = new Point(x,y);
GestureDescription.Builder builder = new GestureDescription.Builder();
Path path = new Path();
path.moveTo(point.x,point.y);
builder.addStroke(new GestureDescription.StrokeDescription(path,100,50));
//path:路徑 startTime:從手勢開始到開始筆畫的時間
final GestureDescription gesture = builder.build();
return dispatchGesture(gesture,
new GestureResultCallback(){
@Override
public void onCompleted(GestureDescription gestureDescription){
super.onCompleted(gestureDescription);
}
@Override
public void onCancelled(GestureDescription gestureDescription){
super.onCancelled(gestureDescription);
}
},null);
}
這一段主要是利用了GestureDescription,這個api是Android7.0之后引入的,所以必須在這個方法前增加 @RequiresApi(api = Build.VERSION_CODES.N)。
利用GestureDescription可以實現在不root手機的情況下進行模擬手勢操作。
相關知識點:
- GestureDescription.dispatchGesture()