使用AccessibilityService实现APP的自动安装与卸载

使用AccessibilityService实现APP的自动安装与卸载

 

  作者:

  蒋东国

  时间:

  2016年12月6日 星期二

  应用来源:

  hqt APP(测试机型:三星 Note4)          

  博客地址:

 http://blog.csdn.net/andrexpert/article/details/53494776

      

 情景再现“平时在使用豌豆荚或者360手机助手下载软件时,我们可以设置软件静默安装和智能安装,这两种情况允许用户无需操作任何界面就能够实现软件的一键下载安装,用户体验大大提高。我们知道静默安装主要是针对于已经Root的手机,只需执行相关的shell命令即可实现,那么,智能安装又是怎么一回事呢?”

      

 从Google官方文档来看,AccessibilityService主要用于开发者为其应用开发一些增强用户体验的功能(辅助功能),比如语音合成、手势导航,甚至可以获得当前活动窗口的内容并模拟用户进行文本输入、Button点击操作等等,可以说功能非常强(从安全的角度来说,一般不建议开启某些应用的AccessibilityService)。从Accessibility-Service.java源码可知,AccessibilityService继承于Service并且采用bind方式启动,因此AccessibilityService的生命周期遵循Service的生命周期,不同的是它仅由系统管理,AccessibilityService的开启、关闭必须由用户自己到”设置(settings)”界面开启。除了应用进程被非正常终止或者被卸载,AccessibilityService被开启后会一直保持着开启的状态。AccessibilityService.java部分源码:

public abstract class AccessibilityService extendsService {
public abstract voidonAccessibilityEvent(AccessibilityEvent event);
 …..
@Override
public final IBinder onBind(Intent intent) {
     returnnew IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() {
    @Override
     publicvoid onServiceConnected() {
         AccessibilityService.this.onServiceConnected();
      }
  @Override
      publicvoid onAccessibilityEvent(AccessibilityEvent event) {
          AccessibilityService.this.onAccessibilityEvent(event);
      }
          ……
     });
  }
}

1.建立AccessibilityService

 实现一个AccessibilityService子类,并重写如下方法:onServiceConnected()(当系统成功启动服务时调用,可用于对服务进行配置或弹出提示信息)、onAccessibilityEvent(AccessibilityEvent event)(当指定事件出发该服务时调用,用于实现事件处理的业务逻辑模块)、onInterrupt()(终止accessibility service时调用)。

      

 @Override
       protected voidonServiceConnected() {
              super.onServiceConnected();
              //系统启动服务成功,弹出提示信息
              Toast.makeText(UnAccessibilityService.this,"连接服务成功",
                                          Toast.LENGTH_SHORT).show();
       }
 
       @Override
       public voidonAccessibilityEvent(AccessibilityEvent event) {
              Log.d("sms","onAccessibilityEvent被调用");
              //卸载事件捕获,调用应用卸载方法
              uninstallApplication(event,”确定”);
       }

  那么问题来了,当用户点击一个Button实现卸载功能时,AccessibilityService是如何捕获点击事件并执行卸载任务的?这就借助于另外两个类:AccessibilityNodeInfo、AccessibilityEvent。AccessibilityEvent封装了所有用户触发的事件,这些事件将由系统发送给AccessibilityService;然后,AccessibilityService再根据/res/xml/accessibility_service_config.xml中的android:accessibilityEventTypes 属性确定是否对该事件进行捕获,由于这里将该属性值设置为” typeAllMask”,我们定义的AccessibilityService将对用户操作的所有事件进行捕获。而AccessibilityNodeInfo则封装了当前状态视图的属性,即当前状态视图组件以树的形式构建。

  接下来,我们将借助Accessibility Service模拟用户完成点击操作,实现应用的卸载。由于系统卸载应用调用的是包名为” com.android.packageinstaller”的应用,那么首先我们在进行模拟操作时先判断当前的视图是否为系统卸载应用界面:

    boolean isInstallPage =event.getPackageName().equals("com.android.packageinstaller");

  然后通过AccessibilityEvent的getSource()方法获得event(即卸载,Intent.ACTION_DELETE)的所有资源,这些资源封装在AccessibilityNodeInfo对象中;

AccessibilityNodeInfo eventSource= event.getSource();

  最后调用AccessibilityNodeInfo的findAccessibilityNodeInfosByText(String text)方法,根据文本遍历当前视图树是否包含text文字属性。当发现包含text内容视图节点时,判断该节点是否为Button且是否为enabled,最终调用AccessibilityNodeInfo的performAction实现模拟用户点击Button操作(即文本内容为text的Button)。

List<AccessibilityNodeInfo> action_nodes =eventSource.findAccessibilityNodeInfosByText(text);
       if(action_nodes !=null&& !action_nodes.isEmpty()){
              AccessibilityNodeInfonode = null;
              for (int i = 0; i <action_nodes.size(); i++) {
                     node =action_nodes.get(i);
                     // 执行按钮点击行为
                     if(node.getClassName().equals("android.widget.Button")&&node.isEnabled()) {
                                   node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                     }
              }
}

2.Demo源码剖析

(1)效果演示(以“路痴宝APP”为例,详见应用宝、豌豆荚市场)



(2)MyAccessibilityService.java:继承于AccessibilityService,APP智能安装、卸载核心服务类

/**
 * @decrible *APP智能安装、卸载辅助Service
 * 
 * Create by jiangdongguo on 2016-12-6 下午4:45:19
 */
public class MyAccessibilityService extends AccessibilityService {
	public static int INVOKE_TYPE = 0; 				// Event默认标志
	public static final int TYPE_INSTALL_APP = 1; 	// 安装应用事件标
	public static final int TYPE_UNINSTALL_APP = 2; // 卸载应用事件标志


	public static void reset() {
		INVOKE_TYPE = 0;
	}


	/**
	 * 连接服务成功后回调该方法
	 */
	@Override
	protected void onServiceConnected() {
		super.onServiceConnected();
		Toast.makeText(MyAccessibilityService.this, "连接服务成功",
				Toast.LENGTH_SHORT).show();
	}


	@Override
	public void onAccessibilityEvent(AccessibilityEvent event) {
		Log.d("sms", "onAccessibilityEvent被调用");
		this.processAccessibilityEnvent(event);
	}


	/**
	 * 功能 :事件处理方法
	 * 
	 * @param event  事件类型
	 */
	private void processAccessibilityEnvent(AccessibilityEvent event) {


		if (event.getSource() != null) {
			switch (INVOKE_TYPE) {
			case TYPE_UNINSTALL_APP:
				uninstallApplication(event); // 静默卸载
				break;
			case TYPE_INSTALL_APP:
				installApplication(event); // 静默安装
			default:
				break;
			}
		}
	}


	/**
	 * 功能:实现静默卸载
	 * 
	 * @param event
	 *            卸载事件
	 */
	private void uninstallApplication(AccessibilityEvent event) {
		findAndPerformActions(event, "确定");
		findAndPerformActions(event, "确认");
		findAndPerformActions(event, "卸载");
	}


	/**
	 * 功能 :实现静默安装
	 * 
	 * @param event
	 *            事件,如ACTION_INSTALL
	 */
	private void installApplication(AccessibilityEvent event) {
		findAndPerformActions(event, "安装");
		findAndPerformActions(event, "下一步");
		findAndPerformActions(event, "完成");
	}


	/**
	 * 功能:模拟用户点击操作
	 * 
	 * @param text
	 */
	private void findAndPerformActions(AccessibilityEvent event, String text) {
		if (event.getSource() != null) {
			Log.d("debug", "source不为空");
			// 判断当前界面为安装界面
			boolean isInstallPage = event.getPackageName().equals(
					"com.android.packageinstaller");
			if (isInstallPage) {
				Log.d("debug", "isInstallPage不为false");
				List<AccessibilityNodeInfo> action_nodes = event.getSource()
						.findAccessibilityNodeInfosByText(text);
				if (action_nodes != null && !action_nodes.isEmpty()) {
					AccessibilityNodeInfo node = null;
					for (int i = 0; i < action_nodes.size(); i++) {
						node = action_nodes.get(i);
						// 执行按钮点击行为
						if (node.getClassName().equals("android.widget.Button")
								&& node.isEnabled()) {
							Log.d("debug", "就是Button");
							node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
						}
					}
				}
			}
		}
	}


	@Override
	public void onInterrupt() {


	}
}
(3)MyAccessibilityUtils.java:为了操作方便,该类封装了检测本应用申请的AccessibilityService服务是否开启、弹出全局提示对话框、自动安装APP、自动卸载APP等方法,代码如下:

/**
 * 辅助服务检测、开启,应用自动卸载、安装
 * 
 * @time 2016-10-19 上午10:04:36
 * @author Jiangdg
 */
public class MyAccessibilityUtils {
	private Context mContext;
	private String locPackageName;
	private String accessibilityClassAbsolutePath;
	
	/**构造方法
	 * @param mContext 上下文,推荐getApplicationContext()
	 * @param packageName 本应用包名
	 * @param accessibilityClassAbsolutePath 辅助服务类绝对路径
	 */
	public MyAccessibilityUtils(Context mContext, String locPackageName,
			String accessibilityClassAbsolutePath) {
		this.mContext = mContext;
		this.locPackageName = locPackageName;
		this.accessibilityClassAbsolutePath = accessibilityClassAbsolutePath;
	}
	
	public  boolean isAccessibilitySettingsOn() {
		boolean isAccessibilityOn = false;
		try {
			int accessibilityEnabled = Settings.Secure.getInt(mContext.getContentResolver(),Settings.Secure.ACCESSIBILITY_ENABLED);
			TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
			if (accessibilityEnabled == 1) {
				String settingValue = Settings.Secure.getString(mContext.getContentResolver(),Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
				if (settingValue != null) {
					TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
					splitter.setString(settingValue);
					while (splitter.hasNext()) {
						String accessabilityService = splitter.next();
	//辅助服务关键路径,格式如"com.corget/com.corget.service.UnAccessibilityService"
						String service = locPackageName+"/"+accessibilityClassAbsolutePath;
						if (accessabilityService.equalsIgnoreCase(service)) {
							isAccessibilityOn = true;
						}
					}
				}
			} 
		} catch (SettingNotFoundException e) {
			e.printStackTrace();
		}
		return isAccessibilityOn;
	}
	
	public void uninstallApp(String uninstallPackage) {
		if (!isAccessibilitySettingsOn()) {
			Toast.makeText(mContext, "辅助服务未开启,卸载应用失败", Toast.LENGTH_SHORT).show();
			return;
		}
		//如果Apk已经安装,则可以执行卸载逻辑
		if (isApkInstalled(uninstallPackage)) {
			MyAccessibilityService.reset();
			MyAccessibilityService.INVOKE_TYPE = MyAccessibilityService.TYPE_UNINSTALL_APP;
			Uri packageURI = Uri.parse("package:" + uninstallPackage);
			if (packageURI != null) {
				Intent uninstallIntent = new Intent(Intent.ACTION_DELETE,packageURI);
				uninstallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
				mContext.startActivity(uninstallIntent);
			}
		}else{
			Toast.makeText(mContext, "应用不存在,卸载失败", Toast.LENGTH_SHORT).show();
		}
	}


	public void installApp(String filePath,String packageName) {
		if (!isAccessibilitySettingsOn()) {
			Toast.makeText(mContext, "辅助服务未开启,安装应用失败", 
Toast.LENGTH_SHORT).show();
			return;
		}
		//如果Apk没有安装,则执行安装逻辑
		if(!isApkInstalled(packageName)){
MyAccessibilityService.INVOKE_TYPE = 
MyAccessibilityService.TYPE_INSTALL_APP;
			Uri packageURI = Uri.fromFile(new File(filePath));
			if (packageURI != null) {
				Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
				installIntent.setData(packageURI);
				installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
				mContext.startActivity(installIntent);
			}
		}
	}
	
	private boolean isApkInstalled(String packageName){
		PackageManager mPackageManager = mContext.getPackageManager();
		//获得所有已经安装的包信息
		List<PackageInfo> infos = mPackageManager.getInstalledPackages(0);
		for(int i=0;i<infos.size();i++){
			if(infos.get(i).packageName.equalsIgnoreCase(packageName)){
				return true;
			}
		}
		return false;
	}
	
	public  void popOpenAlertDialog(final String alertBroadcastAction) {
		Builder mDialogBuilder = new AlertDialog.Builder(mContext);
		AlertDialog dialog = null;
		mDialogBuilder.setIcon(R.drawable.ic_launcher);
		mDialogBuilder.setTitle("警告");
		mDialogBuilder.setMessage("检测到辅助服务没有开启,马上前往设置?");
		mDialogBuilder.setCancelable(false);
		mDialogBuilder.setPositiveButton("前往",
				new DialogInterface.OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						dialog.dismiss();
						// 前往辅助服务设置页面
						Intent startIntent = new Intent(
								Settings.ACTION_ACCESSIBILITY_SETTINGS);
						mContext.startActivity(startIntent);
						// 发送一个广播,以便弹出"开启服务"提示对话框
						mContext.sendBroadcast(new Intent()
								.setAction(alertBroadcastAction));
					}
				});
		mDialogBuilder.setNegativeButton("不了",
				new DialogInterface.OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
							dialog.dismiss();
					}
				});
		dialog = mDialogBuilder.create();
		dialog.show();
	}
}
(5)MainActivity.java:为了方便,这里我将需要安装的路痴宝apk存放到assets目录下,又由于assets目录下的所有文件都是以二进制的形式存在的,因此需要将apk文件复制到本地sd目录才能够进行安装操作。

/**
 *@decrible  路痴宝APP的自动安装与卸载
 *
 * Create by jiangdongguo on 2016-12-6 下午10:33:26
 */
public class MainActivity extends Activity implements OnClickListener{
	private final String ACTION_ALERT_DIALOG = "com.corget.over.dialog";
	private final String PACKAGE_NAME = "com.example.demoaliveclient";
	private final String SERVICE_NAME = "com.example.demoaliveclient.acessibilty.MyAccessibilityService";	
	private final String APP_NAME = "LuChiBaoApp.apk";
	private final String APP_PACKAGENAME = "com.luchibao";
	private Button installLcbBtn;
	private Button uninstallLcbBtn;	
	private MyAccessibilityUtils mAccessUtils;
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }


	private void init() {
        installLcbBtn = (Button)findViewById(R.id.install_weixin_btn);    
        uninstallLcbBtn = (Button)findViewById(R.id.uninstall_weixin_btn);
        installLcbBtn.setOnClickListener(this);
        uninstallLcbBtn.setOnClickListener(this);	
        
		mAccessUtils = new MyAccessibilityUtils(MainActivity.this,
				PACKAGE_NAME, SERVICE_NAME);
		if (!mAccessUtils.isAccessibilitySettingsOn()) {
			mAccessUtils.popOpenAlertDialog(ACTION_ALERT_DIALOG);
		}
	}


	private void installApk() {
		String outFilePath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+APP_NAME;
		if(copyApkFromAssets(APP_NAME,outFilePath)){
			Log.d("Debug","安装apk");
			mAccessUtils.installApp(outFilePath,APP_PACKAGENAME);
		}
	}
    
    private boolean copyApkFromAssets(String fileName,String outFilePath){
    	Log.d("Debug","拷贝apk到sd卡");
    	boolean copyIsFinished = false;
    	try {
    		AssetManager manager = getAssets();
    		InputStream is = manager.open(fileName);
    		//根据路径,创建指定文件
    		File outfile = new File(outFilePath);
    		outfile.createNewFile();
    		FileOutputStream fos = new FileOutputStream(outfile);
    		byte[] temp = new byte[1024];
    		int i = 0;
    		while((i=is.read(temp)) > 0) {
    			Log.d("Debug","拷贝......");
				fos.write(temp,0,i);
			}
    		fos.close();
    		is.close();
    		copyIsFinished = true;
    		Log.d("Debug","拷贝结束");
		} catch (IOException e) {
			e.printStackTrace();
		}
    	return copyIsFinished;
    }


	@Override
	public void onClick(View v) {
		int vId = v.getId();
		switch (vId) {
		case R.id.install_weixin_btn:
			installApk();
			break;
		case R.id.uninstall_weixin_btn:
			mAccessUtils.uninstallApp(APP_PACKAGENAME);
			break;
		default:
			break;
		}
	}
}

(6)AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.demoaliveclient"
    android:sharedUserId="com.teligen.test"
    android:versionCode="1"
    android:versionName="1.0" >
	
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />


    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="21" />


    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:persistent="true"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />


                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <service
            android:name=".AliveClientService"
            android:enabled="true"
            android:exported="true" />
        <!-- 辅助服务 -->
        <service
            android:name="com.example.demoaliveclient.acessibilty.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_settings" />
        </service>
        <!-- 全局广播接收器 -->
        <receiver 
            android:name="com.example.demoaliveclient.acessibilty.AlertDialogReceiver" >
            <intent-filter>
                <action android:name="com.corget.over.dialog" />
            </intent-filter>
        </receiver>
    </application>


</manifest>

说明:在AndroidManifest.xml文件中声明AccessibilityService时,有如下几点必须注意:
    a)android:name属性为自定义MyAccessibilityService的绝对类名;
    b)声明"android.permission.BIND_ACCESSIBILITY_SERVICE"权限;
    c)使用<meta-data../>设定AccessibilityService的配置信息,其中,android:name属性的值
必须为"android.accessibilityservice"、android:resource属性的值为配置信息,保存在res/xml目录下的accessibility_settings.xml文件,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service 
	 android:packageNames="com.android.packageinstaller"
     android:description="@string/accessibility_service_description"
     android:accessibilityEventTypes="typeAllMask" 
     android:accessibilityFeedbackType="feedbackGeneric" 
     android:accessibilityFlags="flagDefault"
     android:notificationTimeout="100" 
     android:canRetrieveWindowContent="true"
  xmlns:android="http://schemas.android.com/apk/res/android" />

  其中,android:packageNames指明AccessibilityService监听哪个应用程序下的窗口活动,这里写com.android.packageinstaller表示监听Android系统的安装界面。android:description指定在无障碍服务当中显示给用户看的说明信息。android:accessibilityEventTypes指定我们在监听窗口中可以模拟哪些事件,这里写typeAllMask表示所有的事件都能模拟。

码字不易,转载请声明出处:http://blog.csdn.net/andrexpert/article/details/53494776 


最后的话:“AccessibilityService由于能够模仿用户对终端进行操作,功能远不止这点,有待深入研究。”


关于资料与Demo智能安装APP

郭神的智能安装:http://blog.csdn.net/guolin_blog/article/details/47803149

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值