Android实现静默安装和智能安装

静默安装,就是指在程序安装时,用户并不会感知到安装的过程,自己就安装完成了。一些系统自带应用市场会具有静默安装的功能,比如小米的应用市场。在一些非系统自带的应用市场,要想完成静默安装,就必须具有root权限。可见权限的重要性,在系统的支持下,你可以做到很多别人做不到的事情。当然,像360手机卫士,应用宝,豌豆荚之类的非系统支持的应用市场,大多使用了智能安装,仍然会弹出系统安装弹窗,但是会迅速自动点击安装按钮。对大多数用户来说,还是未root用户比较多,因为这种方式也是很常见的。

一.静默安装

静默安卓有两个前提条件,一个是手机必须具有root权限,一个是系统是4.2及以上。看一下静默安装的代码:

 /**
 * 静默安装
 * @param apkPath apk文件路径
 */
public static boolean install(String apkPath) {
    boolean result = false;
    DataOutputStream dataOutputStream = null;
    BufferedReader errorStream = null;
    try {
        Process process = Runtime.getRuntime().exec("su");//申请root权限
        dataOutputStream = new DataOutputStream(process.getOutputStream());

        String command = "pm install -r " + apkPath + "\n";//拼接 pm install 命令,执行。-r表示若存在则覆盖安装
        dataOutputStream.write(command.getBytes(Charset.forName("utf-8")));
        dataOutputStream.flush();
        dataOutputStream.writeBytes("exit\n");
        dataOutputStream.flush();
        process.waitFor();//安装过程是同步的,安装完成后再读取结果
        errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream()));
        String message = "";
        String line;
        while ((line = errorStream.readLine()) != null) {
            message += line;
        }
        Log.e("silentInstall", message);
        if (!message.contains("Failure")) {
            result = true;
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (dataOutputStream != null) {
                dataOutputStream.close();
            }
            if (errorStream != null) {
                errorStream.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    return result;
}

代码不多,核心就是调用了这条命令 pm install -r <apkPath>,显然和我们在adb中安装apk一样。-r代表若目标apk存在则覆盖安装。首先通过Runtime.getRuntime().exec("su")申请root权限,否则是没有办法成功执行pm命令的。执行完成后通过读取安装结果,判断是否安装成功。我们也可以先判断一下当前是否具有root权限,再去决定是否静默安装。

/**
 * 判断手机是否拥有Root权限。
 * @return 有root权限返回true,否则返回false。
 */
public static boolean isRoot() {
    boolean bool = false;
    try {
        bool = new File("/system/bin/su").exists() || new File("/system/xbin/su").exists();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return bool;
}

二.智能安装

智能安装利用辅助功能AccessibilityService来实现对安装界面的自动点击。什么是AccessibilityService呢?Accessibility services should only be used to assist users with disabilities in using Android devices and apps. They run in the background and receive callbacks by the system when AccessibilityEvents are fired.它是被设计出来帮助一些无法正常使用安卓设备和app的残疾人的,运行在后台,当系统发生一些AccessibilityEvent时会产生回调。通过这些回调,我们可以通过代码做一些事情。这里我们要做的就是,当系统安装弹窗出现时,通过AccessibilityService自动点击安装或者确定按钮。

首先是做一个配置,在res/xml目录下新建accessibility_service_config.xml文件,内容如下:

<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"/>
  • packageNames,Comma separated package names from which this serivce would like to receive events (leave out for all packages).指定我们的Service监听哪个包名下的事件。这里的com.android.packageinstaller就是指安卓的安装界面。
  • description,当在辅助功能界面点击你自己的app后,会显示这些文字。你可以描述你开启辅助功能的目的,为自己狡辩一下。
  • accessibilityEventTypes,The event types this serivce would like to receive as specified in AccessibilityEvent.我们可以在监听窗口中模拟哪些事件。typeAllMask代表所有。
  • accessibilityFlags,Additional flags as specified in AccessibilityServiceInfo.一些附加参数,默认即可。
  • accessibilityFeedbackType,无障碍服务的反馈方式,比如针对残疾人可以语音反馈,这里并不需要。
  • canRetrieveWindowContent,Attribute whether the accessibility service wants to be able to retrieve the active window content. 我们是否可以检索窗口中的内容,当然应该是可以的。

然后我们需要继承AccessibilityService,重写相关方法实现智能安装的具体逻辑。如下所示:

/**
 * Created by Lu
 * on 2017/1/17 17:54.
 */

public class MyAccessibilityService extends AccessibilityService {

Map<Integer, Boolean> handleMap = new HashMap<>();

/**
 * 当窗口有活动时,会回调此方法
 * @param event
 */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    AccessibilityNodeInfo nodeInfo = event.getSource();
    if (nodeInfo != null) {
        int eventType = event.getEventType();
        if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
            if (handleMap.get(event.getWindowId()) == null) {
                boolean handled = iterateNodesAndHandle(nodeInfo);
                if (handled) {
                    handleMap.put(event.getWindowId(), true);
                }
            }
        }
    }
}

/**
 * 递归处理节点信息
 * 节点名称为Button,内容为 安装,确定,完成的,模拟点击
 * 节点名称是ScrollView,模拟滑动到底部
 */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
    if (nodeInfo != null) {
        int childCount = nodeInfo.getChildCount();
        if ("android.widget.Button".equals(nodeInfo.getClassName())) {
            String nodeContent = nodeInfo.getText().toString();
            if ("安装".equals(nodeContent) || "确定".equals(nodeContent) || "完成".equals(nodeContent)) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                return true;
            }
        } 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;
}

@Override
public void onInterrupt() {

}
}

每次当有新的AccessibilityEvent时,就会回调onAccessibilityEvent方法。这里我们处理了两种事件类型,AccessibilityEvent.TYPE_WINDOW_STATE_CHANGEDAccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,分别代表窗体状态的变化和窗体内容的变化,符合条件的节点再对节点内容进行分析:

  • 当节点名称是Button时,且节点内容是安装 || 确定 || 完成时,模拟点击事件AccessibilityNodeInfo.ACTION_CLICK
  • 当节点名称是ScrollView时,这时候是在显示权限列表,一些系统会要求显示完全部权限,这时候模拟上滑。

既然是一个Service,就要去注册它。这里大多是固定写法。

<service android:name=".MyAccessibilityService"
        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>

在使用时,先提醒用户开启相应辅助功能,如下代码,跳转到辅助功能设置界面:

 Intent intent=new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
 startActivity(intent);

然后安装apk,

Uri uri=Uri.fromFile(new File(path));
Intent intent=new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri,"application/vnd.android.package-archive");
startActivity(intent);

前提是用户确实开启了辅助功能,这时候才会自动智能安装。

不管是哪种方式,缺陷都是很大的。静默安装需要足够的权限,智能安装需要用户去开启辅助功能,很多用户应该都不会去操作的。或许也正想看看你申请了哪些该死的权限。面对安卓杂乱的生态环境,希望在某些小小的方面可以达成一致性,就像最近Google提出要强制统一通知中心。如果安卓能有一个统一的通知机制,当然是指在国内。就会少了多少服务相互唤醒,就不会有那么多厂商去标榜自己的通知到达率。

源码下载

Android静默安装实现方案,仿360手机助手秒装和智能安装功能 ——————郭霖
https://developer.android.com/guide/topics/ui/accessibility/services.html

有任何疑问,欢迎加群讨论:261386924

    原文作者:秉心说
    原文地址: https://www.jianshu.com/p/a46c318ba8b2
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞