Android輔助功能AccessibilityService控制第三方app

最近調研車機旋鈕控制操作第三方應用(高德,百度等;很多中高端汽車中控屏採用旋鈕+按鍵控制,屏幕不能觸控)需要用到輔助功能,百度查了下,然後就直接開幹了。

不要跟我說什麼底層原理,框架內核,老夫敲代碼就是一把梭,複製,粘貼,拿起鍵盤就是幹!

第一步,建個demo工程,建一個繼承AccessibilityService的類;

public class MyService extends AccessibilityService {
    
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
         boolean openAccessibility = AccessbilityTool.isOpenAccessibility(this);
        if (!openAccessibility) {
            String action = Settings.ACTION_ACCESSIBILITY_SETTINGS;
            startActivity(new Intent(action));
        }    
    }

    @Override
    public void onInterrupt() {

    }
    
}

2,創建配置文件、res/xml/service_conf.xml (至於裏面的參數具體作用--百度)

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask|typeViewClicked|typeViewFocused|typeWindowStateChanged|typeWindowContentChanged"
    android:accessibilityFeedbackType="feedbackGeneric|feedbackSpoken"
    android:accessibilityFlags="flagDefault|flagReportViewIds|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews"
    android:canRetrieveWindowContent="true"
    android:canPerformGestures="true"
    android:description="@string/app_name"
    android:notificationTimeout="10"
    android:packageNames="com.autonavi.amapauto"> //需要監聽(模擬控制)的app的包名
    
</accessibility-service>

3.清單文件配置 AndroidManifest.xml:

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <!--  android:theme="@android:style/Theme.Translucent"-->
       
        <service
            android:name=".TestService"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/service_conf" />
        </service>
        
    </application>

5,判斷app是否開啓輔助功能:

  public static boolean isOpenAccessibility(Context context) {
        try {
            String service = context.getPackageName() + "/" + TestService.class.getCanonicalName();
            int accessibility = Settings.Secure.getInt(context.getApplicationContext().getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_ENABLED);
            TextUtils.SimpleStringSplitter stringSplitter = new TextUtils.SimpleStringSplitter(':');
              Log.e(TAG, "accessibility: " + accessibility);
            if (accessibility == 1) {
                String value = Settings.Secure.getString(context.getApplicationContext().getContentResolver(),
                        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
                if (!TextUtils.isEmpty(value)) {
                    stringSplitter.setString(value);
                    while (stringSplitter.hasNext()) {
                        String next = stringSplitter.next();
                        if (next.equalsIgnoreCase(service)) {
                            return true;
                        }
                    }
                }
            }
        } catch (Settings.SettingNotFoundException e) {
            e.printStackTrace();
        }
        return false;
    }

6.接下來就是在自己的AccessibilityService中遍歷查找相關的組件節點了:

 //id格式
String viewId = nodeInfo.getViewIdResourceName();
String viewId = "com.autonavi.amapauto:id/sftv_off"; 

  //遍歷當前窗口界面的節點信息--對於自定義控件幾乎讀取不到
//對於控制別人的app這種方式幾乎無用,我這個案例就是反編譯高德車機地圖的res文件,通過id和文字反推節點信息
        AccessibilityNodeInfo root = getRootInActiveWindow();
        for (int i = 0; i < root.getChildCount(); i++) {
            AccessibilityNodeInfo child = root.getChild(i);
            String resName = child.getViewIdResourceName();
            Log.e(TAG, "resName: " + resName);
        }


 /**
     * 對指定組件節點觸發點擊事件;比如TextView,Button,LinearLayout等
     *
     * @param viewId
     */
    private void setViewClick(String viewId) {
        Log.e(TAG, "點擊指定組件View>>>>>>>");
        AccessibilityNodeInfo root = getRootInActiveWindow();
        List<AccessibilityNodeInfo> infoList = root.findAccessibilityNodeInfosByViewId(viewId);

        if (infoList != null && infoList.size() > 0) {
            for (AccessibilityNodeInfo nodeInfo : infoList) {
                if (nodeInfo != null) {
                    performClick(nodeInfo);
                }
            }
        } else {
            Log.e(TAG, viewId + " = is null");
        }
    }


 /**
     * 設置ListView列表逐行往下滾動(GridView也類似)
     *
     * @param viewId
     */
    private void setListScrollDown(String viewId) {
        Log.e(TAG, "列表往下滾動>>>>>");
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return;
        }
        List<AccessibilityNodeInfo> infoList = root.findAccessibilityNodeInfosByViewId(viewId);
        Log.e(TAG, "list.size: " + infoList.size());
        if (infoList != null && infoList.size() > 0) {
            AccessibilityNodeInfo nodeInfo = infoList.get(0);
            if (nodeInfo != null && getNodeClass(nodeInfo).contains("ListView")) {
                Log.e(TAG, "item 數量: " + nodeInfo.getChildCount());
                for (int k = 0; k < nodeInfo.getChildCount(); k++) {
                    AccessibilityNodeInfo child = nodeInfo.getChild(k);
                    if (child != null) {
                        //逐行滾動。
                        child.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
                        child.performAction(AccessibilityNodeInfo.ACTION_SELECT);
                        // getItemInfoClick(child.getViewIdResourceName());
                    }
                }
            }
        }
    }


  /**
     * 設置選中列表指定item並觸發點擊事件,(GridView也類似)
     * infoList的大小爲當前可見item數量,position的值爲當前列表item的位置
     * @param viewId
     * @param position 
     */
    private void setSelectedListItem(String viewId, int position) {
        Log.e(TAG, "選中ListView列表item>>>>>");
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return;
        }
        List<AccessibilityNodeInfo> infoList = root.findAccessibilityNodeInfosByViewId(viewId);
        Log.e(TAG, "list.size: " + infoList.size());
        if (infoList != null && infoList.size() > 0) {
            AccessibilityNodeInfo nodeInfo = infoList.get(0);
            if (nodeInfo != null && getNodeClass(nodeInfo).contains("ListView")) {
                int childCount = nodeInfo.getChildCount();
                if (position >= 0 && position <= childCount-1) {
                    AccessibilityNodeInfo child = nodeInfo.getChild(position);
                    if (child != null) {
                        child.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
                        child.performAction(AccessibilityNodeInfo.ACTION_SELECT);
                        child.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    }
                } else {
                    AccessibilityNodeInfo child = nodeInfo.getChild(0);
                    child.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                }
            }
        }
    }


  /**
     * 設置ListView滾動到頂部(GridView也類似)
     *
     * @param viewId
     */
    private void setListScrollTop(String viewId) {
        Log.e(TAG, "列表往上滾動到頂部>>>>>");
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return;
        }
        List<AccessibilityNodeInfo> infoList = root.findAccessibilityNodeInfosByViewId(viewId);
        Log.e(TAG, "list.size: " + infoList.size());
        if (infoList != null && infoList.size() > 0) {
            AccessibilityNodeInfo nodeInfo = infoList.get(0);
            if (nodeInfo != null && getNodeClass(nodeInfo).contains("ListView")) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
            }
        }
    }


 /**
     * 路線規劃列表設置選中路線
     * 對於LinearLayout的子view以及RadioGroup等組件內的子控件點擊事件可用
     * @param viewId
     * @param position 需要選中的路線
     */
    private void setSelectedRoute(String viewId, int position) {
        Log.e(TAG, "setSelectedRoute");
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return;
        }
        List<AccessibilityNodeInfo> infoList = root.findAccessibilityNodeInfosByViewId(viewId);
        if (infoList != null && infoList.size() == 1) {
            int childCount = infoList.get(0).getChildCount();
            if (position >= 0 && position <= childCount - 1) {
                AccessibilityNodeInfo child = infoList.get(0).getChild(position);
                child.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            } else {
                AccessibilityNodeInfo child = infoList.get(0).getChild(0);
                child.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
        }
    }

  /**
     * 獲取組件節點區域座標
     *
     * @param nodeInfo
     */
    private void getNodeRect(AccessibilityNodeInfo nodeInfo) {
        Rect rect = new Rect();
        nodeInfo.getBoundsInScreen(rect); //相對於屏幕的位置
     //   nodeInfo.getBoundsInParent(rect);//相對於父控件
        Log.e(TAG, "nodeInfo.rect: " + rect.left + "/" + rect.top + "/" + rect.right + "/" + rect.bottom);
    }

 /**
     * 根據文字獲取組件id及事件處理
     *
     * @param text
     */
    private void getNodeByText(String text) {
        Log.e(TAG, "getNodeByText");
        AccessibilityNodeInfo root = getRootInActiveWindow();
        List<AccessibilityNodeInfo> infoList = root.findAccessibilityNodeInfosByText(text);
        if (infoList != null && infoList.size() > 0) {
            Log.e(TAG, "getNodeByText infoList.size=>" + infoList.size());
            for (AccessibilityNodeInfo nodeInfo : infoList) {
                if (nodeInfo != null) {
                    Log.e(TAG, "getNodeByText nodeInfo.name=>" + nodeInfo.getViewIdResourceName());
                    getNodeRect(nodeInfo);
                    performClick(nodeInfo);
                }
            }
        } else {
            Log.e(TAG, text + "view = is null");
        }
    }

7.問題:

對於高德導航頁面點擊地圖彈出退出導航,繼續導航菜單的模擬點擊目前無法實現,只能採用adb命令模擬點擊屏幕實現;

另外對於ScrollView的滑動問題一直沒有找到解決方式;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章