Android N 静默安装和卸载

最近在做静默安装和卸载功能的时候,应用程序放到系统分区(/system/priv-app/)执行pm命令实现静默安装和卸载的方式在7.0上安装、卸载失败。Google 在N上加强了安全权限,对此用本文来记录如何解决7.0上的静默安装和静默卸载。

静默安装

复现错误

通过7.0之前的安装方法,执行静默安装sdcard下的一个应用,然后安装失败,抛出以下异常log:

1
2
3
4
5
06-27 11:01:10.483 D/MainActivity: onClick: /sdcard/com.ifeng.news2.apk
06-27 11:01:10.489 D/PackageUtils: Enter---->>>installSilent()
06-27 11:01:10.489 D/PackageUtils: installSilent—————>>>>/sdcard/com.ifeng.news2.apk
06-27 11:01:10.494 D/PackageUtils: command--->>>LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm install -r /sdcard/com.ifeng.news2.apk
06-27 11:01:11.552 E/PackageUtils: installSilent successMsg:, ErrorMsg:Error: java.lang.SecurityException: Permission Denial: runInstallCreate from pm command asks to run as user -1 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL

Error: java.lang.SecurityException: Permission Denial: runInstallCreate from pm command asks to run as user -1 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL

解决错误

参考:Android7.0的静默安装失败问题研究

得知pm安装的命令:Runtime.getRuntime().exec(“pm install -i 包名 –user 0 apkpath”)

一开始,我被这个“包名”坑了:

1
2
3
4
5
06-27 14:58:44.239 D/MainActivity: onClick: /sdcard/com.ifeng.news2.apk
06-27 14:58:44.243 D/PackageUtils: Enter---->>>installSilent()
06-27 14:58:44.243 D/PackageUtils: installSilent—————>>>>/sdcard/com.ifeng.news2.apk
06-27 14:58:44.252 D/PackageUtils: command--->>>LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm install -i com.ifeng.news2 --user 0 /sdcard/com.ifeng.news2.apk
06-27 14:58:45.458 E/PackageUtils: installSilent successMsg:, ErrorMsg:Error: java.lang.SecurityException: Package com.ifeng.news2 does not belong to 10045

Error: java.lang.SecurityException: Package com.ifeng.news2 does not belong to 10045

那么,我们来看看belong to 10045的包是什么?

Screenshot20170627150302.png

可以看到10045对应的包是我当前的这个测试应用的包名com.wt.testshortcut,而不是指定需要安装apk应用的包名com.ifeng.news2

因此,我们再改之:

1
2
3
4
5
6
06-27 16:17:58.843 D/MainActivity: onClick: /sdcard/com.ifeng.news2.apk
06-27 16:17:58.846 D/PackageUtils: Enter---->>>installSilent()
06-27 16:17:58.846 D/PackageUtils: installSilent—————>>>>/sdcard/com.ifeng.news2.apk
06-27 16:17:58.856 D/PackageUtils: command--->>>LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm install -i com.wt.testshortcut --user 0 /sdcard/com.ifeng.news2.apk
06-27 16:18:18.359 E/ApkStatusReceiver: packageName-->com.ifeng.news2
06-27 16:18:18.359 E/ApkStatusReceiver: onReceive-->android.intent.action.PACKAGE_ADDED

从上面的log可以看出,我自定义的ApkStatusReceiver监听包安装、卸载、替换的广播,收到了一个包名为com.ifeng.news2的应用被添加,也就是我刚刚我们需要静默安装上去的应用,此时手机已确认该应用被成功安装。

最终pm install的命令就是:pm install -i 作为安装者的应用包名 –user 0 需要安装的应用在移动设备上的路径

源码部分

Screenshot20170627140757.png

以上截图,可以看到第01515行的pm安装命令: pm install [-lrtsfd] [-i PACKAGE] [–user USER_ID] [PATH]

Screenshot20170627141935.png

以上截图,可以看到-i: specify the installer package name (指定安装程序包名称),而不是指定被安装程序包名称,由此也可以推断出上面的安装命令

后记

  • 不要忘记在AndroidManifest.xml添加 “android.permission.INTERACT_ACROSS_USERS_FULL” 权限
  • 如果项目不是在Android源码里面编译的,而是直接在Android Studio上面开发完成,然后push到/system/priv-app/下面的,在执行sdcard下apk的安装,是需要请求外置存储读写权限的,若没有开启,将会抛出异常:
1
PackageUtils: installSilent successMsg:, ErrorMsg:Error: failed to write; /sdcard/com.ifeng.news2.apk (Permission denied)

Error: failed to write; /sdcard/com.ifeng.news2.apk (Permission denied)

最后,我们来看看在Terminal终端执行pm install的情况:

Screenshot20170627193851.png

可以看到,之前在7.0上面执行失败的命令和成功命令都能够在Terminal终端上面执行静默安装成功。

以上就是关于7.0上静默安装的内容,接下来看看静默卸载 …..

静默卸载

复现错误

在7.0上使用pm uninstall package 命令,会抛出一下异常log:

1
2
3
4
5
06-27 20:12:02.631 D/MainActivity: onClick: com.ifeng.news2
06-27 20:12:02.634 D/PackageUtils: Enter---->>>uninstallSilent()
06-27 20:12:02.634 D/PackageUtils: command--->>>LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm uninstall -k com.ifeng.news2
06-27 20:12:03.728 D/PackageUtils: commandResult--->>>com.wt.testshortcut.ShellUtils$CommandResult@aac543c
06-27 20:12:03.743 E/PackageUtils: uninstallSilent successMsg:, ErrorMsg:Security exception: Permission Denial: runUninstall from pm command asks to run as user -1 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULLjava.lang.SecurityException: Permission Denial: runUninstall from pm command asks to run as user -1 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL at com.android.server.am.UserController.handleIncomingUser(UserController.java:1267) at com.android.server.am.ActivityManagerService.handleIncomingUser(ActivityManagerService.java:17433) at android.app.ActivityManager.handleIncomingUser(ActivityManager.java:3395) at com.android.server.pm.PackageManagerShellCommand.translateUserId(PackageManagerShellCommand.java:1157) at com.android.server.pm.PackageManagerShellCommand.runUninstall(PackageManagerShellCommand.java:771) at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:118) at android.os.ShellCommand.exec(ShellCommand.java:94) at com.android.server.pm.PackageManagerService.onShellCommand(PackageManagerService.java:18747) at android.os.Binder.shellCommand(Binder.java:468) at android.os.Binder.onTransact(Binder.java:367) at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:2387) at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:3197) at android.os.Binder.execTransact(Binder.java:565) at java.lang.reflect.Method.invoke(Native Method) at com.qihoo360.mobilesafe.loadedmgr.b.s.a(SourceFile:287) at com.qihoo360.mobilesafe.loaded.SystemServerJar$FilterInstance.filterExecTransact(SourceFile:463) at com.qihoo360.mobilesafe.loadednative.NativeManager.execTransact(SourceFile:203)

通过以上的log,分析可以看出,出现的错误和上面静默安装的错误相同,都是:

ErrorMsg:Security exception: Permission Denial: runUninstall from pm command asks to run as user -1 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL

解决错误

我们现在来看上面静默安装(#源码部分)的第一张图,看到第01522行,命令显示的是:pm uninstall [-k] [–user USER_ID] PACKAGE

确定我们的判断是没有问题的,在7.0上执行pm指令的时候,都需要跟上[–user USER_ID]

Screenshot20170627202038.png

注:其中[-k]是代表是否需要保留需要卸载应用包名的数据

因此,我们修改执行命令,同静默安装一样增加–user 0:

1
2
3
4
5
06-27 20:28:59.893 D/MainActivity: onClick: com.ifeng.news2
06-27 20:28:59.899 D/PackageUtils: Enter---->>>uninstallSilent()
06-27 20:28:59.899 D/PackageUtils: command--->>>LD_LIBRARY_PATH=/vendor/lib*:/system/lib* pm uninstall -k --user 0 com.ifeng.news2
06-27 20:29:01.006 D/PackageUtils: commandResult--->>>com.wt.testshortcut.ShellUtils$CommandResult@67c71a1
06-27 20:29:01.019 E/PackageUtils: uninstallSilent successMsg:, ErrorMsg:Exception occurred while dumping:java.lang.NullPointerException at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:94) at com.android.server.AppOpsService.checkPackage(AppOpsService.java:999) at android.app.AppOpsManager.checkPackage(AppOpsManager.java:1682) at com.android.server.pm.PackageInstallerService.uninstall(PackageInstallerService.java:879) at com.android.server.pm.PackageManagerShellCommand.runUninstall(PackageManagerShellCommand.java:792) at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:118) at android.os.ShellCommand.exec(ShellCommand.java:94) at com.android.server.pm.PackageManagerService.onShellCommand(PackageManagerService.java:18747) at android.os.Binder.shellCommand(Binder.java:468) at android.os.Binder.onTransact(Binder.java:367) at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:2387) at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:3197) at android.os.Binder.execTransact(Binder.java:565) at java.lang.reflect.Method.invoke(Native Method) at com.qihoo360.mobilesafe.loadedmgr.b.s.a(SourceFile:287) at com.qihoo360.mobilesafe.loaded.SystemServerJar$FilterInstance.filterExecTransact(SourceFile:463) at com.qihoo360.mobilesafe.loadednative.NativeManager.execTransact(SourceFile:203)

通过以上的log分析出,是在执行到Preconditions.checkNotNull()方法的时候抛出了异常的。

下面我们一起来看看这个流程,逆向倒推:

Preconditions.checkNotNull() <=== AppOpsService.checkPackage() <=== AppOpsManager.checkPackage() <=== PackageInstallerService.uninstall() <=== PackageManagerShellCommand.runUninstall() <=== PackageManagerShellCommand.onCommand()

源码分析

通过对源码的分析,定位到了PackageInstallerService.uninstall()方法

Screenshot20170627204704.png

PackageInstallerService.uninstall()里面调用AppOpsManager.checkPackage(),需要传递callerPackageName的参数,因为它是null了,导致后面CheckNotNull()方法抛出了java.lang.NullPointerException异常

Screenshot20170627205539.png

这个callerPackageName,不是我需要去卸载应用的包名,而是我需要去执行卸载操作的应用的包名。分析上面的pm uninstall命令,也并没有明确指定哪儿需要传入我当前应用的包名。

好吧,我也尝试像静默安装一样添加当前包名的参数,使用Terminal终端来执行pm uninstall命令卸载:

Screenshot20170627210607.png

结果!命令无效 …..

再来往前看PackageManagerShellCommand.runUninstall()这个方法

Screenshot20170628111723.png

这个方法是运行卸载,取出command命令里面的每一段。其中可以看到option选项只有“-k”、“–user”,“packageName”是我们需要指定卸载掉的包名。再往下看到最底部,有一个uninstall的方法,里面的第二个参数传的是“null”,它正好是我们需要传过去的callerPackageName

是不是这个地方设置了null,导致了后面为null呢?

我尝试着给这个null赋值一个固定的包名:com.wt.testshortcut

1
2
3
4
5
6
7
8
06-28 12:13:39.306 1393-1717/system_process D/PackageManagerShellCommand: =====>opt: -k
06-28 12:13:39.306 1393-1717/system_process D/PackageManagerShellCommand: =====>opt: --user
06-28 12:13:39.306 1393-1717/system_process D/PackageManagerShellCommand: =====>packageName: com.ifeng.news2
06-28 12:13:39.306 1393-1717/system_process D/PackageManagerShellCommand: =====>splitName: null
06-28 12:13:39.306 1393-1717/system_process D/PackageInstallerService: ======>packageName: com.ifeng.news2
06-28 12:13:39.306 1393-1717/system_process D/PackageInstallerService: ======>callerPackageName: com.wt.testshortcut
06-28 12:13:39.306 1393-1717/system_process D/PackageInstallerService: ======>callingUid: 10045
06-28 12:13:39.306 1393-1717/system_process D/PackageInstallerService: ======>callerPackageName: com.wt.testshortcut

此时的callerPackageName固定为com.wt.testshortcut,结果倒是没有报null的错误了,但是命令还是无效,并且应用还出现了ANR现象 …..

尝试着给这个null赋值一个固定的包名的做法是不行的

经过同事的讨论分析,只要满足PackageInstallerService.uninstall()中不走进对callingUid的判断,不去AppOpsManager.checkPackage()代码即可。也就是需要callingUid=Process.SHELL_UID(0)或者callingUid=Process.ROOT_UID (2000)

最后的解决方式:代码里在使用pm uninstall [-k] –user 0 PackageName的基础上,并在AndroidMainifest.xml下添加android:shareUerId=”android.uid.shell”,并需要在系统里进行Android源码编译,成功的完成了静默卸载。

后记

记得之前在Android M 上也是静默卸载失败,当时替换为下面的这种方式解决了卸载的问题:

1
2
3
4
5
6
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent sender = PendingIntent.getActivity(context, 0, intent, 0);
PackageInstaller mPackageInstaller = context.getPackageManager().getPackageInstaller();
mPackageInstaller.uninstall(pkgName, sender.getIntentSender());// 卸载APK
Log.d(TAG, "uninstallApk: " + pkgName);

参考:Android M静默卸载解决方案的探索

但是上面的这种方式在Android N上也没有任何效果了。

针对上面静默卸载的方式,大家有兴趣再分析看看有没有其他的解决办法。

本文为原创文章,转载请注明出处,如有不足,欢迎指正,感谢您的阅读。

if (本文对您有用) { Pay (请随意¥打赏) } else { Commit feedback (底部评论区提交建议、反馈) } 感谢支持!