├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── win
│ │ └── lioil
│ │ └── autoInstall
│ │ └── auto
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── win
│ │ │ └── lioil
│ │ │ └── autoInstall
│ │ │ ├── AccessibilityUtil.java
│ │ │ ├── AutoInstallService.java
│ │ │ ├── InstallUtil.java
│ │ │ └── MainActivity.java
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ ├── accessibility_config.xml
│ │ └── file_paths.xml
│ └── test
│ └── java
│ └── win
│ └── lioil
│ └── autoInstall
│ └── auto
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 参考:
2 | http://www.infoq.com/cn/articles/android-accessibility-installing
3 | https://developer.android.com/guide/topics/ui/accessibility/services
4 | https://developer.android.com/training/accessibility/service
5 |
6 | ## 一.Android Accessibility 简介
7 | 对于那些失明或低视力,色盲,耳聋或听力受损,以及运动技能受限的用户,
8 | Android提供了Accessibility(辅助功能/无障碍)更加简单地操作设备,
9 | 包括文字转语音、触觉反馈、手势操作、轨迹球和手柄操作等。
10 |
11 | 在Android 4.0以前,Accessibility功能单一,仅能过单向获取窗口信息(获取输入框内容);
12 | 在Android 4.1以后,Accessibility增加了与窗口元素的双向交互,可以操作窗口元素(点击按钮)。
13 |
14 | 近期项目需要在小米/华为手机上自动安装/升级APP(不能root),市面上大部分的应用市场APP都通过辅助功能实现免root自动安装,
15 | 于是想借鉴一下方案,试用了5个APP:豌豆荚,360手机助手,百度手机助手,腾讯应用宝,应用汇。
16 | 腾讯应用宝竟然没有实现免root自动安装,必须要root才能自动安装,难道是我没发现设置按钮,找到的麻烦通知一声。。。
17 | 在华为上这几个自动安装都是失效的,在小米上只有豌豆荚(要单独下载插件APP,估计是对小米单独适配了)。
18 |
19 | 自动安装原理(Accessibility):
20 | 启动"x.x.packageinstaller"系统安装界面,获取"安装"按钮,然后模拟用户点击,直到安装结束。
21 | 技术实现看起来非常简单,麻烦在于国内千奇百怪Android系统安装界面,现在只能自己动手适配项目需要的几台手机。。。
22 |
23 | ## 二.自动安装的基本步骤
24 | 完整源码:https://github.com/lifegh/AutoInstall
25 |
26 | ### 1.manifest添加辅助服务, res/xml配置辅助功能
27 | 在AndroidManifest.xml中
28 |
29 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 | 在res/xml/accessibility_config中
51 |
58 |
59 |
60 |
69 |
70 | 注意:[来源豌豆荚 http://www.infoq.com/cn/articles/android-accessibility-installing ]
71 | 在一些使用虚拟键盘的APP中,经常会出现这样的逻辑
72 | Button button = (Button) findViewById(R.id.button);
73 | String num = (String) button.getText();
74 | 在一般情况下,getText方法的返回值是Java.lang.String类的实例,上面这段代码可以正确运行。
75 | 但是在开启Accessibility Service之后,如果没有指定 packageNames, 系统会对所有APP的UI都进行Accessible的处理。
76 | 在这个例子中的表现就是getText方法的返回值变成了android.text.SpannableString类的实例
77 | (Java.lang.String和android.text.SpannableString都实现了java.lang.CharSequence接口),进而造成目标APP崩溃。
78 | 所以强烈建议在注册Accessibility Service时指定目标APP的packageName,
79 | 以减少手机上其他应用的莫名崩溃(代码中有这样的逻辑的各位,也请默默的改为调用toString()方法吧)。
80 |
81 | ### 2.继承服务AccessibilityService,实现自动安装
82 | public class AutoInstallService extends AccessibilityService {
83 | private static final String TAG = AutoInstallService.class.getSimpleName();
84 | private static final int DELAY_PAGE = 320; // 页面切换时间
85 | private final Handler mHandler = new Handler();
86 |
87 | ......
88 |
89 | @Override
90 | public void onAccessibilityEvent(AccessibilityEvent event) {
91 | if (event == null || !event.getPackageName().toString()
92 | .contains("packageinstaller"))//不写完整包名,是因为某些手机(如小米)安装器包名是自定义的
93 | return;
94 | /*
95 | 某些手机安装页事件返回节点有可能为null,无法获取安装按钮
96 | 例如华为mate10安装页就会出现event.getSource()为null,所以取巧改变当前页面状态,重新获取节点。
97 | 该方法在华为mate10上生效,但其它手机没有验证...(目前小米手机没有出现这个问题)
98 | */
99 | Log.i(TAG, "onAccessibilityEvent: " + event);
100 | AccessibilityNodeInfo eventNode = event.getSource();
101 | if (eventNode == null) {
102 | Log.i(TAG, "eventNode: null, 重新获取eventNode...");
103 | performGlobalAction(GLOBAL_ACTION_RECENTS); // 打开最近页面
104 | mHandler.postDelayed(new Runnable() {
105 | @Override
106 | public void run() {
107 | performGlobalAction(GLOBAL_ACTION_BACK); // 返回安装页面
108 | }
109 | }, DELAY_PAGE);
110 | return;
111 | }
112 |
113 | /*
114 | 模拟点击->自动安装,只验证了小米5s plus(MIUI 9.8.4.26)、小米Redmi 5A(MIUI 9.2)、华为mate 10
115 | 其它品牌手机可能还要适配,适配最可恶的就是出现安装广告按钮,误点安装其它垃圾APP(典型就是小米安装后广告推荐按钮,华为安装开始官方安装)
116 | */
117 | AccessibilityNodeInfo rootNode = getRootInActiveWindow(); //当前窗口根节点
118 | if (rootNode == null)
119 | return;
120 | Log.i(TAG, "rootNode: " + rootNode);
121 | if (isNotAD(rootNode))
122 | findTxtClick(rootNode, "安装"); //一起执行:安装->下一步->打开,以防意外漏掉节点
123 | findTxtClick(rootNode, "继续安装");
124 | findTxtClick(rootNode, "下一步");
125 | findTxtClick(rootNode, "打开");
126 | // 回收节点实例来重用
127 | eventNode.recycle();
128 | rootNode.recycle();
129 | }
130 |
131 | // 查找安装,并模拟点击(findAccessibilityNodeInfosByText判断逻辑是contains而非equals)
132 | private void findTxtClick(AccessibilityNodeInfo nodeInfo, String txt) {
133 | List nodes = nodeInfo.findAccessibilityNodeInfosByText(txt);
134 | if (nodes == null || nodes.isEmpty())
135 | return;
136 | Log.i(TAG, "findTxtClick: " + txt + ", " + nodes.size() + ", " + nodes);
137 | for (AccessibilityNodeInfo node : nodes) {
138 | if (node.isEnabled() && node.isClickable() && (node.getClassName().equals("android.widget.Button")
139 | || node.getClassName().equals("android.widget.CheckBox") // 兼容华为安装界面的复选框
140 | )) {
141 | node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
142 | }
143 | }
144 | }
145 |
146 | // 排除广告[安装]按钮
147 | private boolean isNotAD(AccessibilityNodeInfo rootNode) {
148 | return isNotFind(rootNode, "还喜欢") //小米
149 | && isNotFind(rootNode, "官方安装"); //华为
150 | }
151 |
152 | private boolean isNotFind(AccessibilityNodeInfo rootNode, String txt) {
153 | List nodes = rootNode.findAccessibilityNodeInfosByText(txt);
154 | return nodes == null || nodes.isEmpty();
155 | }
156 | }
157 |
158 | ### 3.退出"辅助功能/无障碍"设置
159 | public class AutoInstallService extends AccessibilityService {
160 | private static final String TAG = AutoInstallService.class.getSimpleName();
161 | private static final int DELAY_PAGE = 320; // 页面切换时间
162 | private final Handler mHandler = new Handler();
163 |
164 | @Override
165 | protected void onServiceConnected() {
166 | Log.i(TAG, "onServiceConnected: ");
167 | Toast.makeText(this, getString(R.string.aby_label) + "开启了", Toast.LENGTH_LONG).show();
168 | // 服务开启,模拟两次返回键,退出系统设置界面(实际上还应该检查当前UI是否为系统设置界面,但一想到有些厂商可能篡改设置界面,懒得适配了...)
169 | performGlobalAction(GLOBAL_ACTION_BACK);
170 | mHandler.postDelayed(new Runnable() {
171 | @Override
172 | public void run() {
173 | performGlobalAction(GLOBAL_ACTION_BACK);
174 | }
175 | }, DELAY_PAGE);
176 | }
177 |
178 | @Override
179 | public void onDestroy() {
180 | Log.i(TAG, "onDestroy: ");
181 | Toast.makeText(this, getString(R.string.aby_label) + "停止了,请重新开启", Toast.LENGTH_LONG).show();
182 | // 服务停止,重新进入系统设置界面
183 | AccessibilityUtil.jumpToSetting(this);
184 | }
185 |
186 | ......
187 | }
188 |
189 | ### 4.开启"辅助功能/无障碍"设置
190 | public class AccessibilityUtil {
191 | ......
192 | /**
193 | * 检查系统设置:是否开启辅助服务
194 | * @param service 辅助服务
195 | */
196 | private static boolean isSettingOpen(Class service, Context cxt) {
197 | try {
198 | int enable = Settings.Secure.getInt(cxt.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED, 0);
199 | if (enable != 1)
200 | return false;
201 | String services = Settings.Secure.getString(cxt.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
202 | if (!TextUtils.isEmpty(services)) {
203 | TextUtils.SimpleStringSplitter split = new TextUtils.SimpleStringSplitter(':');
204 | split.setString(services);
205 | while (split.hasNext()) { // 遍历所有已开启的辅助服务名
206 | if (split.next().equalsIgnoreCase(cxt.getPackageName() + "/" + service.getName()))
207 | return true;
208 | }
209 | }
210 | } catch (Throwable e) {//若出现异常,则说明该手机设置被厂商篡改了,需要适配
211 | Log.e(TAG, "isSettingOpen: " + e.getMessage());
212 | }
213 | return false;
214 | }
215 |
216 | /**
217 | * 跳转到系统设置:开启辅助服务
218 | */
219 | public static void jumpToSetting(final Context cxt) {
220 | try {
221 | cxt.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
222 | } catch (Throwable e) {//若出现异常,则说明该手机设置被厂商篡改了,需要适配
223 | try {
224 | Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
225 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
226 | cxt.startActivity(intent);
227 | } catch (Throwable e2) {
228 | Log.e(TAG, "jumpToSetting: " + e2.getMessage());
229 | }
230 | }
231 | }
232 | }
233 |
234 | ### 5.允许"未知来源"设置
235 | public class InstallUtil {
236 | ......
237 |
238 | /**
239 | * 检查系统设置:是否允许安装来自未知来源的应用
240 | */
241 | private static boolean isSettingOpen(Context cxt) {
242 | boolean canInstall;
243 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) // Android 8.0
244 | canInstall = cxt.getPackageManager().canRequestPackageInstalls();
245 | else
246 | canInstall = Settings.Secure.getInt(cxt.getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS, 0) == 1;
247 | return canInstall;
248 | }
249 |
250 | /**
251 | * 跳转到系统设置:允许安装来自未知来源的应用
252 | */
253 | private static void jumpToInstallSetting(Context cxt) {
254 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) // Android 8.0
255 | cxt.startActivity(new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + cxt.getPackageName())));
256 | else
257 | cxt.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS));
258 | }
259 |
260 | /**
261 | * 安装APK
262 | *
263 | * @param apkFile APK文件的本地路径
264 | */
265 | public static void install(Context cxt, File apkFile) {
266 | AccessibilityUtil.wakeUpScreen(cxt); //唤醒屏幕,以便辅助功能模拟用户点击"安装"
267 | try {
268 | Intent intent = new Intent(Intent.ACTION_VIEW);
269 | Uri uri;
270 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
271 | // Android 7.0以上不允许Uri包含File实际路径,需要借助FileProvider生成Uri(或者调低targetSdkVersion小于Android 7.0欺骗系统)
272 | uri = FileProvider.getUriForFile(cxt, cxt.getPackageName() + ".fileProvider", apkFile);
273 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
274 | } else {
275 | uri = Uri.fromFile(apkFile);
276 | }
277 | intent.setDataAndType(uri, "application/vnd.android.package-archive");
278 | cxt.startActivity(intent);
279 | } catch (Throwable e) {
280 | Toast.makeText(cxt, "安装失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
281 | }
282 | }
283 | }
284 |
285 | 简书: https://www.jianshu.com/p/04ebe2641290
286 | CSDN: https://blog.csdn.net/qq_32115439/article/details/80261568
287 | GitHub博客: http://lioil.win/2018/05/09/Android-Accessibility.html
288 | Coding博客: http://c.lioil.win/2018/05/09/Android-Accessibility.html
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion cSV
5 | buildToolsVersion bTV
6 | defaultConfig {
7 | applicationId "win.lioil.autoInstall"
8 | minSdkVersion 17
9 | targetSdkVersion 28
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | shrinkResources true
17 | minifyEnabled true
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | }
22 |
23 | dependencies {
24 | implementation fileTree(dir: 'libs', include: ['*.jar'])
25 | implementation 'com.android.support:appcompat-v7:28.0.0'
26 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
27 | testImplementation 'junit:junit:4.12'
28 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
29 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
30 | }
31 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/win/lioil/autoInstall/auto/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package win.lioil.autoInstall.auto;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.life.autoinstall", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
45 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/java/win/lioil/autoInstall/AccessibilityUtil.java:
--------------------------------------------------------------------------------
1 | package win.lioil.autoInstall;
2 |
3 | import android.app.AlertDialog;
4 | import android.app.KeyguardManager;
5 | import android.content.Context;
6 | import android.content.DialogInterface;
7 | import android.content.Intent;
8 | import android.os.PowerManager;
9 | import android.provider.Settings;
10 | import android.text.TextUtils;
11 | import android.util.Log;
12 |
13 | /**
14 | * 辅助功能/无障碍相关工具
15 | */
16 | public class AccessibilityUtil {
17 | private static final String TAG = AccessibilityUtil.class.getSimpleName();
18 |
19 | /**
20 | * 检查系统设置,并显示设置对话框
21 | *
22 | * @param service 辅助服务
23 | */
24 | public static void checkSetting(final Context cxt, Class service) {
25 | if (isSettingOpen(service, cxt))
26 | return;
27 | new AlertDialog.Builder(cxt)
28 | .setTitle(R.string.aby_setting_title)
29 | .setMessage(R.string.aby_setting_msg)
30 | .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
31 | @Override
32 | public void onClick(DialogInterface dialog, int which) {
33 | jumpToSetting(cxt);
34 | }
35 | })
36 | .show();
37 | }
38 |
39 | /**
40 | * 检查系统设置:是否开启辅助服务
41 | *
42 | * @param service 辅助服务
43 | */
44 | private static boolean isSettingOpen(Class service, Context cxt) {
45 | try {
46 | int enable = Settings.Secure.getInt(cxt.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED, 0);
47 | if (enable != 1)
48 | return false;
49 | String services = Settings.Secure.getString(cxt.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
50 | if (!TextUtils.isEmpty(services)) {
51 | TextUtils.SimpleStringSplitter split = new TextUtils.SimpleStringSplitter(':');
52 | split.setString(services);
53 | while (split.hasNext()) { // 遍历所有已开启的辅助服务名
54 | if (split.next().equalsIgnoreCase(cxt.getPackageName() + "/" + service.getName()))
55 | return true;
56 | }
57 | }
58 | } catch (Throwable e) {//若出现异常,则说明该手机设置被厂商篡改了,需要适配
59 | Log.e(TAG, "isSettingOpen: " + e.getMessage());
60 | }
61 | return false;
62 | }
63 |
64 | /**
65 | * 跳转到系统设置:开启辅助服务
66 | */
67 | public static void jumpToSetting(final Context cxt) {
68 | try {
69 | cxt.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
70 | } catch (Throwable e) {//若出现异常,则说明该手机设置被厂商篡改了,需要适配
71 | try {
72 | Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
73 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
74 | cxt.startActivity(intent);
75 | } catch (Throwable e2) {
76 | Log.e(TAG, "jumpToSetting: " + e2.getMessage());
77 | }
78 | }
79 | }
80 |
81 | /**
82 | * 唤醒点亮和解锁屏幕(60s)
83 | */
84 | public static void wakeUpScreen(Context context) {
85 | try {
86 | //唤醒点亮屏幕
87 | PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
88 | if (pm != null && pm.isScreenOn()) {
89 | PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_DIM_WAKE_LOCK, "wakeUpScreen");
90 | wl.acquire(60000); // 60s后释放锁
91 | }
92 |
93 | //解锁屏幕
94 | KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
95 | if (km != null && km.inKeyguardRestrictedInputMode()) {
96 | KeyguardManager.KeyguardLock kl = km.newKeyguardLock("unLock");
97 | kl.disableKeyguard();
98 | }
99 | } catch (Throwable e) {
100 | Log.e(TAG, "wakeUpScreen: " + e.getMessage());
101 | }
102 | }
103 | }
--------------------------------------------------------------------------------
/app/src/main/java/win/lioil/autoInstall/AutoInstallService.java:
--------------------------------------------------------------------------------
1 | package win.lioil.autoInstall;
2 |
3 | import android.accessibilityservice.AccessibilityService;
4 | import android.os.Handler;
5 | import android.util.Log;
6 | import android.view.accessibility.AccessibilityEvent;
7 | import android.view.accessibility.AccessibilityNodeInfo;
8 | import android.widget.Toast;
9 |
10 | import java.util.List;
11 |
12 | /**
13 | * 辅助服务自动安装APP,该服务在单独进程中允许
14 | */
15 | public class AutoInstallService extends AccessibilityService {
16 | private static final String TAG = AutoInstallService.class.getSimpleName();
17 | private static final int DELAY_PAGE = 320; // 页面切换时间
18 | private final Handler mHandler = new Handler();
19 |
20 | @Override
21 | protected void onServiceConnected() {
22 | Log.i(TAG, "onServiceConnected: ");
23 | Toast.makeText(this, getString(R.string.aby_label) + "开启了", Toast.LENGTH_LONG).show();
24 | // 服务开启,模拟两次返回键,退出系统设置界面(实际上还应该检查当前UI是否为系统设置界面,但一想到有些厂商可能篡改设置界面,懒得适配了...)
25 | performGlobalAction(GLOBAL_ACTION_BACK);
26 | mHandler.postDelayed(new Runnable() {
27 | @Override
28 | public void run() {
29 | performGlobalAction(GLOBAL_ACTION_BACK);
30 | }
31 | }, DELAY_PAGE);
32 | }
33 |
34 | @Override
35 | public void onDestroy() {
36 | Log.i(TAG, "onDestroy: ");
37 | Toast.makeText(this, getString(R.string.aby_label) + "停止了,请重新开启", Toast.LENGTH_LONG).show();
38 | // 服务停止,重新进入系统设置界面
39 | AccessibilityUtil.jumpToSetting(this);
40 | }
41 |
42 | @Override
43 | public void onAccessibilityEvent(AccessibilityEvent event) {
44 | if (event == null || !event.getPackageName().toString()
45 | .contains("packageinstaller"))//不写完整包名,是因为某些手机(如小米)安装器包名是自定义的
46 | return;
47 | /*
48 | 某些手机安装页事件返回节点有可能为null,无法获取安装按钮
49 | 例如华为mate10安装页就会出现event.getSource()为null,所以取巧改变当前页面状态,重新获取节点。
50 | 该方法在华为mate10上生效,但其它手机没有验证...(目前小米手机没有出现这个问题)
51 | */
52 | Log.i(TAG, "onAccessibilityEvent: " + event);
53 | AccessibilityNodeInfo eventNode = event.getSource();
54 | if (eventNode == null) {
55 | Log.i(TAG, "eventNode: null, 重新获取eventNode...");
56 | performGlobalAction(GLOBAL_ACTION_RECENTS); // 打开最近页面
57 | mHandler.postDelayed(new Runnable() {
58 | @Override
59 | public void run() {
60 | performGlobalAction(GLOBAL_ACTION_BACK); // 返回安装页面
61 | }
62 | }, DELAY_PAGE);
63 | return;
64 | }
65 |
66 | /*
67 | 模拟点击->自动安装,只验证了小米5s plus(MIUI 9.8.4.26)、小米Redmi 5A(MIUI 9.2)、华为mate 10
68 | 其它品牌手机可能还要适配,适配最可恶的就是出现安装广告按钮,误点安装其它垃圾APP(典型就是小米安装后广告推荐按钮,华为安装开始官方安装)
69 | */
70 | AccessibilityNodeInfo rootNode = getRootInActiveWindow(); //当前窗口根节点
71 | if (rootNode == null)
72 | return;
73 | Log.i(TAG, "rootNode: " + rootNode);
74 | if (isNotAD(rootNode))
75 | findTxtClick(rootNode, "安装"); //一起执行:安装->下一步->打开,以防意外漏掉节点
76 | findTxtClick(rootNode, "继续安装");
77 | findTxtClick(rootNode, "下一步");
78 | findTxtClick(rootNode, "打开");
79 | // 回收节点实例来重用
80 | eventNode.recycle();
81 | rootNode.recycle();
82 | }
83 |
84 | // 查找安装,并模拟点击(findAccessibilityNodeInfosByText判断逻辑是contains而非equals)
85 | private void findTxtClick(AccessibilityNodeInfo nodeInfo, String txt) {
86 | List nodes = nodeInfo.findAccessibilityNodeInfosByText(txt);
87 | if (nodes == null || nodes.isEmpty())
88 | return;
89 | Log.i(TAG, "findTxtClick: " + txt + ", " + nodes.size() + ", " + nodes);
90 | for (AccessibilityNodeInfo node : nodes) {
91 | if (node.isEnabled() && node.isClickable() && (node.getClassName().equals("android.widget.Button")
92 | || node.getClassName().equals("android.widget.CheckBox") // 兼容华为安装界面的复选框
93 | )) {
94 | node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
95 | }
96 | }
97 | }
98 |
99 | // 排除广告[安装]按钮
100 | private boolean isNotAD(AccessibilityNodeInfo rootNode) {
101 | return isNotFind(rootNode, "还喜欢") //小米
102 | && isNotFind(rootNode, "官方安装"); //华为
103 | }
104 |
105 | private boolean isNotFind(AccessibilityNodeInfo rootNode, String txt) {
106 | List nodes = rootNode.findAccessibilityNodeInfosByText(txt);
107 | return nodes == null || nodes.isEmpty();
108 | }
109 |
110 | @Override
111 | public void onInterrupt() {
112 | }
113 | }
--------------------------------------------------------------------------------
/app/src/main/java/win/lioil/autoInstall/InstallUtil.java:
--------------------------------------------------------------------------------
1 | package win.lioil.autoInstall;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.Context;
5 | import android.content.DialogInterface;
6 | import android.content.Intent;
7 | import android.net.Uri;
8 | import android.os.Build;
9 | import android.provider.Settings;
10 | import android.support.v4.content.FileProvider;
11 | import android.widget.Toast;
12 |
13 | import java.io.File;
14 |
15 | /**
16 | * 安装相关工具
17 | */
18 | public class InstallUtil {
19 | private static final String TAG = InstallUtil.class.getSimpleName();
20 |
21 | /**
22 | * 检查系统设置,并显示设置对话框
23 | */
24 | public static void checkSetting(final Context cxt) {
25 | if (isSettingOpen(cxt))
26 | return;
27 | new AlertDialog.Builder(cxt)
28 | .setTitle(R.string.unknow_setting_title)
29 | .setMessage(R.string.unknow_setting_msg)
30 | .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
31 | @Override
32 | public void onClick(DialogInterface dialog, int which) {
33 | jumpToInstallSetting(cxt);
34 | }
35 | })
36 | .show();
37 | }
38 |
39 | /**
40 | * 检查系统设置:是否允许安装来自未知来源的应用
41 | */
42 | private static boolean isSettingOpen(Context cxt) {
43 | boolean canInstall;
44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) // Android 8.0
45 | canInstall = cxt.getPackageManager().canRequestPackageInstalls();
46 | else
47 | canInstall = Settings.Secure.getInt(cxt.getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS, 0) == 1;
48 | return canInstall;
49 | }
50 |
51 | /**
52 | * 跳转到系统设置:允许安装来自未知来源的应用
53 | */
54 | private static void jumpToInstallSetting(Context cxt) {
55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) // Android 8.0
56 | cxt.startActivity(new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + cxt.getPackageName())));
57 | else
58 | cxt.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS));
59 | }
60 |
61 | /**
62 | * 安装APK
63 | *
64 | * @param apkFile APK文件的本地路径
65 | */
66 | public static void install(Context cxt, File apkFile) {
67 | AccessibilityUtil.wakeUpScreen(cxt); //唤醒屏幕,以便辅助功能模拟用户点击"安装"
68 | try {
69 | Intent intent = new Intent(Intent.ACTION_VIEW);
70 | Uri uri;
71 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
72 | // Android 7.0以上不允许Uri包含File实际路径,需要借助FileProvider生成Uri(或者调低targetSdkVersion小于Android 7.0欺骗系统)
73 | uri = FileProvider.getUriForFile(cxt, cxt.getPackageName() + ".fileProvider", apkFile);
74 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
75 | } else {
76 | uri = Uri.fromFile(apkFile);
77 | }
78 | intent.setDataAndType(uri, "application/vnd.android.package-archive");
79 | cxt.startActivity(intent);
80 | } catch (Throwable e) {
81 | Toast.makeText(cxt, "安装失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/app/src/main/java/win/lioil/autoInstall/MainActivity.java:
--------------------------------------------------------------------------------
1 | package win.lioil.autoInstall;
2 |
3 | import android.Manifest;
4 | import android.app.Activity;
5 | import android.os.Build;
6 | import android.os.Bundle;
7 | import android.view.View;
8 | import android.widget.EditText;
9 |
10 | import java.io.File;
11 |
12 | public class MainActivity extends Activity {
13 | private static final String TAG = MainActivity.class.getSimpleName();
14 | private EditText mEditText;
15 |
16 | @Override
17 | protected void onCreate(Bundle savedInstanceState) {
18 | super.onCreate(savedInstanceState);
19 | setContentView(R.layout.activity_main);
20 | mEditText = findViewById(R.id.apk_path);
21 |
22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
23 | requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1); // 动态申请读取权限
24 | InstallUtil.checkSetting(this); // "未知来源"设置
25 | AccessibilityUtil.checkSetting(MainActivity.this, AutoInstallService.class); // "辅助功能"设置
26 | }
27 |
28 | public void start(View view) {
29 | InstallUtil.install(MainActivity.this, new File(mEditText.getText().toString()));
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
18 |
19 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lioilwin/AutoInstall/81f2859ee5424273d4d756e5f7534944d4f4ada9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 自动安装
3 | 开始
4 |
5 | 自动安装服务
6 | 开启本服务后,在安装应用的过程中会辅助点击相应按钮,实现自动安装和启动应用,省去频繁手动点击。
7 |
8 | 打开系统设置:\n1.在辅助功能(无障碍)中,找到并勾选\"自动安装服务\";\n2.在安全中,找到并勾选\"未知来源\"。
9 | \"辅助功能(无障碍)\"设置
10 | 找到并勾选\"自动安装服务\"。
11 | \"未知来源\"设置
12 | 找到并勾选\"未知来源\"。
13 | 确定
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/accessibility_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
9 |
12 |
15 |
18 |
21 |
--------------------------------------------------------------------------------
/app/src/test/java/win/lioil/autoInstall/auto/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package win.lioil.autoInstall.auto;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.cSV = 28
3 | ext.bTV = '28.0.3'
4 | repositories {
5 | maven {
6 | url 'http://maven.aliyun.com/nexus/content/groups/public/'
7 | }
8 | google()
9 | jcenter()
10 | }
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:3.2.1'
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | maven {
19 | url 'http://maven.aliyun.com/nexus/content/groups/public/'
20 | }
21 | google()
22 | jcenter()
23 | }
24 | }
25 |
26 | task clean(type: Delete) {
27 | delete rootProject.buildDir
28 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------