1. 程式人生 > >【轉】Android-Accessibility(輔助功能/無障礙,自動安裝APP)

【轉】Android-Accessibility(輔助功能/無障礙,自動安裝APP)

參考:
http://www.infoq.com/cn/articles/android-accessibility-installing
https://developer.android.com/guide/topics/ui/accessibility/services
https://developer.android.com/training/accessibility/service

一.Android Accessibility 簡介

對於那些失明或低視力,色盲,耳聾或聽力受損,以及運動技能受限的使用者,
Android提供了Accessibility(輔助功能/無障礙)更加簡單地操作裝置,
包括文字轉語音、觸覺反饋、手勢操作、軌跡球和手柄操作等。

在Android 4.0以前,Accessibility功能單一,僅能過單向獲取視窗資訊(獲取輸入框內容);
在Android 4.1以後,Accessibility增加了與視窗元素的雙向互動,可以操作視窗元素(點選按鈕)。

近期專案需要在小米/華為手機上自動安裝/升級APP(不能root),市面上大部分的應用市場APP都通過輔助功能實現免root自動安裝,
於是想借鑑一下方案,試用了5個APP:豌豆莢,360手機助手,百度手機助手,騰訊應用寶,應用匯。
騰訊應用寶竟然沒有實現免root自動安裝,必須要root才能自動安裝,難道是我沒發現設定按鈕,找到的麻煩通知一聲。。。
在華為上這幾個自動安裝都是失效的,在小米上只有豌豆莢(要單獨下載外掛APP,估計是對小米單獨適配了)。

自動安裝原理(Accessibility):
    啟動"x.x.packageinstaller"系統安裝介面,獲取"安裝"按鈕,然後模擬使用者點選,直到安裝結束。    
技術實現看起來非常簡單,麻煩在於國內千奇百怪Android系統安裝介面,現在只能自己動手適配專案需要的幾臺手機。。。

二.自動安裝的基本步驟

完整原始碼:https://github.com/lifegh/AutoInstall

1.manifest新增輔助服務, res/xml配置輔助功能

在AndroidManifest.xml中
<application ...>
    <!--
    android:label(可選) 在輔助功能(無障礙)的系統設定會使用該名稱,若不設定,則會使用<application android:label
    android:process(可選) 把該服務設在單獨程序中,程序名以[冒號:]開頭,是本應用的私有程序,其它應用無法訪問
    android:permission(必需) 新增許可權以確保只有系統可以繫結到該服務
    -->
    <service android:name=".AutoInstallService" android:label="@string/aby_label" android:process=":install" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <!--在xml檔案配置輔助功能,也可在onServiceConnected()中使用setServiceInfo()動態配置--> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_config" /> </service> </application> 在res/xml/accessibility_config中 <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/aby_desc" android:notificationTimeout="100"/> <!--android:packageNames="com.android.packageinstaller" 國內有不少手機都是自定義安裝介面,packageNames不固定--> <!-- android:description 輔助功能描述 android:packageNames 指定輔助功能監聽的包名,不指定表示監聽所有應用 android:accessibilityEventTypes 事件型別,typeAllMask表示接收所有事件 android:accessibilityFlags 查詢截點方式,一般配置為flagDefault預設方式 android:accessibilityFeedbackType 操作按鈕以後給使用者的反饋型別,包括聲音,震動等 android:notificationTimeout 響應超時 android:canRetrieveWindowContent 是否允許提取視窗的節點資訊 --> 注意:[來源豌豆莢 http://www.infoq.com/cn/articles/android-accessibility-installing ] 在一些使用虛擬鍵盤的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.String和android.text.SpannableString都實現了java.lang.CharSequence介面),進而造成目標APP崩潰。 所以強烈建議在註冊Accessibility Service時指定目標APP的packageName, 以減少手機上其他應用的莫名崩潰(程式碼中有這樣的邏輯的各位,也請默默的改為呼叫toString()方法吧)。 

2.繼承服務AccessibilityService,實現自動安裝

public class AutoInstallService extends AccessibilityService { private static final String TAG = AutoInstallService.class.getSimpleName(); private static final int DELAY_PAGE = 320; // 頁面切換時間 private final Handler mHandler = new Handler(); ...... @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (event == null || !event.getPackageName().toString() .contains("packageinstaller"))//不寫完整包名,是因為某些手機(如小米)安裝器包名是自定義的 return; /* 某些手機安裝頁事件返回節點有可能為null,無法獲取安裝按鈕 例如華為mate10安裝頁就會出現event.getSource()為null,所以取巧改變當前頁面狀態,重新獲取節點。 該方法在華為mate10上生效,但其它手機沒有驗證...(目前小米手機沒有出現這個問題) */ Log.i(TAG, "onAccessibilityEvent: " + event); AccessibilityNodeInfo eventNode = event.getSource(); if (eventNode == null) { Log.i(TAG, "eventNode: null, 重新獲取eventNode..."); performGlobalAction(GLOBAL_ACTION_RECENTS); // 開啟最近頁面 mHandler.postDelayed(new Runnable() { @Override public void run() { performGlobalAction(GLOBAL_ACTION_BACK); // 返回安裝頁面 } }, DELAY_PAGE); return; } /* 模擬點選->自動安裝,只驗證了小米5s plus(MIUI 9.8.4.26)、小米Redmi 5A(MIUI 9.2)、華為mate 10 其它品牌手機可能還要適配,適配最可惡的就是出現安裝廣告按鈕,誤點安裝其它垃圾APP(典型就是小米安裝後廣告推薦按鈕,華為安裝開始官方安裝) */ AccessibilityNodeInfo rootNode = getRootInActiveWindow(); //當前視窗根節點 if (rootNode == null) return; Log.i(TAG, "rootNode: " + rootNode); if (isNotAD(rootNode)) findTxtClick(rootNode, "安裝"); //一起執行:安裝->下一步->開啟,以防意外漏掉節點 findTxtClick(rootNode, "繼續安裝"); findTxtClick(rootNode, "下一步"); findTxtClick(rootNode, "開啟"); // 回收節點例項來重用 eventNode.recycle(); rootNode.recycle(); } // 查詢安裝,並模擬點選(findAccessibilityNodeInfosByText判斷邏輯是contains而非equals) private void findTxtClick(AccessibilityNodeInfo nodeInfo, String txt) { List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByText(txt); if (nodes == null || nodes.isEmpty()) return; Log.i(TAG, "findTxtClick: " + txt + ", " + nodes.size() + ", " + nodes); for (AccessibilityNodeInfo node : nodes) { if (node.isEnabled() && node.isClickable() && (node.getClassName().equals("android.widget.Button") || node.getClassName().equals("android.widget.CheckBox") // 相容華為安裝介面的複選框 )) { node.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } } // 排除廣告[安裝]按鈕 private boolean isNotAD(AccessibilityNodeInfo rootNode) { return isNotFind(rootNode, "還喜歡") //小米 && isNotFind(rootNode, "官方安裝"); //華為 } private boolean isNotFind(AccessibilityNodeInfo rootNode, String txt) { List<AccessibilityNodeInfo> nodes = rootNode.findAccessibilityNodeInfosByText(txt); return nodes == null || nodes.isEmpty(); } } 

3.退出"輔助功能/無障礙"設定

public class AutoInstallService extends AccessibilityService { private static final String TAG = AutoInstallService.class.getSimpleName(); private static final int DELAY_PAGE = 320; // 頁面切換時間 private final Handler mHandler = new Handler(); @Override protected void onServiceConnected() { Log.i(TAG, "onServiceConnected: "); Toast.makeText(this, getString(R.string.aby_label) + "開啟了", Toast.LENGTH_LONG).show(); // 服務開啟,模擬兩次返回鍵,退出系統設定介面(實際上還應該檢查當前UI是否為系統設定介面,但一想到有些廠商可能篡改設定介面,懶得適配了...) performGlobalAction(GLOBAL_ACTION_BACK); mHandler.postDelayed(new Runnable() { @Override public void run() { performGlobalAction(GLOBAL_ACTION_BACK); } }, DELAY_PAGE); } @Override public void onDestroy() { Log.i(TAG, "onDestroy: "); Toast.makeText(this, getString(R.string.aby_label) + "停止了,請重新開啟", Toast.LENGTH_LONG).show(); // 服務停止,重新進入系統設定介面 AccessibilityUtil.jumpToSetting(this); } ...... } 

4.開啟"輔助功能/無障礙"設定

public class AccessibilityUtil {
    ......
    /** * 檢查系統設定:是否開啟輔助服務 * @param service 輔助服務 */ private static boolean isSettingOpen(Class service, Context cxt) { try { int enable = Settings.Secure.getInt(cxt.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED, 0); if (enable != 1) return false; String services = Settings.Secure.getString(cxt.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); if (!TextUtils.isEmpty(services)) { TextUtils.SimpleStringSplitter split = new TextUtils.SimpleStringSplitter(':'); split.setString(services); while (split.hasNext()) { // 遍歷所有已開啟的輔助服務名 if (split.next().equalsIgnoreCase(cxt.getPackageName() + "/" + service.getName())) return true; } } } catch (Throwable e) {//若出現異常,則說明該手機設定被廠商篡改了,需要適配 Log.e(TAG, "isSettingOpen: " + e.getMessage()); } return false; } /** * 跳轉到系統設定:開啟輔助服務 */ public static void jumpToSetting(final Context cxt) { try { cxt.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)); } catch (Throwable e) {//若出現異常,則說明該手機設定被廠商篡改了,需要適配 try { Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); cxt.startActivity(intent); } catch (Throwable e2) { Log.e(TAG, "jumpToSetting: " + e2.getMessage()); } } } } 

5.允許"未知來源"設定

public class InstallUtil {
    ......

    /** * 檢查系統設定:是否允許安裝來自未知來源的應用 */ private static boolean isSettingOpen(Context cxt) { boolean canInstall; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) // Android 8.0 canInstall = cxt.getPackageManager().canRequestPackageInstalls(); else canInstall = Settings.Secure.getInt(cxt.getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS, 0) == 1; return canInstall; } /** * 跳轉到系統設定:允許安裝來自未知來源的應用 */ private static void jumpToInstallSetting(Context cxt) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) // Android 8.0 cxt.startActivity(new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + cxt.getPackageName()))); else cxt.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS)); } /** * 安裝APK * * @param apkFile APK檔案的本地路徑 */ public static void install(Context cxt, File apkFile) { AccessibilityUtil.wakeUpScreen(cxt); //喚醒螢幕,以便輔助功能模擬使用者點選"安裝" try { Intent intent = new Intent(Intent.ACTION_VIEW); Uri uri; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // Android 7.0以上不允許Uri包含File實際路徑,需要藉助FileProvider生成Uri(或者調低targetSdkVersion小於Android 7.0欺騙系統) uri = FileProvider.getUriForFile(cxt, cxt.getPackageName() + ".fileProvider", apkFile); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { uri = Uri.fromFile(apkFile); } intent.setDataAndType(uri, "application/vnd.android.package-archive"); cxt.startActivity(intent); } catch (Throwable e) { Toast.makeText(cxt, "安裝失敗:" + e.getMessage(), Toast.LENGTH_LONG).show(); } } } 

簡書: https://www.jianshu.com/p/04ebe2641290
CSDN: https://blog.csdn.net/qq_32115439/article/details/80261568
GitHub部落格: http://lioil.win/2018/05/09/Android-Accessibility.html
Coding部落格: http://c.lioil.win/2018/05/09/Android-Accessibility.html



from : https://www.jianshu.com/p/04ebe2641290