前言
前几天看到一个很有趣的应用视频“小不点”交互机器人,其中有一段是用户给它发一段文字/语音,譬如“我想在美团点一份鸡排”,然后“小不点”自动将美团应用弹出,并进行“鸡排”搜索等操作,如下图进行简化后的demo所示。
当时感觉到这样子的交互方式挺有趣的,在安卓上也有一定的方案可以实现,今天就基于AccessibilityService来实现了一下。(demo中省去一些自然语言处理的应用,最近也在学习这方面的知识)。
一、demo的简化
在demo中,省去了原本与机器人交互的输入框和对用户的自然语言处理,而直接摆放一个按钮来直接实现在美团应用中搜索鸡排。
简单的布局文件:按钮点击后会调用Activity中的 meituan 函数
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.cxmscb.cxm.arobot.MainActivity"> <Button android:onClick="meituan" android:textSize="16sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="我想在美团买鸡排" /> </LinearLayout>
布局对应的Activity :
public class MainActivity extends AppCompatActivity { // 用来获取获取打开美团的intent信使 PackageManager packageManager ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 获取安装包管理器 packageManager = this.getPackageManager(); // 用来让用户打开应用的辅助功能,这样辅助服务才会有效 Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); startActivity(intent); } public void meituan(View view) { // 设置辅助服务为运行状态。这样做主要为了不破坏用户对应用的自主操作 MyApplication.getInstance().setFlag(true); // 设置美团搜索的信息变量 MyApplication.getInstance().setParams("鸡排"); // 获取启动美团的intent Intent intent = packageManager.getLaunchIntentForPackage("com.sankuai.meituan");//"jp.co.johospace.jorte"就是我们获得要启动应用的包名 // 每次启动美团应用时,但是以重新启动应用的形式打开 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // 跳转 startActivity(intent); } @Override protected void onResume() { super.onResume(); // 设置辅助服务为不运行状态。 MyApplication.getInstance().setFlag(false); } }
二、AccessibilityService的应用
在 MainAcitivity 中,我们请求用户打开应用的ACCESSIBILITY服务,主要是为了应用的辅助服务能够生效。为了避免辅助服务随时随刻地运行影响到美团应用的正常使用,我们还在上面设置了个Flag。
为自定义的AccessibilityService的xml配置 :
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_service_description" android:accessibilityEventTypes="typeAllMask" android:accessibilityFlags="flagDefault" android:accessibilityFeedbackType="feedbackGeneric" android:canRetrieveWindowContent="true" />
其中 description 为 用户允许应用的辅助功能的说明字符串,这里没有指定所要辅助的应用 packageNames,当没有指定时,默认辅助所有的应用。typeAllMask 是设置响应事件的类型,feedbackGeneric 是设置回馈给用户的方式,有语音播出和振动。
对自定义的AccessibilityService在mainfest清单文件中的注册:
<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>
其中label为用户在辅助功能列表中所看到的名称,注意加上对应的权限和action,并在meta-data中添加对AccessibilityService的配置。
自定义的AccessibilityService中的具体实现
当启动应用的辅助功能后,有个后台服务将会手机界面进行事件监听,在进行一些函数回调,如 onAccessibilityEvent 函数,其中传入的就是当前发生的窗口事件,我们需要在其中对事件类型判断和对窗口中的布局进行解析获取指定布局中的控件。
对于美团应用中的布局控件,我们可以使用DDMS来获取到,如下图以美团的第一个页面布局为例:
打开美团应用后,我们进行搜索时需要点击上方的“搜索商家…”控件。所以我们需要指定页面到来时,解析到控件,并对该控件(或其父布局)进行点击。其他页面类似。(也有一些页面中的控件需要输入文件)。
从图中我们可以这个“搜索商家…”控件的类为TextView,我们可以指定“文字”和类来对该控件进行搜索。
public class MyAccessibilityService extends AccessibilityService { // 当前事件发生的包名 String nowPackageName; @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) @Override public void onAccessibilityEvent(AccessibilityEvent event) { // 获取当前事件的包名 nowPackageName = event.getPackageName().toString(); // 判断是否为美团应用、并判断当前状态是否为运行状态 if(nowPackageName.equals("com.sankuai.meituan")&&MyApplication.getInstance().getFlag()){ // 判断是否为我们所需的窗口状态变化 if(event.getEventType()==AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED){ // 获取事件活动的窗口布局根节点 AccessibilityNodeInfo rootNode = this.getRootInActiveWindow(); // 解析根节点 handle(rootNode); } } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private boolean handle(AccessibilityNodeInfo info) { // 判断节点是否有子控件 if (info.getChildCount() == 0) { // 判断节点是否有文字并且有“搜索”文字 if(info.getText() != null&&info.getText().toString().contains("搜索")){ // 美团的第一个页面布局:判断节点是否为TextView,文字是否匹配 if("搜索商家、品类或商圈".equals(info.getText().toString())&&"android.widget.TextView".equals(info.getClassName())){ // 判断节点控件是否可点击,不可点击则点击其父布局,以此类推。 AccessibilityNodeInfo parent = info; while (parent!=null){ if(parent.isClickable()){ // 模拟点击,跳出循环 parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); break; } parent = parent.getParent(); } } // 对第二个布局进行判断:判断是否为一个EidtText对象 else if("搜索商家、品类或商圈".equals(info.getText().toString())&&"android.widget.EditText".equals(info.getClassName())){ // 以剪贴板的形式进行文字输入 ClipboardManager clipboardManager = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE); ClipData clipData =ClipData.newPlainText("scb", MyApplication.getInstance().getParams()); clipboardManager.setPrimaryClip(clipData); // 模拟粘贴 info.performAction(AccessibilityNodeInfo.ACTION_PASTE); } // 对第三个布局进行判断:判断是否为一个文字为“搜索”的TextView else if("搜索".equals(info.getText().toString())&&"android.widget.TextView".equals(info.getClassName())){ // 以同样的原理进行控件点击 AccessibilityNodeInfo parent = info; while (parent!=null){ if(parent.isClickable()){ parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); break; } parent = parent.getParent(); } // 服务进行结束后,设置Flag MyApplication.getInstance().setFlag(false); return true; }else { // 其他条件下设置Flag MyApplication.getInstance().setFlag(false); } } } else { // 当 当前节点 有子控件时,解析它的孩子,以此递归 for (int i = 0; i < info.getChildCount(); i++) { if(info.getChild(i)!=null){ handle(info.getChild(i)); } } } return false; } @Override public void onInterrupt() { Toast.makeText(this,"zz",Toast.LENGTH_SHORT).show(); } }
可以看出,上面的代码是按照美团的搜索过程理顺下来的逻辑。
三、思考
从代码中我们可以看出,对其他应用的调用过于依赖其应用,依赖于其布局,当其他应用的布局发生改变时,功能便无法生效了。
从代码中我们还可以看出,这种对其他应用的调用很难进行模块化,很容易导致代码过多,当支持调用的应用多时,代码也会增加很多。
对美团应用的调起过程中,太多中间的页面环节。