乍听起来,静默安装是很是流氓的一件事,它让用户不知觉的状况下被「收割」。可是技术自己是中立的,咱们只谈谈实现静默安装这件事儿。java
下面我将介绍三种静默安装的方案,每种方案各有利弊,可是目的是一致的。node
众所周知,手机被 Root 后能够作好多奇妙的事情,好比静默安装,直接调用 pm install 命令就能够实现,来看代码:android
public static boolean silentInstall(String apkPath) {
boolean result = false;
DataOutputStream dataOutputStream = null;
BufferedReader errorStream = null;
BufferedReader successStream = null;
Process process = null;
try {
// 申请 su 权限
process = Runtime.getRuntime().exec("su");
dataOutputStream = new DataOutputStream(process.getOutputStream());
// 执行 pm install 命令
String command = "pm install -r " + apkPath + "\n";
dataOutputStream.write(command.getBytes(Charset.forName("UTF-8")));
dataOutputStream.writeBytes("exit\n");
dataOutputStream.flush();
process.waitFor();
errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream()));
StringBuilder errorMsg = new StringBuilder();
String line;
while ((line = errorStream.readLine()) != null) {
errorMsg.append(line);
}
log.debug("silent install error message:{}", errorMsg);
StringBuilder successMsg = new StringBuilder();
successStream = new BufferedReader(new InputStreamReader(process.getInputStream()));
// 读取命令执行结果
while ((line = successStream.readLine()) != null) {
successMsg.append(line);
}
log.debug("silent install success message:{}", successMsg);
// 若是执行结果中包含 Failure 字样就认为是操做失败,不然就认为安装成功
if (!(errorMsg.toString().contains("Failure") || successMsg.toString().contains("Failure"))) {
result = true;
}
} catch (Exception e) {
log.error(e);
} finally {
try {
if (process != null) {
process.destroy();
}
if (dataOutputStream != null) {
dataOutputStream.close();
}
if (errorStream != null) {
errorStream.close();
}
if (successStream != null) {
successStream.close();
}
} catch (Exception e) {
// ignored
}
}
return result;
}
public static boolean isRoot() {
return new File("/system/bin/su").exists() || new File("/system/xbin/su").exists();
}
复制代码
首先申请 Root 权限,而后执行 pm install- r <apk 路径>
命令,-r 参数表示容许覆盖安装。 process.waitFor() 说明安装过程是同步的,等待命令执行完成,而后读取执行结果。注意:不要在主线程调用静默安装的代码,安装过程会比较耗时,致使线程一直等待结果。面试
结论:只要手机被 Root,该方法十分奏效。可是绝大部分用户不懂 Root,即便手机被 Root了,还须要用户受权,因此该方案局限性很是大。浏览器
当咱们选择手动安装应用时,会跳转到应用安装界面,这个界面就是系统的 PackageInstaller 提供,专门用来让用户有感知地安装应用。性能优化
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.fromFile(new File("/sdcard/news.apk")));
intent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(intent);
复制代码
分析 PackageInstaller 的源码,咱们发现它会经过 PackageManager 调用 installPackage 方法,这是个隐藏的抽象方法,实现类是 ApplicationPackageManager。主要看一下四个参数:packageURI 就是 apk 的路径;observer 是安装的监听器,应用安装完成时会被回调,不能为 null;flags 是标志位,指定安装的参数;installersPackageName 表示可选的安装来源,好比应用宝之类的。bash
public abstract void installPackage(
Uri packageURI, PackageInstallObserver observer,
int flags, String installerPackageName);
复制代码
ApplicationPackageManager 里面 mPM 是一个 IPackageManager 类型的对象,它会执行具体的安装任务。架构
try {
mPM.installPackage(originPath, observer.getBinder(), flags, installerPackageName,
verificationParams, null);
} catch (RemoteException ignored) {
}
复制代码
ContextImpl 的 getPackageManager 方法,经过 ActivityThread 获取 IPackageManager 对象用来构造 ApplicationPackageManager,而后返回 ApplicationPackageManager。app
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm)); } return null; } 复制代码
ActivityThread 的 getPackageManager 方法,其实就是获取系统服务的过程。ide
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
//Slog.v("PackageManager", "returning cur default = " + sPackageManager);
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
//Slog.v("PackageManager", "default service binder = " + b);
sPackageManager = IPackageManager.Stub.asInterface(b);
//Slog.v("PackageManager", "default service = " + sPackageManager);
return sPackageManager;
}
复制代码
经过以上分析,咱们经过 PackageManager 调用 installPackage 方法就好了,下面看代码:
public static boolean silentInstall(PackageManager packageManager, String apkPath) {
Class<?> pmClz = packageManager.getClass();
try {
if (Build.VERSION.SDK_INT >= 21) {
Class<?> aClass = Class.forName("android.app.PackageInstallObserver");
Constructor<?> constructor = aClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object installObserver = constructor.newInstance();
Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, aClass, int.class, String.class);
method.setAccessible(true);
method.invoke(packageManager, Uri.fromFile(new File(apkPath)), installObserver, 2, null);
} else {
Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, Class.forName("android.content.pm.IPackageInstallObserver"), int.class, String.class);
method.setAccessible(true);
method.invoke(packageManager, Uri.fromFile(new File(apkPath)), null, 2, null);
}
return true;
} catch (Exception e) {
log.error(e);
}
return false;
}
复制代码
因为 PackageManager 在不一样版本上的 installPackage 方法参数不一致,因此咱们根据编译版本作了处理。在 API 21 及以上,须要传递一个非 null 的 PackageInstallObserver,这个类是不可见 的,咱们就用反射建立一个 observer 对象,flags 指定 INSTALL_REPLACE_EXISTING
,用常量表示就是 2。在 API 21 如下,observer 类型是IPackageInstallObserver,一样使用反射处理便可。
最后声明权限 <uses-permission android:name="android.permission.INSTALL_PACKAGES"/>
,还要使用系统签名,这个很是关键,要否则就会出现异常: java.lang.SecurityException: Neither user 10052 nor current process has android.permission.INSTALL_PACKAGES.
。
结论:经过调用系统 API 静默安装,终于能够堂堂正正地搞事情了!虽然这是官方提供的接口,可是为了避免让你随心所欲,强制使用系统签名,因此对于第三方应用采用的可能性是零。
如今大多数应用采起的是这种办法,让用户主动打开辅助功能,而后模拟点击用户操做,进行自动安装。核心就是 AccessibilityService,咱们来实现这一功能。
<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/accessibility_service_description"
android:packageNames="com.android.packageinstaller"
/>
复制代码
accessibilityEventTypes: 指定咱们在监听窗口中能够模拟哪些事件,typeAllMask 表示全部的事件都能模拟。 accessibilityFeedbackType: 指定无障碍服务的反馈方式。 canRetrieveWindowContent: 指定是否容许咱们的程序读取窗口中的节点和内容,固然是 true。 description: 当用户手动配置服务时,显示给用户看的说明信息。 packageNames: 指定要监听哪一个应用程序下的窗口活动,这里写 com.android.packageinstaller 表示监听 Android 系统的安装界面。 配置里面描述的内容
<resources>
<string name="app_name">InstallTest</string>
<string name="accessibility_service_description">智能安装服务,无需用户的任何操做就能够自动安装程序。</string>
</resources>
复制代码
public class MyAccessibilityService extends AccessibilityService {
private static final String TAG = "[TAG]";
private Map<Integer, Boolean> handleMap = new HashMap<>();
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
AccessibilityNodeInfo nodeInfo = event.getSource();
if (nodeInfo != null) {
int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
if (handleMap.get(event.getWindowId()) == null) {
boolean handled = iterateNodesAndHandle(nodeInfo);
if (handled) {
handleMap.put(event.getWindowId(), true);
}
}
}
}
}
@Override
public void onInterrupt() {
}
// 遍历节点,模拟点击安装按钮
private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
if (nodeInfo != null) {
int childCount = nodeInfo.getChildCount();
if ("android.widget.Button".equals(nodeInfo.getClassName())) {
String nodeCotent = nodeInfo.getText().toString();
Log.d(TAG, "content is: " + nodeCotent);
if ("安装".equals(nodeCotent) || "完成".equals(nodeCotent) || "肯定".equals(nodeCotent)) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
return true;
}
}
// 遇到 ScrollView 的时候模拟滑动一下
else if ("android.widget.ScrollView".equals(nodeInfo.getClassName())) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
}
for (int i = 0; i < childCount; i++) {
AccessibilityNodeInfo childNodeInfo = nodeInfo.getChild(i);
if (iterateNodesAndHandle(childNodeInfo)) {
return true;
}
}
}
return false;
}
}
复制代码
当进入安装界面就会回调 onAccessibilityEvent() 这个方法,咱们只处理 TYPE_WINDOW_CONTENT_CHANGED 和 TYPE_WINDOW_STATE_CHANGED 两个事件。为了防止重复处理事件,用 map 来过滤事件,而后递归遍历节点,找到「安装」、「完成」、「缺点」的按钮就模拟点击。因为安装界面须要用户看完权限才出现按钮,因此遇到 ScrollView 的时候就模拟滚动,直到出现安装按钮。
<service
android:name=".MyAccessibilityService"
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/accessibility_service_config"
/>
</service>
复制代码
android:label:这个就是用户看到的无障碍服务的名称。 android:permission: 须要用到 BIND_ACCESSIBILITY_SERVICE 这个权限。 action: android.accessibilityservice.AccessibilityService 有了这个 action,用户才能在设置里面看到咱们的服务,不然用户没法开启咱们的 AccessibilityService,也就不能进到 MyAccessibilityService 里面了。
private void smartInstall() {
Uri uri = Uri.fromFile(new File("/sdcard/test.apk"));
Intent localIntent = new Intent(Intent.ACTION_VIEW);
localIntent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(localIntent);
}
复制代码
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);
复制代码
智能安装是一种妥协的方案,在没有 Root 和安装权限的状况下,确实解放了用户的拇指。看看市面上的应用,大部分都采用了这种方法。应用市场使用智能安装能够理解,视频浏览器工具一类不相干的应用也要开启?我真是呵呵了。
在如今这个金三银四的面试季,我本身在网上也搜集了不少资料作成了文档和架构视频资料免费分享给你们【包括高级UI、性能优化、架构师课程、NDK、Kotlin、混合式开发(ReactNative+Weex)、Flutter等架构技术资料】,但愿能帮助到您面试前的复习且找到一个好的工做,也节省你们在网上搜索资料的时间来学习。