IBigerBiger的成长之路

Android增量更新(二)静默安装

在前一篇中有说到关于增量更新的相关知识,在文章最后抛出了静默安装的概念,那么这一篇就对于Android的静默安装进行相关的讲解。

首先什么是静默安装?

静默安装,就是不用弹出系统的安装界面,在不影响用户任何操作的情况下不知不觉地将程序装好。这种方式看起来好像还不错,但是深思极恐,因为Android系统会在安装界面当中把程序所声明的权限展示给用户看,用户来评估一下这些权限然后决定是否要安装该程序,但如果使用了静默安装的方式,也就没有地方让用户看权限了,相当于用户被动接受了这些权限。这样一些腊鸡应用或者收费应用就会在神不知鬼不觉中安装到我们的手机中,会引发很多乱七八糟的问题,在Android官方看来,这显示是一种非常危险的行为,因此静默安装这一行为系统是不会开放给开发者的。

但是总是弹出一个安装对话框确实是一种体验比较差的行为,特别是对于用户刚刚使用手机,或者刷机,或者应用频繁更新时候,一直去点击安装,完成对于用户来说是不好的体验,这一点Google自己也意识到了,因此Android系统对自家的Google Play商店开放了静默安装权限,也就是说所有从Google Play上下载的应用都可以不用弹出安装对话框了。这一点充分说明了拥有权限的重要性,自家的系统想怎么改就怎么改。借鉴Google的做法,很多国内的手机厂商也采用了类似的处理方式,比如说小米手机在小米商店中下载应用也是不需要弹出安装对话框的。

那么我们从开发的角度应该如何实现静默安装呢?

当然静默安装要分两种情况考虑的

  • Root情况下的静默安装

  • 非Root情况下的静默安装

因为Root与非Root的差异比较大,Root后的方法再非Root的情况下不可使用,所以需要分为两种情况下讨论

Root情况下静默安装

Root情况下直接调用Android系统的Shell指令就可以了

首先看一下怎么去执行Shell指令,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void runShellCommand(String command) {
Process process = null;
BufferedReader bufferedReader = null;
StringBuilder mShellCommandSB =new StringBuilder();
Log.d("wenfeng", "runShellCommand :" + command);
mShellCommandSB.delete(0, mShellCommandSB.length());
String[] cmd = new String[] { "/system/bin/sh", "-c", command }; //调用bin文件
try {
byte b[] = new byte[1024];
process = Runtime.getRuntime().exec(cmd);
bufferedReader = new BufferedReader(new InputStreamReader(
process.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
mShellCommandSB.append(line);
}
Log.d("wenfeng", "runShellCommand result : " + mShellCommandSB.toString());
process.waitFor();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
// TODO: handle exception
}
}
if (process != null) {
process.destroy();
}
}
}

这是执行Shell命令的方法,而安装则可以通过两种命令来实现

1.调用pm指令,下面就是调用pm指令(pm install -r)把/sdcard/new.apk安装

1
runShellCommand("pm install -r /sdcard/new.apk");

2.调用cp指令,把apk拷贝到data/app目录下,然后用户下次重启时,apk就会自动安装。

1
runShellCommand("cp /sdcard/new.apk /data/app")

非Root情况下静默安装

上面的命令相关系统是不授予我们权限调用的,因此只能在拥有ROOT权限的手机上去申请权限才行。那么在非Root权限下自然就不能使用上述的方式了,所以我们只能通过其他的手段来实现相应的功能。

无障碍服务实现静默安装

当然这种方式并非严格意义上的静默安装,因为用户还是会看到安装界面的,只不过可以在安装界面当中释放用户的操作,由智能安装功能来模拟用户点击,安装完成之后自动关闭界面。很多应用商店都用到这种方式批量安装apk,下载后自动弹出安装界面,然后自动帮你点击安装。

这个无障碍服务最开始google设计是用于残疾人的,让他们更加方便的使用手机。这个功能是需要用户手动开启的,并且只支持Android 4.1之后的手机。

接下来我们看一下如何利用这个无障碍服务来说实现”静默安装”。

关于这个无障碍服务我们会在后面进行相关的讲解,所以这里就介绍的并非那么详细了

首先在res/xml目录下新建一个accessibility_service_config.xml文件,代码如下所示:

1
2
3
4
5
6
7
8
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:packageNames="com.android.packageinstaller"
android:description="@string/accessibility_service_description"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackGeneric"
android:canRetrieveWindowContent="true"
/>

接下来在AndroidManifest.xml文件里面配置无障碍服务:

1
2
3
4
5
6
7
8
9
10
11
12
<service
android:name=".SmartInstallService"
android:label="我的智能安装"
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/slient_install_config" />
</service>

接下来就是要去实现智能安装功能的具体逻辑了,创建一个SmartInstallService类并继承自AccessibilityService,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SmartInstallService extends AccessibilityService{
private String TAG = "SmartInstallService";
@Override
protected void onServiceConnected() {
super.onServiceConnected();
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
findAndPerformActionButton("安装");
findAndPerformActionButton("完成");
}
private void findAndPerformActionButton(String text) {
if (getRootInActiveWindow() == null)//取得当前激活窗体的根节点
return;
//通过文字找到当前的节点
List<AccessibilityNodeInfo> nodes = getRootInActiveWindow().findAccessibilityNodeInfosByText(text);
for (int i = 0; i < nodes.size(); i++) {
AccessibilityNodeInfo node = nodes.get(i);
// 执行点击行为
if (node.getClassName().equals("android.widget.Button") && node.isEnabled()) {
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
@Override
public void onInterrupt() {
}
}

这里的逻辑其实很简单,就是对于当前界面出现的按钮,判断按钮上的文字是不是“安装”、“完成”这几种类型,如果是的话就模拟一下点击事件,这样也就相当于帮用户自动操作了这些按钮。

当运行后就会发现设置里面的无障碍多出了一项,如下


Snip20170331_2.png

这里的我的智能安装就是前面我们设置的了,只要打开,上面的SmartInstallService就开始运行了,通过监听屏幕来进行相关的操作了

所以我们前面的增量更新就是如下效果了


Untitled40.gif

不知道为什么模拟器的监听会慢这么多。。。

到这里就基本实现了无障碍服务实现静默安装,虽说不能说完全实现静默安装,但是我们已经在权限允许的范围内尽可能地去完成了,毕竟大部分的应用市场采用的都是这种方式来实现静默安装的。

反射调用PackageManager实现静默安装

这个方式最早是在Android 无需root实现apk的静默安装看到的,确实实现方式相对来说还是比较好的,但是实际上的可用性却不强,为什么在后面说。

而且文章的实现过于复杂了,其实不需要这么复杂,拉出一大坨类。如果你有Android系统源码的话,在源码环境编译只需要在Android.mk增加一句就可以了,把framework.jar包含进去编译,自然就可以找到那一大坨类。

1
2
LOCAL_JAVA_LIBRARIES := \
framework \

然后就可以直接在代码里面引用IPackageManager那些类了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void install()
{
File file = new File(fileName);
try {
Class<?> clazz = Class.forName("android.os.ServiceManager");
Method method = clazz.getMethod("getService", String.class);
IBinder iBinder = (IBinder) method.invoke(null, "package");
IPackageManager ipm = IPackageManager.Stub.asInterface(iBinder);
@SuppressWarnings("deprecation")
VerificationParams verificationParams = new VerificationParams(null, null, null, VerificationParams.NO_UID, null);
ipm.installPackageAsUser(fileName, null, 2, null, verificationParams, "armeabi-v7a",0);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

installPackageAsUser会远程调用PackageManagerService进行安装,但安装前需要校验权限,只有System权限以上才能通过校验。所以为了编译System权限的app,需要在AndroidManifest增加一句 android:sharedUserId=”android.uid.system”

1
2
3
4
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xinyuanhxy.silentinstalldemo"
android:sharedUserId="android.uid.system"
>

系统在安装AndroidManifest包含android:sharedUserId=”android.uid.system”的app时,都会首先看下app的签名,app签名不对直接就安装失败的。这个签名在原生系统或许我们可以获取到,但是在众多的第三方ROM中,厂商是不可能提供的,所以这种方案的可行性并不高。

写在后面的话

到这里就差不多结束了关于静默安装的讲解,确实在目前非Root的情况下,实现真正意义上的静默安装是不可能也不现实的,不过这也确实是对整个系统的保护,毕竟我不想我的手机莫名其妙的出现各种乱七八糟的应用,peace~~~