一、智能安装

(一)什么是智能安装?

最近因为公司需求需要实现智能安装,apk从服务器上下载后,自动打开安装,安装完后自己打开,难了我好长时间才实现的,记录一下。
首先实现有两种方式:
静默安装: 在后台无声无息的安装apk,页面上没有任何显示,这个其实实现起来比智能安装还要简单,但是他必须要有root权限才可以 ,否则实现起来很麻烦。
智能安装: 这种也是自动安装,不需要用户操作,但是页面上会存在安装页面,不需要root权限
在这里主要讲的是智能安装

(二)AccessibilityService

AccessibilityService是Android提供的辅助服务,辅助服务的设计初衷提供给无法和界面进行交互的残疾用户。来协助帮助他们进行一些用户操作,比如点击,返回,长按,获取屏幕信息等能力。后来被开发者另辟蹊径,用于一些插件开发,做一些监听第三方应用的插件。
生命周期:
辅助服务的生命周期由系统专门管理,并遵循Server的生命周期。服务的启动只能用户在设备设置中明确启动服务来触发。当系统绑定到服务后,它会调用AccessibilityService#onServiceConnected()方法。当用户在设置设置中关闭时,辅助服务功能将停止,或者调用AccessibilityService#disableSelf()方法。giant服务会被关闭销毁
有兴趣可以去官方文档去深入学习

在使用的时候必须要打开对应的无障碍权限,如下图,配置对应文件,才会显示这个app对应的辅助权限,上面的文字信息可根据需求去修改

android app安装后自启动 安卓开机自动安装app_ide

(三)实现

接下来就是如何实现了:

(1)首先在res/xml目录下新建一个accessibility_service_config.xml文件

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:packageNames="com.android.packageinstaller"
    android:description="智能安装服务,无需用户的任何操作就可以自动安装程序"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault"
    android:notificationTimeout="100"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:canRetrieveWindowContent="true"
    />

packageNames: 指的是我们要监听哪个包下的窗口活动,这里指定的时安装页面
description: 描述这个功能,这个文本对应点进上面某个服务页面上显示
accessibilityEventTypes: 指定我们在监听窗口中可以模拟哪些事件,这里写typeAllMask表示所有的事件都能模拟
accessibilityFlags: 可以指定无障碍服务的一些附加参数,这里我们传默认值flagDefault就行
notificationTimeout: 接受事件的时间间隔,通常将其设置为100即可
accessibilityFeedbackType: 指定服务的反馈类型,包括语音反馈,震动反馈,视频反馈等,这里选的是通用反馈
canRetrieveWindowContent: 指定是否允许我们的程序读取窗口中的节点和内容,必须写true。

(2) 安装方法
在这里我只粘贴了打开apk的方法,因为需求的原因,没有粘贴上下文,可在需要的位置使用
mSavePath: 是新apk的保存地址
apkName 文件名字

private void installApk(){
		File apkfile = new File(mSavePath, apkName);
		Log.i("info","apk保存地址:"+apkfile.getAbsolutePath());
		if (!apkfile.exists()){
			return;
		}
		Uri apkUri;// = FileProvider.getUriForFile(mContext, "com.zjs.znstorepack.fileprovider", apkfile);
		Intent i = new Intent(Intent.ACTION_VIEW);
		// 通过Intent安装APK文件
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
			apkUri = FileProvider.getUriForFile(mContext,
					"com.installdemo.test.fileprovider",
					apkfile);
		} else {
			apkUri = Uri.fromFile(apkfile);
		}
		//打开新版本应用的
		i.setDataAndType(apkUri, "application/vnd.android.package-archive");
		mContext.startActivity(i);
		//安装完成 进入完成页面 提示完成、打开,如果需要模拟点击打开,可以加上,如果在模拟点击打开无效,可注掉,通过广播的方式去实现安装后启动
		//i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
		//彻底杀死进程,通过广播打开app时,必须注释掉
		//android.os.Process.killProcess(android.os.Process.myPid());
	}

(3)创建一个MyAccessibilityService类并继承自AccessibilityService

public class MyAccessibilityService extends AccessibilityService {
    Map<Integer, Boolean> handledMap = new HashMap<>();

    public MyAccessibilityService() {
    }

    @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 (handledMap.get(event.getWindowId()) == null) {
                    boolean handled = iterateNodesAndHandle(nodeInfo);
                    if (handled) {
                        handledMap.put(event.getWindowId(), true);
                    }
                }
            }
        }else{
            LogToFile.i("info","此时节点为null");
        }
    }

    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)) {
                    //模拟点击
                    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() {
    }

}

每当窗口有活动时,就会有消息回调到onAccessibilityEvent()方法中,因此所有的逻辑都是从这里开始的。首先我们可以通过传入的AccessibilityEvent参数来获取当前事件的类型,在这里需要监听TYPE_WINDOW_CONTENT_CHANGED和TYPE_WINDOW_STATE_CHANGED这两种事件就可以了,对应窗口文本变化和窗口状态变化,因为在整个安装过程中,这两个事件必定有一个会被触发。当然也有两个同时都被触发的可能,那么为了防止二次处理的情况,这里我们使用了一个Map来过滤掉重复事件。

调用的方法也很好理解,就是进入后,遍历所有子节点,递归调用,如果是按钮,判断上面的文本是不是 “安装” ,如果是模拟点击一下,如果ScrollView 就模拟滑动一下,继续操作

(4) AndroidManifest.xml,在application标签内加上下面的代码

<service
            android:name=".service.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.permission.BIND_ACCESSIBILITY_SERVICE的权限, 且必须要有一个值为android.accessibilityservice.AccessibilityService的action,然后我们通过将刚才创建的配置文件指定进去

到此配置完成,打开手机辅助服务后,即可实现

二、安装后自启动

上述操作完成后,一般情况下,可以实现智能安装,打开指定安装包,自动点击安装按钮,点击完后自动点击打开按钮(做了判断的情况下),当然也有可能会遇到和我一样的问题,就是安装完成后,不会自动点打开(就很迷,必须设备接鼠标后移动一下才会点击,没找到原因),这样的话,可以通过广播去实现,因为刚接触安卓不久,对于广播还不是很了解,在这里直接上代码,

(1)自定义receiver,继承BroadcastReceiver

public class MyReceiver extends BroadcastReceiver {
 
	@Override
	public void onReceive(Context context, Intent intent) {
		String action = intent.getAction();
		String localPkgName = context.getPackageName();//取得MyReceiver所在的App的包名
		Uri data = intent.getData();
		String installedPkgName = data.getSchemeSpecificPart();//取得安装的Apk的包名,只在该app覆盖安装后自启动
		if((action.equals(Intent.ACTION_PACKAGE_ADDED)
				|| action.equals(Intent.ACTION_PACKAGE_REPLACED)) && installedPkgName.equals(localPkgName)){
			Intent launchIntent = new Intent(context, MainActivity.class);//默认打开页面
			launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
			context.startActivity(launchIntent);
		}
	}
}

(2)AndroidManifest.xml,在application标签内加上下面的代码

<receiver android:name=".receiver.MyReceiver">
            <intent-filter>
                <action android:name="android.intent.action.PACKAGE_ADDED" />
                <action android:name="android.intent.action.PACKAGE_REPLACED" />
                <action android:name="android.intent.action.PACKAGE_REMOVED" />
                <data android:scheme="package"/>
            </intent-filter>
        </receiver>

注意: 通过广播启动的时候,安装方法中的这两行一定要注释掉,要不然会出现其他bug
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
android.os.Process.killProcess(android.os.Process.myPid());

到此应该是可以实现智能安装且自启动了,有什么问题或是错误,及时评论指出