为了支持跨活动的集成测试robotium在待测应用启动后使用ActiveMonitor每50毫秒监听系统中最新创建的活动并将其放在内部保留的活动堆栈模拟Android系统里的活动堆栈内。因为创建活动的时间比较长而且一般来说待测应用也不会经常性的创建和销毁活动界面所以50毫秒检查一次就足够了。由于robotium保存有当前系统内所有的界面而且它与待测应用运行在同一个进程里因此它可以随意查看和在界面里搜索需要的控件。其关键代码摘抄和批注如代码清单3-16所示。
代码清单3-16 robotium获取待测应用打开的所有活动的源码批注
1. // 在测试用例的setUp函数中,一般是调用Solo的这个构造函数实例化Solo
2. // 其源码保存在Solo.java里
3. public Solo(Instrumentation instrumentation, Activity activity) {
4.     this.sleeper = new Sleeper();
5.     // ActivityUtils负责监听和管理活动创建和销毁的消息
6.     this.activityUtils = new ActivityUtils(instrumentation, activity, sleeper);
7.     this.viewFetcher = new ViewFetcher(activityUtils);
8.     // ......
9.     this.clicker = new Clicker(activityUtils, viewFetcher,
10.                               scroller,robotiumUtils,
11.                                                 instrumentation, sleeper, waiter);
12.     // ......
13. }
14. // 下面的源码都保存在ActivityUtils.java里
15. public ActivityUtils(Instrumentation inst, Activity activity, Sleeper sleeper) {
16.      // ......
17.
18.     // 定时器,每个50毫秒触发一次,判断是否有新活动创建还是
19.     // 有老活动销毁
20.     activitySyncTimer = new Timer();
21.     // 保存当前监听到的待测应用的活动堆栈
22.     activitiesStoredInActivityStack = new Stack<String>();
23.     // 通过ActivityMonitor来监听和管理活动
24.     setupActivityMonitor();
25.     setupActivityStackListener();
26. }
27.
28. /**
29.  * This is were the activityMonitor is set up. The monitor will keep check
30.  * for the currently active activity.
31.  */
32. private void setupActivityMonitor() {
33.    try {
34.        // 通过设置ActivityMonitor的IntentFilter为空
35.            // 告诉系统这个ActivityMonitor对所有新创建的
36.            // 活动都感兴趣
37.            IntentFilter filter = null;
38.            activityMonitor = inst.addMonitor(filter, null, false);
39.    } catch (Exception e) {
40.            e.printStackTrace();
41.    }
42. }
43.
44. /**
45.  * This is were the activityStack listener is set up. The listener will keep track of the
46.  * opened activities and their positions.
47.  */
48. private void setupActivityStackListener() {
49.    // 设置定时器的回调函数每隔50毫秒调用一次
50.    // 通过判断监视待测应用的ActivityMonitor获取的最新活动的状态
51.    // 来决定是否新增或删除活动
52.    TimerTask activitySyncTimerTask = new TimerTask() {
53.           @Override
54.           public void run() {
55.               if (activityMonitor != null){
56.                 Activity activity = activityMonitor.getLastActivity();
57.                 if (activity != null){
58.                   if(!activitiesStoredInActivityStack.isEmpty() &&
59.                       activitiesStoredInActivityStack.peek().equals(
60.                                               activity.toString()))
61.                   return;
62.
63.                   if(!activity.isFinishing()){
64.
                       if(activitiesStoredInActivityStack.remove(activity.toString()))
65.                                               removeActivityFromStack(activity);
66.
67.                     addActivityToStack(activity);
68.                   }
69.               }
70.           }
71.       }
72.    };
73.    activitySyncTimer.schedule(activitySyncTimerTask, 0, ACTIVITYSYNCTIME);
74. }
而robotium在单击一个控件时首先会从当前最上层的活动界面取出所有视图并根据API的要求过滤视图再获取过滤出来的视图的大小和位置计算出控件的中点坐标最后向Android系统注入单击消息需要包含单击的坐标来实现单击控件的功能。以本书经常用到的clickOnText函数为例在代码清单3-17中摘抄和批注其关键代码
代码清单3-17 robotium clickOnText的源码批注
1. // Solo.clickOnText
2. // 仅仅是简单地将调用转发到clicker对象的clickOnText
3. // 源码位置Solo.java
4. public void clickOnText(String text) {
5.     clicker.clickOnText(text, false, 1, true, 0);
6. }
7.
8.     // clicker.clickOnText函数
9.     // 可以看到clickOnText的第一个参数名也就是根据文本单击控件的文本参数
10.    // 名为regex隐含的意思是接受正则表达式
11.    //
12.    // 源码位置Clicker.java
13. public void clickOnText(String regex, boolean longClick, int match, boolean scroll, int time) {
14.    waiter.waitForText(regex, 0, TIMEOUT, scroll, true);
15.    TextView textToClick = null;
16.    // 获取待测应用当前活动上所有的视图或者说控件因为
17.    // Android里大部分控件都是从View继承下来的
18.    ArrayList <TextView> allTextViews = viewFetcher.getCurrentViews(TextView.class);
19.    // 移掉一些不可见的控件因为不可见的控件是无法单击的
20.    // 这样可以避免一个活动界面上有两个控件具有相同的文本
21.    // 其中一个不可见导致将单击消息发送到错误的控件上
22.    allTextViews = RobotiumUtils.removeInvisibleViews(allTextViews);
23.    if (match == 0) {
24.           match = 1;
25.    }
26.    for (TextView textView : allTextViews){
27.      // 针对每个可见的控件判断其显示的文本与正则表达式相匹配
28.           if (RobotiumUtils.checkAndGetMatches(
29.                   regex, textView, uniqueTextViews) == match) {
30.                  uniqueTextViews.clear();
31.                  textToClick = textView;
32.                  break;
33.           }
34.    }
35.    // 如果有相匹配的控件则试图根据控件的大小和位置计算
36.    // 控件的中点坐标发送单击消息
37.    if (textToClick != null) {
38.           clickOnScreen(textToClick, longClick, time);
39.    // 如果当前的界面是一个列表而且有滚动条那么一次向下
40.    // 滚动一次再次查找是否有匹配输入正则表达式的控件。
41.    // 这是因为Android系统不会为尚未显示的控件分配任何内存
42.    // 注意里面的调用是clickOnText函数自己也就是说这是一个
43.    // 递归调用
44.    } else if (scroll && scroller.scroll(Scroller.DOWN)) {
45.           clickOnText(regex, longClick, match, scroll, time);
46.    // 否则没有任何控件上的文本匹配输入的正则表达式
47.    // 只好报错了
48.    } else {
49.           int sizeOfUniqueTextViews = uniqueTextViews.size();
50.           uniqueTextViews.clear();
51.           if (sizeOfUniqueTextViews > 0)
52.               Assert.assertTrue("There are only " + sizeOfUniqueTextViews +
53.                                  " matches of " + regex, false);
54.           else {
55.                 for (TextView textView : allTextViews) {
56.                        Log.d(LOG_TAG, regex + " not found. Have found: " +
57.                              textView.getText());
58.                 }
59.                 Assert.assertTrue("The text: " + regex + " is not found!",
60.                                    false);
61.           }
62.    }
63. }
64.
65. // 这个函数根据传入控件的大小和位置计算要单击的中点
66. // 并向Android系统对控件发送单击消息
67. public void clickOnScreen(View view, boolean longClick, int time) {
68.    if(view == null)
69.        Assert.assertTrue("View is null and can therefore not be clicked!",
70.                          false);
71.    int[] xy = new int[2];
72.
73.    view.getLocationOnScreen(xy);
74.
75.    final int viewWidth = view.getWidth();
76.    final int viewHeight = view.getHeight();
77.    final float x = xy[0] + (viewWidth / 2.0f);
78.    float y = xy[1] + (viewHeight / 2.0f);
79.
80.    if (longClick)
81.           clickLongOnScreen(x, y, time);
82.    else
83.           clickOnScreen(x, y);
84. }