├── README.md └── com └── robotium └── solo ├── ActivityUtils.java ├── Asserter.java ├── By.java ├── Checker.java ├── Clicker.java ├── Condition.java ├── DialogUtils.java ├── GLRenderWrapper.java ├── Getter.java ├── Presser.java ├── Reflect.java ├── RobotiumTextView.java ├── RobotiumUtils.java ├── RobotiumWeb.js ├── RobotiumWebClient.java ├── Rotator.java ├── ScreenshotTaker.java ├── Scroller.java ├── Searcher.java ├── Sender.java ├── Setter.java ├── Sleeper.java ├── Solo.java ├── Swiper.java ├── Tapper.java ├── TextEnterer.java ├── Timeout.java ├── ViewFetcher.java ├── ViewLocationComparator.java ├── Waiter.java ├── WebElement.java ├── WebElementCreator.java ├── WebUtils.java └── Zoomer.java /README.md: -------------------------------------------------------------------------------- 1 | robotium 2 | ======== 3 | 4 | robotium source code chinese comment 5 | ownered by 银古 6 | -------------------------------------------------------------------------------- /com/robotium/solo/ActivityUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.lang.ref.WeakReference; 4 | import java.util.ArrayList; 5 | import java.util.Iterator; 6 | import java.util.Stack; 7 | import java.util.Timer; 8 | import java.util.TimerTask; 9 | 10 | import junit.framework.Assert; 11 | import android.app.Activity; 12 | import android.app.Instrumentation; 13 | import android.app.Instrumentation.ActivityMonitor; 14 | import android.content.IntentFilter; 15 | import android.util.Log; 16 | import android.view.KeyEvent; 17 | 18 | 19 | /** 20 | * 用于Activity操作的工具类 21 | * Contains activity related methods. Examples are: 22 | * getCurrentActivity(), getActivityMonitor(), setActivityOrientation(int orientation). 23 | * 24 | * @author Renas Reda, renas.reda@robotium.com 25 | * 26 | */ 27 | 28 | class ActivityUtils { 29 | // Instrument 各种事件发送强大利器 30 | private final Instrumentation inst; 31 | // activitymonitor 所有的activity变化都可以监控 32 | private ActivityMonitor activityMonitor; 33 | // 普通的 activity 34 | private Activity activity; 35 | // 用于做延时的,看了UIAutomator的代码,都用SystemClock.sleep()了,比这个看着优雅 36 | private final Sleeper sleeper; 37 | // 日志标签,log 日志输出会带上robotium的标签.标记框架是他们的 38 | private final String LOG_TAG = "Robotium"; 39 | // 短等待时间100ms 40 | private final int MINISLEEP = 100; 41 | // 用于activitymonitor循环抓取当前activity的等待50ms 42 | private static final int ACTIVITYSYNCTIME = 50; 43 | // activity堆栈,用于存放所有开启状态的activity,采用WeakReference,避免对GC产生影响 44 | private Stack> activityStack; 45 | // Activity对象引用变量,使用WeakReference,避免对GC产生影响 46 | private WeakReference weakActivityReference; 47 | // 堆栈存储activity的名字 48 | private Stack activitiesStoredInActivityStack; 49 | // 定时器,用于定时获取最新的activity,定时时间就是上面定义的50ms 50 | private Timer activitySyncTimer; 51 | /** 52 | * 构造函数 53 | * Constructs this object. 54 | * 55 | * @param inst the {@code Instrumentation} instance. 获取instrument一般都是通过getIntrument()获取的传递给构造函数 56 | * @param activity the start {@code Activity} 应用启动的activity,一般是传递mainActivity 57 | * @param sleeper the {@code Sleeper} instance Sleep工具类 58 | */ 59 | 60 | public ActivityUtils(Instrumentation inst, Activity activity, Sleeper sleeper) { 61 | this.inst = inst; 62 | this.activity = activity; 63 | this.sleeper = sleeper; 64 | createStackAndPushStartActivity(); 65 | activitySyncTimer = new Timer(); 66 | activitiesStoredInActivityStack = new Stack(); 67 | // 开启 activity监控 68 | setupActivityMonitor(); 69 | setupActivityStackListener(); 70 | } 71 | 72 | 73 | 74 | /** 75 | * 创建一个堆栈,用于存放创建的activity.因为 activity创建了新的老的就在后面了,所以使用堆栈的先进后出功能 76 | * 77 | * Creates a new activity stack and pushes the start activity. 78 | */ 79 | 80 | private void createStackAndPushStartActivity(){ 81 | // 初始化一个堆栈 82 | activityStack = new Stack>(); 83 | // 如果构造函数传入的activity不为null,那么假如堆栈最为当前最新的activity 84 | if (activity != null){ 85 | WeakReference weakReference = new WeakReference(activity); 86 | activity = null; 87 | activityStack.push(weakReference); 88 | } 89 | } 90 | 91 | /** 92 | * 返回所有处于打开或运行状态的activity,一般代码编写返回一个 List较好 93 | * Returns a {@code List} of all the opened/active activities. 94 | * 95 | * @return a {@code List} of all the opened/active activities 96 | */ 97 | 98 | public ArrayList getAllOpenedActivities() 99 | { 100 | // 构造一个 List 用于返回 activity数组 101 | ArrayList activities = new ArrayList(); 102 | // 遍历activityStack堆栈中的所有activity 加如到List中 103 | Iterator> activityStackIterator = activityStack.iterator(); 104 | // 判断是否可以继续遍历 105 | while(activityStackIterator.hasNext()){ 106 | // 获取当前activity,堆栈指针指向下个activity对象 107 | Activity activity = activityStackIterator.next().get(); 108 | // 判断activity对象非空,才加入,可能由于gc导致对象已经被回收,导致null异常 109 | if(activity!=null) 110 | activities.add(activity); 111 | } 112 | // 返回所有的当前存活activity 113 | return activities; 114 | } 115 | 116 | /** 117 | * 通过instrument构造一个activityMonitor用于监控activity的创建 118 | * This is were the activityMonitor is set up. The monitor will keep check 119 | * for the currently active activity. 120 | */ 121 | 122 | private void setupActivityMonitor() { 123 | 124 | try { 125 | // 为了addMonitor方法需要,创建一个null对象 126 | IntentFilter filter = null; 127 | // 获取一个activityMonitor 128 | activityMonitor = inst.addMonitor(filter, null, false); 129 | } catch (Exception e) { 130 | e.printStackTrace(); 131 | } 132 | } 133 | 134 | /** 135 | * 通过定时任务不断刷新获取当前最新创建的activity,定时每50ms运行一次,因此存在一定的概率获取的不是最新的activity 136 | * This is were the activityStack listener is set up. The listener will keep track of the 137 | * opened activities and their positions. 138 | */ 139 | 140 | private void setupActivityStackListener() { 141 | // 创建一个定时任务 142 | TimerTask activitySyncTimerTask = new TimerTask() { 143 | @Override 144 | public void run() { 145 | // 检查activitymonitor是否已创建,避免null异常 146 | if (activityMonitor != null){ 147 | // 获取当前最新的activity 148 | Activity activity = activityMonitor.getLastActivity(); 149 | // 检查获取对象是否为null 150 | if (activity != null){ 151 | // 如果该activity已经存储在activity堆栈中,则不进行重复添加 152 | if(!activitiesStoredInActivityStack.isEmpty() && activitiesStoredInActivityStack.peek().equals(activity.toString())){ 153 | return; 154 | } 155 | // 移除可能存在同名对象,避免堆栈加入脏数据 156 | if (activitiesStoredInActivityStack.remove(activity.toString())){ 157 | removeActivityFromStack(activity); 158 | } 159 | // 确保activity还处于存活状态,并加入堆栈 160 | if (!activity.isFinishing()){ 161 | addActivityToStack(activity); 162 | } 163 | } 164 | } 165 | } 166 | }; 167 | // 开启定时任务,每50ms执行一次 168 | activitySyncTimer.schedule(activitySyncTimerTask, 0, ACTIVITYSYNCTIME); 169 | } 170 | 171 | /** 172 | * 从activity堆栈中移除一个activity 173 | * Removes a given activity from the activity stack 174 | * 175 | * @param activity the activity to remove 176 | */ 177 | 178 | private void removeActivityFromStack(Activity activity){ 179 | // 遍历整个堆栈 180 | Iterator> activityStackIterator = activityStack.iterator(); 181 | while(activityStackIterator.hasNext()){ 182 | // 获取当前位置的activity 183 | Activity activityFromWeakReference = activityStackIterator.next().get(); 184 | // 如果发现当前堆栈中存在 null对象,则移除之 185 | if(activityFromWeakReference == null){ 186 | activityStackIterator.remove(); 187 | } 188 | // 找对了对应的activity,则移除之 189 | if(activity!=null && activityFromWeakReference!=null && activityFromWeakReference.equals(activity)){ 190 | activityStackIterator.remove(); 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * 获取ActivityMonitor对象,一般这个也没啥用 197 | * Returns the ActivityMonitor used by Robotium. 198 | * 199 | * @return the ActivityMonitor used by Robotium 200 | */ 201 | 202 | public ActivityMonitor getActivityMonitor(){ 203 | return activityMonitor; 204 | } 205 | 206 | /** 207 | * 设置屏幕方向,横或者纵 208 | * Sets the Orientation (Landscape/Portrait) for the current activity. 209 | * 210 | * @param orientation An orientation constant such as {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE} or {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_PORTRAIT} 211 | */ 212 | 213 | public void setActivityOrientation(int orientation) 214 | { 215 | Activity activity = getCurrentActivity(); 216 | activity.setRequestedOrientation(orientation); 217 | } 218 | 219 | /** 220 | * 获取当前的activity,true标识需要等待500ms,false标识不需要等待500ms 221 | * Returns the current {@code Activity}, after sleeping a default pause length. 222 | * 223 | * @param shouldSleepFirst whether to sleep a default pause first 224 | * @return the current {@code Activity} 225 | */ 226 | 227 | public Activity getCurrentActivity(boolean shouldSleepFirst) { 228 | return getCurrentActivity(shouldSleepFirst, true); 229 | } 230 | 231 | /** 232 | * 获取当前activity,并且等待500ms 233 | * Returns the current {@code Activity}, after sleeping a default pause length. 234 | * 235 | * @return the current {@code Activity} 236 | */ 237 | 238 | public Activity getCurrentActivity() { 239 | return getCurrentActivity(true, true); 240 | } 241 | 242 | /** 243 | * 把activity加入堆栈中 244 | * Adds an activity to the stack 245 | * 246 | * @param activity the activity to add 247 | */ 248 | 249 | private void addActivityToStack(Activity activity){ 250 | // activity名加入堆栈 251 | activitiesStoredInActivityStack.add(activity.toString()); 252 | weakActivityReference = new WeakReference(activity); 253 | activity = null; 254 | // activity弱引用对象加入堆栈 255 | activityStack.push(weakActivityReference); 256 | } 257 | 258 | /** 259 | * 一直等待,知道出现抓取到一个存活的activity,未找到存活activity则不断迭代循环,有概率导致无限死循环 260 | * 可自行修改添加一个超时时间,避免引发无法循环 261 | * Waits for an activity to be started if one is not provided 262 | * by the constructor. 263 | */ 264 | 265 | private final void waitForActivityIfNotAvailable(){ 266 | // 如果当前堆栈中的activity为空,当初始化时传入的activity为null,可导致该状态 267 | if(activityStack.isEmpty() || activityStack.peek().get() == null){ 268 | // 不断尝试获取当前activity,直到获取到一个存活的activity 269 | if (activityMonitor != null) { 270 | Activity activity = activityMonitor.getLastActivity(); 271 | // 此处可能导致无限循环 272 | // activityMonitor初始化是为得到当前activity.应用又没有新打开页面,调用该方法就死循环了 273 | // 传入一个null的activity对象,在 初始化之后,没打开新的 activity就不断null,死循环了 274 | while (activity == null){ 275 | // 等待300ms 276 | sleeper.sleepMini(); 277 | // 获取当前activity 278 | activity = activityMonitor.getLastActivity(); 279 | } 280 | // 非空对象加入堆栈 281 | addActivityToStack(activity); 282 | } 283 | else{ 284 | // 等待300ms 285 | sleeper.sleepMini(); 286 | // 初始化activityMonitor 287 | setupActivityMonitor(); 288 | // 继续获取最新的activity 289 | waitForActivityIfNotAvailable(); 290 | } 291 | } 292 | } 293 | 294 | /** 295 | * 获取当前最新的activity,shouldSleepFirst为true,那么等待500ms后在获取, 296 | * waitForActivity为true那么尝试获取最新的activity,为false则不尝试获取最新的,直接从activity堆栈中获取栈顶的activity返回 297 | * 298 | * Returns the current {@code Activity}. 299 | * 300 | * @param shouldSleepFirst whether to sleep a default pause first 301 | * @param waitForActivity whether to wait for the activity 302 | * @return the current {@code Activity} 303 | */ 304 | 305 | public Activity getCurrentActivity(boolean shouldSleepFirst, boolean waitForActivity) { 306 | // 是否需要等待 307 | if(shouldSleepFirst){ 308 | sleeper.sleep(); 309 | } 310 | // 是否需要获取最新的 311 | if(waitForActivity){ 312 | waitForActivityIfNotAvailable(); 313 | } 314 | // 获取堆栈中的栈顶activity 315 | if(!activityStack.isEmpty()){ 316 | activity=activityStack.peek().get(); 317 | } 318 | return activity; 319 | } 320 | 321 | /** 322 | * 检查 activity堆栈是否为空 323 | * Check if activity stack is empty. 324 | * 325 | * @return true if activity stack is empty 326 | */ 327 | 328 | public boolean isActivityStackEmpty() { 329 | return activityStack.isEmpty(); 330 | } 331 | 332 | /** 333 | * 通过不断触发返回按钮尝试回到指定名字的activity 334 | * Returns to the given {@link Activity}. 335 | * 336 | * @param name the name of the {@code Activity} to return to, e.g. {@code "MyActivity"} 337 | */ 338 | 339 | public void goBackToActivity(String name) 340 | { 341 | // 获取所有存活的activity 342 | ArrayList activitiesOpened = getAllOpenedActivities(); 343 | boolean found = false; 344 | // 遍历所有存活的activity,如果不存在指定的activity,则为false,找到为 true 345 | for(int i = 0; i < activitiesOpened.size(); i++){ 346 | if(activitiesOpened.get(i).getClass().getSimpleName().equals(name)){ 347 | found = true; 348 | break; 349 | } 350 | } 351 | // 如果找对需要返回的activity在activity堆栈中.那么尝试货到该activity 352 | if(found){ 353 | // 判断当前activity是否为需要返回的,不是是不断发送返回指令,直到找到 354 | while(!getCurrentActivity().getClass().getSimpleName().equals(name)) 355 | { 356 | try{ 357 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 358 | // instrument 触发该指令可能导致的exception 359 | }catch(SecurityException ignored){} 360 | } 361 | } 362 | // 没有找到则打印先关日志.并且抛错 363 | else{ 364 | for (int i = 0; i < activitiesOpened.size(); i++){ 365 | Log.d(LOG_TAG, "Activity priorly opened: "+ activitiesOpened.get(i).getClass().getSimpleName()); 366 | } 367 | Assert.fail("No Activity named: '" + name + "' has been priorly opened"); 368 | } 369 | } 370 | 371 | /** 372 | * 在当前activity中按照id查询String 373 | * Returns a localized string. 374 | * 375 | * @param resId the resource ID for the string 376 | * @return the localized string 377 | */ 378 | 379 | public String getString(int resId) 380 | { 381 | Activity activity = getCurrentActivity(false); 382 | return activity.getString(resId); 383 | } 384 | 385 | /** 386 | * solo生命周期结束,释放相关资源 387 | * Finalizes the solo object. 388 | */ 389 | 390 | @Override 391 | public void finalize() throws Throwable { 392 | // 停止activity监控定时任务 393 | activitySyncTimer.cancel(); 394 | try { 395 | // 清理activityMonitor对象 396 | // Remove the monitor added during startup 397 | if (activityMonitor != null) { 398 | inst.removeMonitor(activityMonitor); 399 | activityMonitor = null; 400 | } 401 | } catch (Exception ignored) {} 402 | super.finalize(); 403 | } 404 | 405 | /** 406 | * 关闭所有存活的activity 407 | * All activites that have been opened are finished. 408 | */ 409 | 410 | public void finishOpenedActivities(){ 411 | // 停止activity监听定时任务 412 | // Stops the activityStack listener 413 | activitySyncTimer.cancel(); 414 | // 获取所有存活的activity 415 | ArrayList activitiesOpened = getAllOpenedActivities(); 416 | // 结束所有存活的activity 417 | // Finish all opened activities 418 | for (int i = activitiesOpened.size()-1; i >= 0; i--) { 419 | sleeper.sleep(MINISLEEP); 420 | finishActivity(activitiesOpened.get(i)); 421 | } 422 | // 释放对象 423 | activitiesOpened = null; 424 | sleeper.sleep(MINISLEEP); 425 | // Finish the initial activity, pressing Back for good measure 426 | finishActivity(getCurrentActivity(true, false)); 427 | this.activity = null; 428 | sleeper.sleepMini(); 429 | // 点击2次back按钮退出程序 430 | try { 431 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 432 | sleeper.sleep(MINISLEEP); 433 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 434 | } catch (Throwable ignored) { 435 | // Guard against lack of INJECT_EVENT permission 436 | } 437 | // 清空堆栈信息 438 | clearActivityStack(); 439 | } 440 | 441 | /** 442 | *清空堆栈信息 443 | * Clears the activity stack. 444 | */ 445 | 446 | private void clearActivityStack(){ 447 | activityStack.clear(); 448 | activitiesStoredInActivityStack.clear(); 449 | } 450 | 451 | /** 452 | * 调用activity的清理方法结束activity生命周期 453 | * Finishes an activity. 454 | * 455 | * @param activity the activity to finish 456 | */ 457 | 458 | private void finishActivity(Activity activity){ 459 | if(activity != null) { 460 | try{ 461 | activity.finish(); 462 | }catch(Throwable e){ 463 | e.printStackTrace(); 464 | } 465 | } 466 | } 467 | 468 | } 469 | -------------------------------------------------------------------------------- /com/robotium/solo/Asserter.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import junit.framework.Assert; 4 | import android.app.Activity; 5 | import android.app.ActivityManager; 6 | 7 | /** 8 | * 测试的断言方法,提供断言支持,主要包装junit提供的 9 | * Contains assert methods examples are assertActivity() and assertLowMemory(). 10 | * 11 | * @author Renas Reda, renas.reda@robotium.com 12 | * 13 | */ 14 | 15 | class Asserter { 16 | // activity操作工具类 17 | private final ActivityUtils activityUtils; 18 | // 等待工具类 19 | private final Waiter waiter; 20 | 21 | /** 22 | * 对象构造初始化 23 | * Constructs this object. 24 | * 25 | * @param activityUtils the {@code ActivityUtils} instance. 26 | * @param waiter the {@code Waiter} instance. 27 | */ 28 | 29 | public Asserter(ActivityUtils activityUtils, Waiter waiter) { 30 | this.activityUtils = activityUtils; 31 | this.waiter = waiter; 32 | } 33 | 34 | /** 35 | * 断言判断当前activity是否是想要的 36 | * message 当前activity名与name名字不一致则给出断言提示 37 | * name 期望的activity名字 38 | * Asserts that an expected {@link Activity} is currently active one. 39 | * 40 | * @param message the message that should be displayed if the assert fails 41 | * @param name the name of the {@code Activity} that is expected to be active e.g. {@code "MyActivity"} 42 | */ 43 | 44 | public void assertCurrentActivity(String message, String name) { 45 | // 使用wait工具等待期望的activity出现,直接获取activity堆栈的栈顶activity,默认超时10s 46 | boolean foundActivity = waiter.waitForActivity(name); 47 | // 如果期望的activity未找到,则用断言提示相关异常 48 | if(!foundActivity) 49 | Assert.assertEquals(message, name, activityUtils.getCurrentActivity().getClass().getSimpleName()); 50 | } 51 | 52 | /** 53 | * 按照Class类断言当前activity是否是期望的activity 54 | * message 如果不是期望的,断言的提示信息 55 | * expectedClass 期望的activity类 56 | * Asserts that an expected {@link Activity} is currently active one. 57 | * 58 | * @param message the message that should be displayed if the assert fails 59 | * @param expectedClass the {@code Class} object that is expected to be active e.g. {@code MyActivity.class} 60 | */ 61 | 62 | public void assertCurrentActivity(String message, Class expectedClass) { 63 | // null检查 64 | if(expectedClass == null){ 65 | Assert.fail("The specified Activity is null!"); 66 | } 67 | // 检查期望的class对应的activity是否出现,直接获取activity堆栈的栈顶activity,默认超时10s 68 | boolean foundActivity = waiter.waitForActivity(expectedClass); 69 | // 未找到,断言给出错误提示 70 | if(!foundActivity) { 71 | Assert.assertEquals(message, expectedClass.getName(), activityUtils.getCurrentActivity().getClass().getName()); 72 | } 73 | } 74 | 75 | /** 76 | * 断言当前activity是否与输入的activity名字一致 77 | * message 不一致的提示信息 78 | * name 期望的activity名字 79 | * isNewInstance 为true则等待最新出现的activity,为false则直接获取activity堆栈的栈顶activity做比较 80 | * Asserts that an expected {@link Activity} is currently active one, with the possibility to 81 | * verify that the expected {@code Activity} is a new instance of the {@code Activity}. 82 | * 83 | * @param message the message that should be displayed if the assert fails 84 | * @param name the name of the {@code Activity} that is expected to be active e.g. {@code "MyActivity"} 85 | * @param isNewInstance {@code true} if the expected {@code Activity} is a new instance of the {@code Activity} 86 | */ 87 | 88 | public void assertCurrentActivity(String message, String name, boolean isNewInstance) { 89 | // 检查当前activity的名字是否是期望的 90 | assertCurrentActivity(message, name); 91 | // 检查当前activity的类对象是否是存活的 92 | assertCurrentActivity(message, activityUtils.getCurrentActivity().getClass(), 93 | isNewInstance); 94 | } 95 | 96 | /** 97 | * 检查class类是否为当前的activity 98 | * message 当不是期望的activity时提示异常信息 99 | * expectedClass 期望的activity类 100 | * isNewInstance 为true则等待最新出现的activity,为false则直接获取activity堆栈的栈顶activity做比较 101 | * Asserts that an expected {@link Activity} is currently active one, with the possibility to 102 | * verify that the expected {@code Activity} is a new instance of the {@code Activity}. 103 | * 104 | * @param message the message that should be displayed if the assert fails 105 | * @param expectedClass the {@code Class} object that is expected to be active e.g. {@code MyActivity.class} 106 | * @param isNewInstance {@code true} if the expected {@code Activity} is a new instance of the {@code Activity} 107 | */ 108 | 109 | public void assertCurrentActivity(String message, Class expectedClass, 110 | boolean isNewInstance) { 111 | boolean found = false; 112 | // 先判断当前类是否是期望的 113 | assertCurrentActivity(message, expectedClass); 114 | // 获取activity堆栈的栈顶activity 115 | Activity activity = activityUtils.getCurrentActivity(false); 116 | // 判断当前打开的所有的activity中是否存在期望的 117 | for (int i = 0; i < activityUtils.getAllOpenedActivities().size() - 1; i++) { 118 | String instanceString = activityUtils.getAllOpenedActivities().get(i).toString(); 119 | if (instanceString.equals(activity.toString())) 120 | found = true; 121 | } 122 | // 断言判断是否出现 123 | Assert.assertNotSame(message, isNewInstance, found); 124 | } 125 | 126 | /** 127 | * 检查当前是否内存过低 128 | * Asserts that the available memory is not considered low by the system. 129 | */ 130 | 131 | public void assertMemoryNotLow() { 132 | // 构建内存信息对象 133 | ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); 134 | // 获取当前activity对象获取内存信息 135 | ((ActivityManager)activityUtils.getCurrentActivity().getSystemService("activity")).getMemoryInfo(mi); 136 | // 通过lowMemory状态判断是否内存过低 137 | Assert.assertFalse("Low memory available: " + mi.availMem + " bytes!", mi.lowMemory); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /com/robotium/solo/By.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | /** 4 | * 提供给操作WebView中的对象使用 5 | * 6 | * Used in conjunction with the web methods. Examples are By.id(String id) and By.cssSelector(String selector). 7 | * 8 | * @author Renas Reda, renas.reda@robotium.com 9 | * 10 | */ 11 | 12 | public abstract class By { 13 | 14 | /** 15 | * 构造 Id对象 用于 WebView操作中WebElement的查找 16 | * Select a WebElement by its id. 17 | * 18 | * @param id the id of the web element 19 | * @return the Id object 20 | */ 21 | 22 | public static By id(final String id) { 23 | return new Id(id); 24 | 25 | } 26 | 27 | /** 28 | * 构造Xpath对象用于WebView操作中WebElement的查找 29 | * Select a WebElement by its xpath. 30 | * 31 | * @param xpath the xpath of the web element 32 | * @return the Xpath object 33 | */ 34 | 35 | public static By xpath(final String xpath) { 36 | return new Xpath(xpath); 37 | 38 | } 39 | 40 | /** 41 | * 构造一个CssSelector对象,用于WebView操作中WebElement的查找 42 | * Select a WebElement by its css selector. 43 | * 44 | * @param selectors the css selector of the web element 45 | * @return the CssSelector object 46 | */ 47 | 48 | public static By cssSelector(final String selectors) { 49 | return new CssSelector(selectors); 50 | 51 | } 52 | 53 | /** 54 | * 构造一个 Name对象,用于WebView操作中的WebElement查找 55 | * Select a WebElement by its name. 56 | * 57 | * @param name the name of the web element 58 | * @return the Name object 59 | */ 60 | 61 | public static By name(final String name) { 62 | return new Name(name); 63 | 64 | } 65 | 66 | /** 67 | * 构造一个 ClassName对象,用于WebView操作中的WebElement查找 68 | * Select a WebElement by its class name. 69 | * 70 | * @param className the class name of the web element 71 | * @return the ClassName object 72 | */ 73 | 74 | public static By className(final String className) { 75 | return new ClassName(className); 76 | 77 | } 78 | 79 | /** 80 | * 构造一个 Text对象,用于WebView操作中的WebElement查找 81 | * Select a WebElement by its text content. 82 | * 83 | * @param textContent the text content of the web element 84 | * @return the TextContent object 85 | */ 86 | 87 | public static By textContent(final String textContent) { 88 | return new Text(textContent); 89 | 90 | } 91 | 92 | /** 93 | * 构造一个 TagName对象,用于WebView操作中的WebElement查找 94 | * Select a WebElement by its tag name. 95 | * 96 | * @param tagName the tag name of the web element 97 | * @return the TagName object 98 | */ 99 | 100 | public static By tagName(final String tagName) { 101 | return new TagName(tagName); 102 | 103 | } 104 | 105 | /** 106 | * 父类方法,提給给子类实现 107 | * Returns the value. 108 | * 109 | * @return the value 110 | */ 111 | 112 | public String getValue(){ 113 | return ""; 114 | } 115 | 116 | // Id对象继承By用于WebView操作中的按照id查找WebElement 117 | static class Id extends By { 118 | private final String id; 119 | 120 | public Id(String id) { 121 | this.id = id; 122 | } 123 | 124 | @Override 125 | public String getValue(){ 126 | return id; 127 | } 128 | } 129 | // Xpath对象继承By用于WebView操作中的按照Xpath查找WebElement 130 | static class Xpath extends By { 131 | private final String xpath; 132 | 133 | public Xpath(String xpath) { 134 | this.xpath = xpath; 135 | } 136 | 137 | @Override 138 | public String getValue(){ 139 | return xpath; 140 | } 141 | } 142 | // CssSelector对象继承By用于WebView操作中的按照CssSelector查找WebElement 143 | static class CssSelector extends By { 144 | private final String selector; 145 | 146 | public CssSelector(String selector) { 147 | this.selector = selector; 148 | } 149 | 150 | @Override 151 | public String getValue(){ 152 | return selector; 153 | } 154 | } 155 | // Name对象继承By用于WebView操作中的按照Name查找WebElement 156 | static class Name extends By { 157 | private final String name; 158 | 159 | public Name(String name) { 160 | this.name = name; 161 | } 162 | 163 | @Override 164 | public String getValue(){ 165 | return name; 166 | } 167 | } 168 | // ClassName对象继承By用于WebView操作中的按照ClassName查找WebElement 169 | static class ClassName extends By { 170 | private final String className; 171 | 172 | public ClassName(String className) { 173 | this.className = className; 174 | } 175 | 176 | @Override 177 | public String getValue(){ 178 | return className; 179 | } 180 | } 181 | // Text对象继承By用于WebView操作中的按照Text查找WebElement 182 | static class Text extends By { 183 | private final String textContent; 184 | 185 | public Text(String textContent) { 186 | this.textContent = textContent; 187 | } 188 | 189 | @Override 190 | public String getValue(){ 191 | return textContent; 192 | } 193 | } 194 | // TagName对象继承By用于WebView操作中的按照TagName查找WebElement 195 | static class TagName extends By { 196 | private final String tagName; 197 | 198 | public TagName(String tagName){ 199 | this.tagName = tagName; 200 | } 201 | 202 | @Override 203 | public String getValue(){ 204 | return tagName; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /com/robotium/solo/Checker.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import android.widget.CheckedTextView; 5 | import android.widget.CompoundButton; 6 | import android.widget.Spinner; 7 | import android.widget.TextView; 8 | 9 | 10 | /** 11 | * Check类View检查工具类,提供各种信息检查用 12 | * Contains various check methods. Examples are: isButtonChecked(), 13 | * isSpinnerTextSelected. 14 | * 15 | * @author Renas Reda, renas.reda@robotium.com 16 | * 17 | */ 18 | 19 | class Checker { 20 | // view获取工具类 21 | private final ViewFetcher viewFetcher; 22 | // wait等待工具类,用于获取各类 View和判断text等内容是否出现 23 | private final Waiter waiter; 24 | 25 | /** 26 | * 构造函数 27 | * Constructs this object. 28 | * 29 | * @param viewFetcher the {@code ViewFetcher} instance 30 | * @param waiter the {@code Waiter} instance 31 | */ 32 | 33 | public Checker(ViewFetcher viewFetcher, Waiter waiter){ 34 | this.viewFetcher = viewFetcher; 35 | this.waiter = waiter; 36 | } 37 | 38 | 39 | /** 40 | * 获取指定类型的第index个CompoundButton对象,检查是否被选中了.CompoundButton有选中和未选中2种状态 41 | * Checks if a {@link CompoundButton} with a given index is checked. 42 | * 43 | * @param expectedClass the expected class, e.g. {@code CheckBox.class} or {@code RadioButton.class} 44 | * @param index of the {@code CompoundButton} to check. {@code 0} if only one is available 45 | * @return {@code true} if {@code CompoundButton} is checked and {@code false} if it is not checked 46 | */ 47 | 48 | public boolean isButtonChecked(Class expectedClass, int index) 49 | { 50 | // 调用waiter的方法获取指定条件的view 51 | return (waiter.waitForAndGetView(index, expectedClass).isChecked()); 52 | } 53 | 54 | /** 55 | * 获取知道类型的指定text的CompoundButton类型控件,取第一个,检查是否被选中,如果没有找到指定条件的控件,那么也返回false 56 | * Checks if a {@link CompoundButton} with a given text is checked. 57 | * 58 | * @param expectedClass the expected class, e.g. {@code CheckBox.class} or {@code RadioButton.class} 59 | * @param text the text that is expected to be checked 60 | * @return {@code true} if {@code CompoundButton} is checked and {@code false} if it is not checked 61 | */ 62 | 63 | public boolean isButtonChecked(Class expectedClass, String text) 64 | { 65 | // 按照给定条件查找View,可拖动刷新查找 66 | T button = waiter.waitForText(expectedClass, text, 0, Timeout.getSmallTimeout(), true); 67 | // 检查是否找到且被选中 68 | if(button != null && button.isChecked()){ 69 | return true; 70 | } 71 | return false; 72 | } 73 | 74 | /** 75 | * 查找指定text的第一个CheckedTextView类型控件,检查是否被选中,选中返回true,未选中返回false.如果未找到也返回false. 76 | * Checks if a {@link CheckedTextView} with a given text is checked. 77 | * 78 | * @param checkedTextView the {@code CheckedTextView} object 79 | * @param text the text that is expected to be checked 80 | * @return {@code true} if {@code CheckedTextView} is checked and {@code false} if it is not checked 81 | */ 82 | 83 | public boolean isCheckedTextChecked(String text) 84 | { 85 | // 按照指定条件查找View 86 | CheckedTextView checkedTextView = waiter.waitForText(CheckedTextView.class, text, 0, Timeout.getSmallTimeout(), true); 87 | // 检查是否找到且被选中 88 | if(checkedTextView != null && checkedTextView.isChecked()) { 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | 95 | /** 96 | * 查找指定text的所有Spinner类型控件,检查是否有被选中,有选中返回true,未选中返回false.如果未找到也返回false. 97 | * Checks if a given text is selected in any {@link Spinner} located on the current screen. 98 | * 99 | * @param text the text that is expected to be selected 100 | * @return {@code true} if the given text is selected in any {@code Spinner} and false if it is not 101 | */ 102 | 103 | public boolean isSpinnerTextSelected(String text) 104 | { 105 | // 刷新一次页面 106 | waiter.waitForAndGetView(0, Spinner.class); 107 | // 查找指定text的 Spinner 108 | ArrayList spinnerList = viewFetcher.getCurrentViews(Spinner.class); 109 | // 遍历检查其中是否有符合条件的 110 | for(int i = 0; i < spinnerList.size(); i++){ 111 | if(isSpinnerTextSelected(i, text)) 112 | return true; 113 | } 114 | return false; 115 | } 116 | 117 | /** 118 | * 查找指定text的第spinnerIndex个Spinner类型控件,检查是否有被选中,有选中返回true,未选中返回false.如果未找到也返回false. 119 | * 因该方法内未作null判断,因此有导致nullpoint异常的可能 120 | * Checks if a given text is selected in a given {@link Spinner} 121 | * @param spinnerIndex the index of the spinner to check. 0 if only one spinner is available 122 | * @param text the text that is expected to be selected 123 | * @return true if the given text is selected in the given {@code Spinner} and false if it is not 124 | */ 125 | 126 | public boolean isSpinnerTextSelected(int spinnerIndex, String text) 127 | { 128 | // 获取指定的Spinner 129 | Spinner spinner = waiter.waitForAndGetView(spinnerIndex, Spinner.class); 130 | // 未检查获取的是否是null,如无Spinner类型,可导致nullpoint异常 131 | // 获取Spinner当前被选中的text 132 | TextView textView = (TextView) spinner.getChildAt(0); 133 | // 检查是否为指定的 134 | if(textView.getText().equals(text)) 135 | return true; 136 | else 137 | return false; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /com/robotium/solo/Condition.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | /** 4 | * 判断条件接口 5 | * Represents a conditional statement.
6 | * Implementations may be used with {@link Solo#waitForCondition(Condition, int)}. 7 | */ 8 | public interface Condition { 9 | 10 | /** 11 | * 判定条件满足返回true,不满足返回false 12 | * Should do the necessary work needed to check a condition and then return whether this condition is satisfied or not. 13 | * @return {@code true} if condition is satisfied and {@code false} if it is not satisfied 14 | */ 15 | public boolean isSatisfied(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /com/robotium/solo/DialogUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.os.SystemClock; 7 | import android.view.ContextThemeWrapper; 8 | import android.view.View; 9 | import android.view.inputmethod.InputMethodManager; 10 | import android.widget.EditText; 11 | 12 | 13 | /** 14 | * 弹框处理工具类 15 | * Contains the waitForDialogToClose() method. 16 | * 17 | * @author Renas Reda, renas.reda@robotium.com 18 | * 19 | */ 20 | 21 | class DialogUtils { 22 | // activity操作工具类 23 | private final ActivityUtils activityUtils; 24 | // view获取工具类 25 | private final ViewFetcher viewFetcher; 26 | // 等待工具类 27 | private final Sleeper sleeper; 28 | // 1s 29 | private final static int TIMEOUT_DIALOG_TO_CLOSE = 1000; 30 | // 200ms 31 | private final int MINISLEEP = 200; 32 | 33 | /** 34 | * 构造函数 35 | * Constructs this object. 36 | * 37 | * @param activityUtils the {@code ActivityUtils} instance 38 | * @param viewFetcher the {@code ViewFetcher} instance 39 | * @param sleeper the {@code Sleeper} instance 40 | */ 41 | 42 | public DialogUtils(ActivityUtils activityUtils, ViewFetcher viewFetcher, Sleeper sleeper) { 43 | this.activityUtils = activityUtils; 44 | this.viewFetcher = viewFetcher; 45 | this.sleeper = sleeper; 46 | } 47 | 48 | 49 | /** 50 | * 检查在指定时间内弹框是否关闭了. 51 | * Waits for a {@link android.app.Dialog} to close. 52 | * 53 | * @param timeout the amount of time in milliseconds to wait 54 | * @return {@code true} if the {@code Dialog} is closed before the timeout and {@code false} if it is not closed 55 | */ 56 | 57 | public boolean waitForDialogToClose(long timeout) { 58 | // 先等待弹框出现 59 | waitForDialogToOpen(TIMEOUT_DIALOG_TO_CLOSE, false); 60 | // 设置超时时间 61 | final long endTime = SystemClock.uptimeMillis() + timeout; 62 | // 循环检查弹框是否关闭了 63 | while (SystemClock.uptimeMillis() < endTime) { 64 | 65 | if(!isDialogOpen()){ 66 | return true; 67 | } 68 | // 等待200ms 69 | sleeper.sleep(MINISLEEP); 70 | } 71 | return false; 72 | } 73 | 74 | 75 | 76 | /** 77 | * 检查指定时间内,是否有弹框出现, 78 | * timeout 设置的指定超时时间,单位 ms 79 | * sleepFirst 是否需要先等待500ms,再做检查 80 | * Waits for a {@link android.app.Dialog} to open. 81 | * 82 | * @param timeout the amount of time in milliseconds to wait 83 | * @return {@code true} if the {@code Dialog} is opened before the timeout and {@code false} if it is not opened 84 | */ 85 | 86 | public boolean waitForDialogToOpen(long timeout, boolean sleepFirst) { 87 | // 设置超时时间 88 | final long endTime = SystemClock.uptimeMillis() + timeout; 89 | // 是否需要等待500ms后再查找 90 | if(sleepFirst) 91 | sleeper.sleep(); 92 | // 循环检查是否弹框出现了 93 | while (SystemClock.uptimeMillis() < endTime) { 94 | 95 | if(isDialogOpen()){ 96 | return true; 97 | } 98 | // 等待300ms 99 | sleeper.sleepMini(); 100 | } 101 | return false; 102 | } 103 | 104 | /** 105 | * 检查是否有弹框出现 106 | * Checks if a dialog is open. 107 | * 108 | * @return true if dialog is open 109 | */ 110 | 111 | private boolean isDialogOpen(){ 112 | // 获取当前显示的activity 113 | final Activity activity = activityUtils.getCurrentActivity(false); 114 | // 获取当前的所有DecorView类型View 115 | final View[] views = viewFetcher.getWindowDecorViews(); 116 | // 获取最新的DecorView,DecorView是根 117 | View view = viewFetcher.getRecentDecorView(views); 118 | // 遍历检查是否有打开的弹框 119 | if(!isDialog(activity, view)){ 120 | for(View v : views){ 121 | if(isDialog(activity, v)){ 122 | return true; 123 | } 124 | } 125 | } 126 | else { 127 | return true; 128 | } 129 | return false; 130 | } 131 | 132 | /** 133 | * 判断decorView是否是给定activity的,即检查弹框是否是当前activity的 134 | * Checks that the specified DecorView and the Activity DecorView are not equal. 135 | * 136 | * @param activity the activity which DecorView is to be compared 137 | * @param decorView the DecorView to compare 138 | * @return true if not equal 139 | */ 140 | 141 | private boolean isDialog(Activity activity, View decorView){ 142 | // 检查decorView是都可见的,不可见直接返回false 143 | if(decorView == null || !decorView.isShown()){ 144 | return false; 145 | } 146 | // 获取Context 147 | Context viewContext = null; 148 | if(decorView != null){ 149 | viewContext = decorView.getContext(); 150 | } 151 | // 获取需要的Context 152 | if (viewContext instanceof ContextThemeWrapper) { 153 | ContextThemeWrapper ctw = (ContextThemeWrapper) viewContext; 154 | viewContext = ctw.getBaseContext(); 155 | } 156 | // 获取activity对应的Context 157 | Context activityContext = activity; 158 | Context activityBaseContext = activity.getBaseContext(); 159 | // 检查Context 是否是一致的,并且 activity不是在弹框中的 160 | return (activityContext.equals(viewContext) || activityBaseContext.equals(viewContext)) && (decorView != activity.getWindow().getDecorView()); 161 | } 162 | 163 | /** 164 | * 隐藏软键盘 165 | * editText 指定的编辑框 166 | * shouldSleepFirst 是否要先等待500ms再操作 167 | * shouldSleepAfter 执行完后是否要等待500ms再返回,仅对传入editText非null有效 168 | * Hides the soft keyboard 169 | * 170 | * @param shouldSleepFirst whether to sleep a default pause first 171 | * @param shouldSleepAfter whether to sleep a default pause after 172 | */ 173 | 174 | public void hideSoftKeyboard(EditText editText, boolean shouldSleepFirst, boolean shouldSleepAfter) { 175 | // 获取当前activity 176 | Activity activity = activityUtils.getCurrentActivity(shouldSleepFirst); 177 | // 获取输入控制管理器服务 178 | InputMethodManager inputMethodManager = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); 179 | // 调用隐藏软键盘方法 180 | if(editText != null) { 181 | inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0); 182 | return; 183 | } 184 | // 如果没有指定editText,获取当前焦点所在的View 185 | View focusedView = activity.getCurrentFocus(); 186 | // 如果获取的 View不是EditText 187 | if(!(focusedView instanceof EditText)) { 188 | // 获取当前页面的最新 EditText 189 | EditText freshestEditText = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(EditText.class)); 190 | // 如果可以取到EditText那么设置可用的 191 | if(freshestEditText != null){ 192 | focusedView = freshestEditText; 193 | } 194 | } 195 | // 隐藏软键盘 196 | if(focusedView != null) { 197 | inputMethodManager.hideSoftInputFromWindow(focusedView.getWindowToken(), 0); 198 | } 199 | // 如果设置了等待,那么等待500ms后返回 200 | if(shouldSleepAfter){ 201 | sleeper.sleep(); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /com/robotium/solo/GLRenderWrapper.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.nio.IntBuffer; 4 | import java.util.concurrent.CountDownLatch; 5 | import javax.microedition.khronos.egl.EGLConfig; 6 | import javax.microedition.khronos.opengles.GL10; 7 | import android.graphics.Bitmap; 8 | import android.opengl.GLES20; 9 | import android.opengl.GLSurfaceView; 10 | import android.opengl.GLSurfaceView.Renderer; 11 | import android.view.View; 12 | 13 | /** 14 | * 构造定制化的renderer做页面渲染,用作截屏使用 15 | * Used to wrap and replace the renderer to gain access to the gl context. 16 | * 17 | * @author Per-Erik Bergman, bergman@uncle.se 18 | * 19 | */ 20 | 21 | class GLRenderWrapper implements Renderer { 22 | // 变量缓存渲染器,用于存储系统原有的渲染器 23 | private Renderer renderer; 24 | // 宽度 25 | private int width; 26 | // 高度 27 | private int height; 28 | // 图形操作接口 29 | private final GLSurfaceView view; 30 | // 原子计数器,同步线程操作 31 | private CountDownLatch latch; 32 | // 设置是否要截屏 33 | private boolean takeScreenshot = true; 34 | // 获取GL版本 35 | private int glVersion; 36 | 37 | /** 38 | * 构造函数 39 | * Constructs this object. 40 | * 41 | * @param view the current glSurfaceView 42 | * @param renderer the renderer to wrap 43 | * @param latch the count down latch 44 | */ 45 | 46 | public GLRenderWrapper(GLSurfaceView view, 47 | Renderer renderer, CountDownLatch latch) { 48 | this.view = view; 49 | this.renderer = renderer; 50 | this.latch = latch; 51 | // 设置宽度 52 | this.width = view.getWidth(); 53 | // 设置高度 54 | this.height = view.getHeight(); 55 | // 通过反射获取GL版本信息 56 | Integer out = new Reflect(view).field("mEGLContextClientVersion") 57 | .out(Integer.class); 58 | // 可以获取GL版本,那么设置对应的版本,否则设置版本为-1,设置截图操作为false 59 | if ( out != null ) { 60 | this.glVersion = out.intValue(); 61 | } else { 62 | this.glVersion = -1; 63 | this.takeScreenshot = false; 64 | } 65 | } 66 | 67 | @Override 68 | /* 不修改,调用默认实现 69 | * (non-Javadoc) 70 | * @see android.opengl.GLSurfaceView.Renderer#onSurfaceCreated(javax.microedition.khronos.opengles.GL10, javax.microedition.khronos.egl.EGLConfig) 71 | */ 72 | 73 | public void onSurfaceCreated(GL10 gl, EGLConfig config) { 74 | renderer.onSurfaceCreated(gl, config); 75 | } 76 | 77 | @Override 78 | /* 获取相关的高度和宽度,调用继续使用原有的 79 | * (non-Javadoc) 80 | * @see android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(javax.microedition.khronos.opengles.GL10, int, int) 81 | */ 82 | 83 | public void onSurfaceChanged(GL10 gl, int width, int height) { 84 | this.width = width; 85 | this.height = height; 86 | renderer.onSurfaceChanged(gl, width, height); 87 | } 88 | 89 | @Override 90 | /* 修改绘图内容渲染完成后保存渲染的界面内容 91 | * (non-Javadoc) 92 | * @see android.opengl.GLSurfaceView.Renderer#onDrawFrame(javax.microedition.khronos.opengles.GL10) 93 | */ 94 | 95 | public void onDrawFrame(GL10 gl) { 96 | // 调用原有的绘图渲染 97 | renderer.onDrawFrame(gl); 98 | // 如果设置了截图 99 | if (takeScreenshot) { 100 | // 图片缓存变量 101 | Bitmap screenshot = null; 102 | // 按照 GL版本,调用对应的图片处理方法 103 | if (glVersion >= 2) { 104 | screenshot = savePixels(0, 0, width, height); 105 | } else { 106 | screenshot = savePixels(0, 0, width, height, gl); 107 | } 108 | // 处理图片,把图片内容返回给对应view对象 109 | new Reflect(view).field("mDrawingCache").type(View.class) 110 | .in(screenshot); 111 | // 释放锁对象 112 | latch.countDown(); 113 | // 标志为未截图 114 | takeScreenshot = false; 115 | } 116 | } 117 | 118 | /** 119 | * 设置是否需要截图 120 | * Tell the wrapper to take a screen shot 121 | */ 122 | 123 | public void setTakeScreenshot() { 124 | takeScreenshot = true; 125 | } 126 | 127 | /** 128 | * 设置计数器 129 | * Set the count down latch 130 | */ 131 | 132 | public void setLatch(CountDownLatch latch) { 133 | this.latch = latch; 134 | } 135 | 136 | /** 137 | * 获取图像保存为bitmap 138 | * Extract the bitmap from OpenGL 139 | * 140 | * @param x the start column 141 | * @param y the start line 142 | * @param w the width of the bitmap 143 | * @param h the height of the bitmap 144 | */ 145 | 146 | private Bitmap savePixels(int x, int y, int w, int h) { 147 | // 存储图片内容 148 | int b[] = new int[w * (y + h)]; 149 | int bt[] = new int[w * h]; 150 | IntBuffer ib = IntBuffer.wrap(b); 151 | ib.position(0); 152 | // 处理图片 153 | GLES20.glReadPixels(x, 0, w, y + h, GLES20.GL_RGBA, 154 | GLES20.GL_UNSIGNED_BYTE, ib); 155 | // 处理成指定的高度和宽度 156 | for (int i = 0, k = 0; i < h; i++, k++) { 157 | // remember, that OpenGL bitmap is incompatible with Android bitmap 158 | // and so, some correction need. 159 | for (int j = 0; j < w; j++) { 160 | int pix = b[i * w + j]; 161 | int pb = (pix >> 16) & 0xff; 162 | int pr = (pix << 16) & 0x00ff0000; 163 | int pix1 = (pix & 0xff00ff00) | pr | pb; 164 | bt[(h - k - 1) * w + j] = pix1; 165 | } 166 | } 167 | // 处理成BitMap 168 | Bitmap sb = Bitmap.createBitmap(bt, w, h, Bitmap.Config.ARGB_8888); 169 | return sb; 170 | } 171 | 172 | /** 173 | * 处理图片 174 | * Extract the bitmap from OpenGL 175 | * 176 | * @param x the start column 177 | * @param y the start line 178 | * @param w the width of the bitmap 179 | * @param h the height of the bitmap 180 | * @param gl the current GL reference 181 | */ 182 | 183 | private static Bitmap savePixels(int x, int y, int w, int h, GL10 gl) { 184 | // 缓存像素内容 185 | int b[] = new int[w * (y + h)]; 186 | int bt[] = new int[w * h]; 187 | IntBuffer ib = IntBuffer.wrap(b); 188 | ib.position(0); 189 | // 处理图片 190 | gl.glReadPixels(x, 0, w, y + h, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, ib); 191 | // 处理成指定的高度和宽度 192 | for (int i = 0, k = 0; i < h; i++, k++) { 193 | // remember, that OpenGL bitmap is incompatible with Android bitmap 194 | // and so, some correction need. 195 | for (int j = 0; j < w; j++) { 196 | int pix = b[i * w + j]; 197 | int pb = (pix >> 16) & 0xff; 198 | int pr = (pix << 16) & 0x00ff0000; 199 | int pix1 = (pix & 0xff00ff00) | pr | pb; 200 | bt[(h - k - 1) * w + j] = pix1; 201 | } 202 | } 203 | // 处理成Bitmap 204 | Bitmap sb = Bitmap.createBitmap(bt, w, h, Bitmap.Config.ARGB_8888); 205 | return sb; 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /com/robotium/solo/Getter.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import junit.framework.Assert; 4 | import android.app.Activity; 5 | import android.app.Instrumentation; 6 | import android.content.Context; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | 10 | 11 | /** 12 | * 按照指定条件获取View或者其他的一些信息 13 | * Contains various get methods. Examples are: getView(int id), 14 | * getView(Class classToFilterBy, int index). 15 | * 16 | * @author Renas Reda, renas.reda@robotium.com 17 | * 18 | */ 19 | 20 | class Getter { 21 | // Instrument,用于发送事件 22 | private final Instrumentation instrumentation; 23 | // activity工具类 24 | private final ActivityUtils activityUtils; 25 | // View等待工具类 26 | private final Waiter waiter; 27 | // 1s 28 | private final int TIMEOUT = 1000; 29 | 30 | /** 31 | * 构造函数 32 | * Constructs this object. 33 | * 34 | * @param inst the {@code Instrumentation} instance 35 | * @param viewFetcher the {@code ViewFetcher} instance 36 | * @param waiter the {@code Waiter} instance 37 | */ 38 | 39 | public Getter(Instrumentation instrumentation, ActivityUtils activityUtils, Waiter waiter){ 40 | this.instrumentation = instrumentation; 41 | this.activityUtils = activityUtils; 42 | this.waiter = waiter; 43 | } 44 | 45 | 46 | /** 47 | * 获取指定类型的第index个View,找不到返回null 48 | * Returns a {@code View} with a certain index, from the list of current {@code View}s of the specified type. 49 | * 50 | * @param classToFilterBy which {@code View}s to choose from 51 | * @param index choose among all instances of this type, e.g. {@code Button.class} or {@code EditText.class} 52 | * @return a {@code View} with a certain index, from the list of current {@code View}s of the specified type 53 | */ 54 | 55 | public T getView(Class classToFilterBy, int index) { 56 | //获取指定class类型的第index个View,默认超时10s,10s内未找到返回null 57 | return waiter.waitForAndGetView(index, classToFilterBy); 58 | } 59 | 60 | /** 61 | * 获取指定class类型和text的第1个view,找不到提示异常.可设置是否只查找可见的 62 | * onlyVisible true 只找可见的,false 查找所有的 63 | * Returns a {@code View} that shows a given text, from the list of current {@code View}s of the specified type. 64 | * 65 | * @param classToFilterBy which {@code View}s to choose from 66 | * @param text the text that the view shows 67 | * @param onlyVisible {@code true} if only visible texts on the screen should be returned 68 | * @return a {@code View} showing a given text, from the list of current {@code View}s of the specified type 69 | */ 70 | 71 | public T getView(Class classToFilterBy, String text, boolean onlyVisible) { 72 | // 获取指定class类型和text的第1个view,默认短超时 73 | T viewToReturn = (T) waiter.waitForText(classToFilterBy, text, 0, Timeout.getSmallTimeout(), false, onlyVisible, false); 74 | // 未找到提示异常 75 | if(viewToReturn == null) 76 | Assert.fail(classToFilterBy.getSimpleName() + " with text: '" + text + "' is not found!"); 77 | 78 | return viewToReturn; 79 | } 80 | 81 | /** 82 | * 按照指定资源id,获取当前activity中的 String 83 | * Returns a localized string 84 | * 85 | * @param id the resource ID for the string 86 | * @return the localized string 87 | */ 88 | 89 | public String getString(int id) 90 | { 91 | // 获取当前activity 92 | Activity activity = activityUtils.getCurrentActivity(false); 93 | // 返回id对应对的string 94 | return activity.getString(id); 95 | } 96 | 97 | /** 98 | * 按照指定资源id,获取当前activity的String. 99 | * 100 | * Returns a localized string 101 | * 102 | * @param id the resource ID for the string 103 | * @return the localized string 104 | */ 105 | 106 | public String getString(String id) 107 | { 108 | // 将String类型的标识解析成对应的Int型Id再从当前activity查找对应的String 109 | // 获取 Context 110 | Context targetContext = instrumentation.getTargetContext(); 111 | // 获取应用名 112 | String packageName = targetContext.getPackageName(); 113 | // 按照String类型id查询对应的int id,现在当前应用中查,找不到,整个android中查 114 | int viewId = targetContext.getResources().getIdentifier(id, "string", packageName); 115 | if(viewId == 0){ 116 | viewId = targetContext.getResources().getIdentifier(id, "string", "android"); 117 | } 118 | // 按照指定资源id,获取当前activity中的 String 119 | return getString(viewId); 120 | } 121 | 122 | /** 123 | * 获取指定id的第index个 View,如设置的index小于1,那么返回当前activity中id为 0的view. 124 | * 可设置超时时间,如果超时时间设置为0,则默认改成10s,设置为负值则直接返回null 125 | * 126 | * Returns a {@code View} with a given id. 127 | * 128 | * @param id the R.id of the {@code View} to be returned 129 | * @param index the index of the {@link View}. {@code 0} if only one is available 130 | * @param timeout the timeout in milliseconds 131 | * @return a {@code View} with a given id 132 | */ 133 | 134 | public View getView(int id, int index, int timeout){ 135 | // 获取当前activity 136 | final Activity activity = activityUtils.getCurrentActivity(false); 137 | View viewToReturn = null; 138 | // 传入index 小于1,默认返回当前activity中的id为0的view 139 | if(index < 1){ 140 | index = 0; 141 | viewToReturn = activity.findViewById(id); 142 | } 143 | // 如果找到了,则返回,这块代码可以嵌入上面的if块中,较好理解 144 | if (viewToReturn != null) { 145 | return viewToReturn; 146 | } 147 | // 获取指定id,指定数量的view出现,可设置超时时间,如果超时时间设置为0,则默认改成10s,设置为负值则直接返回null 148 | return waiter.waitForView(id, index, timeout); 149 | } 150 | 151 | /** 152 | * 获取指定id的第index个 View,如设置的index小于1,那么返回当前activity中id为 0的view.默认超时10s 153 | * Returns a {@code View} with a given id. 154 | * 155 | * @param id the R.id of the {@code View} to be returned 156 | * @param index the index of the {@link View}. {@code 0} if only one is available 157 | * @return a {@code View} with a given id 158 | */ 159 | 160 | public View getView(int id, int index){ 161 | return getView(id, index, 0); 162 | } 163 | 164 | /** 165 | * 获取指定id的第index个 View,如设置的index小于1,那么返回当前activity中id为 0的view.默认超时1s 166 | * 将String类型的标识解析成对应的Int型Id再从当前activity查找对应的View 167 | * 168 | * Returns a {@code View} with a given id. 169 | * 170 | * @param id the id of the {@link View} to return 171 | * @param index the index of the {@link View}. {@code 0} if only one is available 172 | * @return a {@code View} with a given id 173 | */ 174 | 175 | public View getView(String id, int index){ 176 | // 将String类型的标识解析成对应的Int型Id 177 | View viewToReturn = null; 178 | // 获取应用上下文 179 | Context targetContext = instrumentation.getTargetContext(); 180 | // 获取应用名 181 | String packageName = targetContext.getPackageName(); 182 | // 按照String类型id查询对应的int id,现在当前应用中查 183 | int viewId = targetContext.getResources().getIdentifier(id, "id", packageName); 184 | // 查询对应的view 185 | if(viewId != 0){ 186 | viewToReturn = getView(viewId, index, TIMEOUT); 187 | } 188 | // 如果未找到,将id解析成android对应的继续查找 189 | if(viewToReturn == null){ 190 | int androidViewId = targetContext.getResources().getIdentifier(id, "id", "android"); 191 | // 如果可以获取对应的id 继续查找 192 | if(androidViewId != 0){ 193 | viewToReturn = getView(androidViewId, index, TIMEOUT); 194 | } 195 | } 196 | // 找到则直接返回 197 | if(viewToReturn != null){ 198 | return viewToReturn; 199 | } 200 | // 未找到则设置id 为0继续查找 201 | return getView(viewId, index); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /com/robotium/solo/Presser.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.widget.EditText; 4 | import android.widget.Spinner; 5 | import junit.framework.Assert; 6 | import android.app.Instrumentation; 7 | import android.view.KeyEvent; 8 | import android.view.inputmethod.EditorInfo; 9 | 10 | /** 11 | * 按操作工具类 12 | * Contains press methods. Examples are pressMenuItem(), 13 | * pressSpinnerItem(). 14 | * 15 | * @author Renas Reda, renas.reda@robotium.com 16 | * 17 | */ 18 | 19 | class Presser{ 20 | // 点击操作工具类 21 | private final Clicker clicker; 22 | // Instrument 用于发送事件 23 | private final Instrumentation inst; 24 | // 等待工具类 25 | private final Sleeper sleeper; 26 | // View等待工具类 27 | private final Waiter waiter; 28 | // 弹框处理工具类 29 | private final DialogUtils dialogUtils; 30 | // View获取工具类 31 | private final ViewFetcher viewFetcher; 32 | 33 | 34 | /** 35 | * 构造函数 36 | * Constructs this object. 37 | * 38 | * @param viewFetcher the {@code ViewFetcher} instance 39 | * @param clicker the {@code Clicker} instance 40 | * @param inst the {@code Instrumentation} instance 41 | * @param sleeper the {@code Sleeper} instance 42 | * @param waiter the {@code Waiter} instance 43 | * @param dialogUtils the {@code DialogUtils} instance 44 | */ 45 | 46 | public Presser(ViewFetcher viewFetcher, Clicker clicker, Instrumentation inst, Sleeper sleeper, Waiter waiter, DialogUtils dialogUtils) { 47 | this.viewFetcher = viewFetcher; 48 | this.clicker = clicker; 49 | this.inst = inst; 50 | this.sleeper = sleeper; 51 | this.waiter = waiter; 52 | this.dialogUtils = dialogUtils; 53 | } 54 | 55 | 56 | /** 57 | * 点击Menu中的第n个Item,Item从左往右从上到下,按顺序排列,默认每行包含3个Item 58 | * Presses a {@link android.view.MenuItem} with a given index. Index {@code 0} is the first item in the 59 | * first row, Index {@code 3} is the first item in the second row and 60 | * index {@code 5} is the first item in the third row. 61 | * 62 | * @param index the index of the {@code MenuItem} to be pressed 63 | */ 64 | 65 | public void pressMenuItem(int index){ 66 | // 设置每行3个 Item 67 | pressMenuItem(index, 3); 68 | } 69 | 70 | /** 71 | * 点击Menu中的第n个Item,Item从左往右从上到下,按顺序排列, 72 | * index 需要点击的第n个Item,从1开始 73 | * itemsPerRow 每一行包含的Item数量 74 | * Presses a {@link android.view.MenuItem} with a given index. Supports three rows with a given amount 75 | * of items. If itemsPerRow equals 5 then index 0 is the first item in the first row, 76 | * index 5 is the first item in the second row and index 10 is the first item in the third row. 77 | * 78 | * @param index the index of the {@code MenuItem} to be pressed 79 | * @param itemsPerRow the amount of menu items there are per row. 80 | */ 81 | 82 | public void pressMenuItem(int index, int itemsPerRow) { 83 | // Item缓存,存储4行,每行的开头序号 84 | int[] row = new int[4]; 85 | // 初始化Item id 信息 86 | for(int i = 1; i <=3; i++) 87 | row[i] = itemsPerRow*i; 88 | // 等待500ms 89 | sleeper.sleep(); 90 | try{ 91 | // 点击Menu按钮 92 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_MENU); 93 | // 等待Menu出现 94 | dialogUtils.waitForDialogToOpen(Timeout.getSmallTimeout(), true); 95 | // 点击2次上方向键.Item位置回到第一个 96 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP); 97 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP); 98 | }catch(SecurityException e){ 99 | Assert.fail("Can not press the menu!"); 100 | } 101 | // 如果指定Item在第一行,则在第一行移动,往右移动,移动到指定的Item 102 | if (index < row[1]) { 103 | for (int i = 0; i < index; i++) { 104 | sleeper.sleepMini(); 105 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT); 106 | } 107 | // 在第二行 108 | } else if (index >= row[1] && index < row[2]) { 109 | // 下移到下一行,即第二行 110 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 111 | // 移动到指定的Item 112 | for (int i = row[1]; i < index; i++) { 113 | sleeper.sleepMini(); 114 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT); 115 | } 116 | // 在第三行,或者之后的行 117 | } else if (index >= row[2]) { 118 | // 移动到第三行 119 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 120 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 121 | // 移动到指定的Item 122 | for (int i = row[2]; i < index; i++) { 123 | sleeper.sleepMini(); 124 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT); 125 | } 126 | } 127 | 128 | try{ 129 | // 点击确认 130 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_ENTER); 131 | }catch (SecurityException ignored) {} 132 | } 133 | 134 | /** 135 | * 点击软件盘当前按键的下一个按键 136 | * Presses the soft keyboard next button. 137 | */ 138 | 139 | public void pressSoftKeyboardNextButton(){ 140 | // 获取一个EditText.只有EditText才有软键盘 141 | final EditText freshestEditText = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(EditText.class)); 142 | // 可以获取EditText 143 | if(freshestEditText != null){ 144 | inst.runOnMainSync(new Runnable() 145 | { 146 | public void run() 147 | { 148 | // 点击当前按钮位置的下一个按钮 149 | freshestEditText.onEditorAction(EditorInfo.IME_ACTION_NEXT); 150 | } 151 | }); 152 | } 153 | } 154 | 155 | /** 156 | * 点击第spinnerIndex个 Spinner的第itemIndex个Item 157 | * spinnerIndex 指定的Spinner顺序 158 | * itemIndex 指定的Item顺序,如果是正值,那么往下移动,负值往上移动 159 | * Presses on a {@link android.widget.Spinner} (drop-down menu) item. 160 | * 161 | * @param spinnerIndex the index of the {@code Spinner} menu to be used 162 | * @param itemIndex the index of the {@code Spinner} item to be pressed relative to the currently selected item. 163 | * A Negative number moves up on the {@code Spinner}, positive moves down 164 | */ 165 | 166 | public void pressSpinnerItem(int spinnerIndex, int itemIndex) 167 | { 168 | // 点击下来列表 169 | clicker.clickOnScreen(waiter.waitForAndGetView(spinnerIndex, Spinner.class)); 170 | // 等待下拉列表出现 171 | dialogUtils.waitForDialogToOpen(Timeout.getSmallTimeout(), true); 172 | 173 | try{ 174 | // 发送事件,初始化位置,最下面 175 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 176 | }catch(SecurityException ignored){} 177 | // 如果指定的itemIndex为负值,那么往上移动 178 | boolean countingUp = true; 179 | if(itemIndex < 0){ 180 | countingUp = false; 181 | itemIndex *= -1; 182 | } 183 | // 按照指定的顺序,移动 Item到对应的位置 184 | for(int i = 0; i < itemIndex; i++) 185 | { 186 | sleeper.sleepMini(); 187 | // 向下 188 | if(countingUp){ 189 | try{ 190 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 191 | }catch(SecurityException ignored){} 192 | // 向下 193 | }else{ 194 | try{ 195 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP); 196 | }catch(SecurityException ignored){} 197 | } 198 | } 199 | // 点击确认按钮 200 | try{ 201 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_ENTER); 202 | }catch(SecurityException ignored){} 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /com/robotium/solo/Reflect.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | /** 6 | * 反射操作工具类 7 | * A reflection utility class. 8 | * 9 | * @author Per-Erik Bergman, bergman@uncle.se 10 | * 11 | */ 12 | 13 | class Reflect { 14 | private Object object; 15 | 16 | /** 17 | * 构造函数,禁止传入空值 18 | * Constructs this object 19 | * 20 | * @param object the object to reflect on 21 | */ 22 | 23 | public Reflect(Object object) { 24 | // 传入空值报异常 25 | if (object == null) 26 | throw new IllegalArgumentException("Object can not be null."); 27 | this.object = object; 28 | } 29 | 30 | /** 31 | * 获取对应属性字段 32 | * Get a field from the object 33 | * 34 | * @param name the name of the field 35 | * 36 | * @return a field reference 37 | */ 38 | 39 | public FieldRf field(String name) { 40 | return new FieldRf(object, name); 41 | } 42 | 43 | /** 44 | * 定义一个字段属性类 45 | * A field reference. 46 | */ 47 | public class FieldRf { 48 | // 对应的 Class 49 | private Class clazz; 50 | // 获取字段属性的对象 51 | private Object object; 52 | // 属性名 53 | private String name; 54 | 55 | /**构造函数 56 | * 57 | * Constructs this object 58 | * 59 | * @param object the object to reflect on 60 | * @param name the name of the field 61 | */ 62 | 63 | public FieldRf(Object object, String name) { 64 | this.object = object; 65 | this.name = name; 66 | } 67 | 68 | /** 69 | * 构造指定class类型的对象 70 | * Constructs this object 71 | * 72 | * @param outclazz the output type 73 | * 74 | * @return T 75 | */ 76 | 77 | public T out(Class outclazz) { 78 | Field field = getField(); 79 | Object obj = getValue(field); 80 | return outclazz.cast(obj); 81 | } 82 | 83 | /** 84 | * 设置字段的值 85 | * Set a value to a field 86 | * 87 | * @param value the value to set 88 | */ 89 | 90 | public void in(Object value) { 91 | Field field = getField(); 92 | try { 93 | // 设置属性为传入的值 94 | field.set(object, value); 95 | // 无效参数异常 96 | } catch (IllegalArgumentException e) { 97 | e.printStackTrace(); 98 | // 权限异常 99 | } catch (IllegalAccessException e) { 100 | e.printStackTrace(); 101 | } 102 | } 103 | 104 | /** 105 | * 设置class类型,并返回对象本身 106 | * Set the class type 107 | * 108 | * @param clazz the type 109 | * 110 | * @return a field reference 111 | */ 112 | 113 | public FieldRf type(Class clazz) { 114 | this.clazz = clazz; 115 | return this; 116 | } 117 | 118 | // 获取字段 119 | private Field getField() { 120 | // 如未设置class类型,那么使用对象自身class作为class类型 121 | if (clazz == null) { 122 | clazz = object.getClass(); 123 | } 124 | // 获取name执行的属性字段 125 | Field field = null; 126 | try { 127 | field = clazz.getDeclaredField(name); 128 | // 字段属性设置为运行赋值 129 | field.setAccessible(true); 130 | } catch (NoSuchFieldException ignored) {} 131 | return field; 132 | } 133 | // 获取字段属性值 134 | private Object getValue(Field field) { 135 | // 如果字段为null那么返回null 136 | if (field == null) { 137 | return null; 138 | } 139 | // 获取字段对应的值,对象类型 140 | Object obj = null; 141 | try { 142 | obj = field.get(object); 143 | } catch (IllegalArgumentException e) { 144 | e.printStackTrace(); 145 | } catch (IllegalAccessException e) { 146 | e.printStackTrace(); 147 | } 148 | return obj; 149 | } 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /com/robotium/solo/RobotiumTextView.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.content.Context; 4 | import android.widget.TextView; 5 | 6 | /** 7 | * Robotium定制化TextView,可以用于WebElement对象的表示 8 | * Used to create a TextView object that is based on a web element. Contains the web element text and location. 9 | * 10 | * @author Renas Reda, renas.reda@robotium.com 11 | * 12 | */ 13 | 14 | class RobotiumTextView extends TextView { 15 | // 屏幕中对应的X坐标 16 | private int locationX = 0; 17 | // 屏幕中对应的Y坐标 18 | private int locationY = 0; 19 | 20 | /** 21 | * 构造函数 22 | * Constructs this object 23 | * 24 | * @param context the given context 25 | */ 26 | 27 | public RobotiumTextView(Context context){ 28 | super(context); 29 | } 30 | 31 | /** 32 | * 构造函数 33 | * Constructs this object 34 | * 35 | * @param context the given context 36 | * @param text the given text to be set 37 | */ 38 | 39 | public RobotiumTextView(Context context, String text, int locationX, int locationY) { 40 | super(context); 41 | this.setText(text); 42 | setLocationX(locationX); 43 | setLocationY(locationY); 44 | } 45 | 46 | /** 47 | * 获取控件对应的屏幕坐标 48 | * Returns the location on screen of the {@code TextView} that is based on a web element 49 | */ 50 | 51 | @Override 52 | public void getLocationOnScreen(int[] location) { 53 | 54 | location[0] = locationX; 55 | location[1] = locationY; 56 | } 57 | 58 | /** 59 | * 设置对应屏幕的X坐标 60 | * Sets the X location of the TextView 61 | * 62 | * @param locationX the X location of the {@code TextView} 63 | */ 64 | 65 | public void setLocationX(int locationX){ 66 | this.locationX = locationX; 67 | } 68 | 69 | 70 | /** 71 | * 设置对应屏幕的Y坐标 72 | * Sets the Y location 73 | * 74 | * @param locationY the Y location of the {@code TextView} 75 | */ 76 | 77 | public void setLocationY(int locationY){ 78 | this.locationY = locationY; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /com/robotium/solo/RobotiumUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | import java.util.regex.PatternSyntaxException; 10 | import android.view.View; 11 | import android.widget.TextView; 12 | 13 | /** 14 | * Robotium操作工具类 15 | * Contains utility methods. Examples are: removeInvisibleViews(Iterable viewList), 16 | * filterViews(Class classToFilterBy, Iterable viewList), sortViewsByLocationOnScreen(List views). 17 | * 18 | * @author Renas Reda, renas.reda@robotium.com 19 | * 20 | */ 21 | 22 | public class RobotiumUtils { 23 | 24 | 25 | /** 26 | * 移除给定列表中的分可见View,并返回剩余的 27 | * Removes invisible Views. 28 | * 29 | * @param viewList an Iterable with Views that is being checked for invisible Views 30 | * @return a filtered Iterable with no invisible Views 31 | */ 32 | 33 | public static ArrayList removeInvisibleViews(Iterable viewList) { 34 | ArrayList tmpViewList = new ArrayList(); 35 | for (T view : viewList) { 36 | // 可见的view加入返回List 37 | if (view != null && view.isShown()) { 38 | tmpViewList.add(view); 39 | } 40 | } 41 | return tmpViewList; 42 | } 43 | 44 | /** 45 | * 查找给定列表中的指定类型View,并返回 46 | * classToFilterBy 给定的View class 47 | * Filters Views based on the given class type. 48 | * 49 | * @param classToFilterBy the class to filter 50 | * @param viewList the Iterable to filter from 51 | * @return an ArrayList with filtered views 52 | */ 53 | 54 | public static ArrayList filterViews(Class classToFilterBy, Iterable viewList) { 55 | ArrayList filteredViews = new ArrayList(); 56 | for (Object view : viewList) { 57 | // 如果是指定的class类型,加入返回列表 58 | if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) { 59 | filteredViews.add(classToFilterBy.cast(view)); 60 | } 61 | } 62 | viewList = null; 63 | return filteredViews; 64 | } 65 | 66 | /** 67 | * 查找给定列表中的指定类型Views,并返回 68 | * classSet[] 指定的一组class 69 | * Filters all Views not within the given set. 70 | * 71 | * @param classSet contains all classes that are ok to pass the filter 72 | * @param viewList the Iterable to filter form 73 | * @return an ArrayList with filtered views 74 | */ 75 | 76 | public static ArrayList filterViewsToSet(Class classSet[], Iterable viewList) { 77 | ArrayList filteredViews = new ArrayList(); 78 | for (View view : viewList) { 79 | if (view == null) 80 | continue; 81 | // 属于指定类型的view加入返回列表 82 | for (Class filter : classSet) { 83 | if (filter.isAssignableFrom(view.getClass())) { 84 | filteredViews.add(view); 85 | break; 86 | } 87 | } 88 | } 89 | return filteredViews; 90 | } 91 | 92 | /** 93 | * 按照控件屏幕位置从上往下排序 94 | * Orders Views by their location on-screen. 95 | * 96 | * @param views The views to sort. 97 | * @see ViewLocationComparator 98 | */ 99 | 100 | public static void sortViewsByLocationOnScreen(List views) { 101 | Collections.sort(views, new ViewLocationComparator()); 102 | } 103 | 104 | /** 105 | * 按照控件在屏幕上的展示信息排序,从上往下,或从左往右 106 | * views 需要排序的views 107 | * yAxisFirst 为true则从上往下排序,为false则从左往右排序 108 | * Orders Views by their location on-screen. 109 | * 110 | * @param views The views to sort. 111 | * @param yAxisFirst Whether the y-axis should be compared before the x-axis. 112 | * @see ViewLocationComparator 113 | */ 114 | 115 | public static void sortViewsByLocationOnScreen(List views, boolean yAxisFirst) { 116 | Collections.sort(views, new ViewLocationComparator(yAxisFirst)); 117 | } 118 | 119 | /** 120 | * 校验view的文本内容,错误提示信息和帮助提醒信息是否与给定的regex匹配,返回uniqueTextViews中的view总数 121 | * regex 给定的正则 122 | * view 需要校验的view 123 | * uniqueTextViews 已存在的view列表,如果匹配则加入此列表 124 | * Checks if a View matches a certain string and returns the amount of total matches. 125 | * 126 | * @param regex the regex to match 127 | * @param view the view to check 128 | * @param uniqueTextViews set of views that have matched 129 | * @return number of total matches 130 | */ 131 | 132 | public static int getNumberOfMatches(String regex, TextView view, Set uniqueTextViews){ 133 | // 如果传入view为null,那么直接返回总数量 134 | if(view == null) { 135 | return uniqueTextViews.size(); 136 | } 137 | // 按照输入的regex构造正则对象 138 | Pattern pattern = null; 139 | try{ 140 | pattern = Pattern.compile(regex); 141 | }catch(PatternSyntaxException e){ 142 | pattern = Pattern.compile(regex, Pattern.LITERAL); 143 | } 144 | // 获取view 的 text并按照正则匹配 145 | Matcher matcher = pattern.matcher(view.getText().toString()); 146 | //如果配置,把 view加入uniqueTextViews 147 | if (matcher.find()){ 148 | uniqueTextViews.add(view); 149 | } 150 | // 如果view设置了错误提示信息.那么错误提示信息也作为检查条件,如果错误信息匹配了输入的regex, 151 | // 那么加入uniqueTextViews.因uniqueTextViews为Set类型,所以不会存在重复view.重复add不生效 152 | if (view.getError() != null){ 153 | matcher = pattern.matcher(view.getError().toString()); 154 | if (matcher.find()){ 155 | uniqueTextViews.add(view); 156 | } 157 | } 158 | // 检查view 的提示信息是否和给定的regex匹配,如果配置也当做符合的view 159 | if (view.getText().toString().equals("") && view.getHint() != null){ 160 | matcher = pattern.matcher(view.getHint().toString()); 161 | if (matcher.find()){ 162 | uniqueTextViews.add(view); 163 | } 164 | } 165 | // 返回uniqueTextViews总数 166 | return uniqueTextViews.size(); 167 | } 168 | 169 | /** 170 | * 按照给定的text过滤views,返回配置的views,text被当做正则表达式解析 171 | * Filters a collection of Views and returns a list that contains only Views 172 | * with text that matches a specified regular expression. 173 | * 174 | * @param views The collection of views to scan. 175 | * @param regex The text pattern to search for. 176 | * @return A list of views whose text matches the given regex. 177 | */ 178 | 179 | public static List filterViewsByText(Iterable views, String regex) { 180 | return filterViewsByText(views, Pattern.compile(regex)); 181 | } 182 | 183 | /** 184 | * 按照给定的text规则正则表达式过滤views,返回配置的views 185 | * views 传入的views 186 | * regex 正则表达式 187 | * Filters a collection of Views and returns a list that contains only Views 188 | * with text that matches a specified regular expression. 189 | * 190 | * @param views The collection of views to scan. 191 | * @param regex The text pattern to search for. 192 | * @return A list of views whose text matches the given regex. 193 | */ 194 | 195 | public static List filterViewsByText(Iterable views, Pattern regex) { 196 | final ArrayList filteredViews = new ArrayList(); 197 | // 遍历view 198 | for (T view : views) { 199 | // 与正则匹配的加入返回列表 200 | if (view != null && regex.matcher(view.getText()).matches()) { 201 | filteredViews.add(view); 202 | } 203 | } 204 | return filteredViews; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /com/robotium/solo/RobotiumWeb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Used by the web methods. 3 | * 4 | * @author Renas Reda, renas.reda@robotium.com 5 | * 6 | */ 7 | // 获取所有的Web元素 8 | function allWebElements() { 9 | for (var key in document.all){ 10 | try{ 11 | // 通过Robotium WebClient 12 | promptElement(document.all[key]); 13 | }catch(ignored){} 14 | } 15 | // 通知脚本执行完毕 16 | finished(); 17 | } 18 | // 遍历所有 TEXT节点 19 | function allTexts() { 20 | //初始化一个range 21 | var range = document.createRange(); 22 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT,null,false); 23 | while(n=walk.nextNode()){ 24 | try{ 25 | // 通知Robotium WebClient 26 | promptText(n, range); 27 | }catch(ignored){} 28 | } 29 | // 通知脚本执行完毕 30 | finished(); 31 | } 32 | 33 | // 点击Element 34 | function clickElement(element){ 35 | // 构造点击动作事件 36 | var e = document.createEvent('MouseEvents'); 37 | e.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); 38 | element.dispatchEvent(e); 39 | } 40 | //查找指定id的element,click为true则点击,为false则告诉Robotium WebClient相关信息 41 | function id(id, click) { 42 | // 获取id对应的 Element 43 | var element = document.getElementById(id); 44 | // 找到Element则发送点击 45 | if(element != null){ 46 | // true则点击 47 | if(click == 'true'){ 48 | clickElement(element); 49 | } 50 | // 否则告诉Robotium WebClient相关信息 51 | else{ 52 | promptElement(element); 53 | } 54 | } 55 | // 按照id未找到则遍历所有元素查询 56 | else { 57 | for (var key in document.all){ 58 | try{ 59 | element = document.all[key]; 60 | // 找到则执行后续操作 61 | if(element.id == id) { 62 | // 为true则点击 63 | if(click == 'true'){ 64 | clickElement(element); 65 | return; 66 | } 67 | // 其他则告知 robotiumWebClient相关信息 68 | else{ 69 | promptElement(element); 70 | } 71 | } 72 | } catch(ignored){} 73 | } 74 | } 75 | // js执行完毕 76 | finished(); 77 | } 78 | // 按照xpath查找相关elements,click为true则点击查找到的第一个,非true则提交elements相关信息给RobotiumWebClient 79 | function xpath(xpath, click) { 80 | // 按照xpath查找指定elements 81 | var elements = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 82 | 83 | if (elements){ 84 | // 遍历elements 85 | var element = elements.iterateNext(); 86 | while(element) { 87 | // 为true则点击 88 | if(click == 'true'){ 89 | clickElement(element); 90 | return; 91 | } 92 | // 其他则提交相关信息给RobotiumWebClient 93 | else{ 94 | promptElement(element); 95 | element = result.iterateNext(); 96 | } 97 | } 98 | // 脚本执行结束 99 | finished(); 100 | } 101 | } 102 | // 按照css查找相关elements,click为 true则点击找到的第一个,否则提交相关elements信息给RobotiumWebClient 103 | function cssSelector(cssSelector, click) { 104 | // 按照css查找相关elements 105 | var elements = document.querySelectorAll(cssSelector); 106 | // 遍历elements 107 | for (var key in elements) { 108 | if(elements != null){ 109 | try{ 110 | // click为 true则点击,并退出 111 | if(click == 'true'){ 112 | clickElement(elements[key]); 113 | return; 114 | } 115 | //提交element相关信息给RobotiumWebClient 116 | else{ 117 | promptElement(elements[key]); 118 | } 119 | }catch(ignored){} 120 | } 121 | } 122 | // 脚本执行结束 123 | finished(); 124 | } 125 | // 按照name查找对应的element.click为true则点击遇到的第一个,否则提交element信息给RobotiumWebClient 126 | function name(name, click) { 127 | // 获取遍历实例 128 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT,null,false); 129 | // 遍历 130 | while(n=walk.nextNode()){ 131 | try{ 132 | // 检查是否是指定的name 133 | var attributeName = n.getAttribute('name'); 134 | if(attributeName != null && attributeName.trim().length>0 && attributeName == name){ 135 | // click为 true则点击,并退出 136 | if(click == 'true'){ 137 | clickElement(n); 138 | return; 139 | } 140 | //提交element相关信息给RobotiumWebClient 141 | else{ 142 | promptElement(n); 143 | } 144 | } 145 | }catch(ignored){} 146 | } 147 | // 脚本执行结束 148 | finished(); 149 | } 150 | // 按照classname查找element,click为true则点击遇到的第一个,否则提交element信息给RobotiumWebClient 151 | function className(nameOfClass, click) { 152 | // 获取遍历实例 153 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT,null,false); 154 | // 遍历 155 | while(n=walk.nextNode()){ 156 | try{ 157 | var className = n.className; 158 | // 找到对应的element 159 | if(className != null && className.trim().length>0 && className == nameOfClass) { 160 | // click为 true则点击,并退出 161 | if(click == 'true'){ 162 | clickElement(n); 163 | return; 164 | } 165 | //提交element相关信息给RobotiumWebClient 166 | else{ 167 | promptElement(n); 168 | } 169 | } 170 | }catch(ignored){} 171 | } 172 | // 脚本执行结束 173 | finished(); 174 | } 175 | // 按照text查找element,click为true则点击遇到的第一个,否则提交element信息给RobotiumWebClient 176 | function textContent(text, click) { 177 | // 获取对应的遍历实例 178 | var range = document.createRange(); 179 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT,null,false); 180 | // 遍历 181 | while(n=walk.nextNode()){ 182 | try{ 183 | var textContent = n.textContent; 184 | // 找到指定的element 185 | if(textContent.trim() == text.trim()){ 186 | // click为 true则点击,并退出 187 | if(click == 'true'){ 188 | clickElement(n); 189 | return; 190 | } 191 | //提交element相关信息给RobotiumWebClient 192 | else{ 193 | promptText(n, range); 194 | } 195 | } 196 | }catch(ignored){} 197 | } 198 | // 脚本执行结束 199 | finished(); 200 | } 201 | // 按照tagname查找element,click为true则点击遇到的第一个,否则提交element信息给RobotiumWebClient 202 | function tagName(tagName, click) { 203 | // 查找对应的element 204 | var elements = document.getElementsByTagName(tagName); 205 | for (var key in elements) { 206 | if(elements != null){ 207 | try{ 208 | // click为 true则点击,并退出 209 | if(click == 'true'){ 210 | clickElement(elements[key]); 211 | return; 212 | } 213 | //提交element相关信息给RobotiumWebClient 214 | else{ 215 | promptElement(elements[key]); 216 | } 217 | }catch(ignored){} 218 | } 219 | } 220 | // 脚本执行结束 221 | finished(); 222 | } 223 | // 指定id的element设置text 224 | function enterTextById(id, text) { 225 | var element = document.getElementById(id); 226 | if(element != null) 227 | element.value = text; 228 | 229 | finished(); 230 | } 231 | // 指定xpath的element设置text 232 | function enterTextByXpath(xpath, text) { 233 | // 只获取一个 234 | var element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; 235 | if(element != null) 236 | element.value = text; 237 | 238 | finished(); 239 | } 240 | // 指定css的element设置text 241 | function enterTextByCssSelector(cssSelector, text) { 242 | var element = document.querySelector(cssSelector); 243 | if(element != null) 244 | element.value = text; 245 | 246 | finished(); 247 | } 248 | // 指定name的element设置text 249 | function enterTextByName(name, text) { 250 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT,null,false); 251 | while(n=walk.nextNode()){ 252 | var attributeName = n.getAttribute('name'); 253 | if(attributeName != null && attributeName.trim().length>0 && attributeName == name) 254 | n.value=text; 255 | } 256 | finished(); 257 | } 258 | // 指定classname的element设置text,参数名字写成className较好 259 | function enterTextByClassName(name, text) { 260 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT,null,false); 261 | while(n=walk.nextNode()){ 262 | var className = n.className; 263 | if(className != null && className.trim().length>0 && className == name) 264 | n.value=text; 265 | } 266 | finished(); 267 | } 268 | // 按照已有text内容查找对应的element并设置为指定的text 269 | function enterTextByTextContent(textContent, text) { 270 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT,null,false); 271 | while(n=walk.nextNode()){ 272 | var textValue = n.textContent; 273 | if(textValue == textContent) 274 | n.parentNode.value = text; 275 | } 276 | finished(); 277 | } 278 | // 指定tagname的element设置text 279 | function enterTextByTagName(tagName, text) { 280 | var elements = document.getElementsByTagName(tagName); 281 | if(elements != null){ 282 | elements[0].value = text; 283 | } 284 | finished(); 285 | } 286 | // 获取Element属性,并调用prompt方法,弹出属性,Robotium修改过的WebClient抓取这些信息,来构造页面元素 Element 287 | function promptElement(element) { 288 | // 获取element的id 289 | var id = element.id; 290 | // 获取element的 text 291 | var text = element.innerText; 292 | // 过滤掉空格 293 | if(text.trim().length == 0){ 294 | text = element.value; 295 | } 296 | // 获取element的name属性 297 | var name = element.getAttribute('name'); 298 | // 获取element的classname属性 299 | var className = element.className; 300 | // 获取element的tagname属性 301 | var tagName = element.tagName; 302 | 获取剩余的其他属性 303 | var attributes = ""; 304 | var htmlAttributes = element.attributes; 305 | // 遍历剩余属性,通过#$分割属性 306 | for (var i = 0, htmlAttribute; htmlAttribute = htmlAttributes[i]; i++){ 307 | attributes += htmlAttribute.name + "::" + htmlAttribute.value; 308 | if (i + 1 < htmlAttributes.length) { 309 | attributes += "#$"; 310 | } 311 | } 312 | // 获取element大小, 313 | var rect = element.getBoundingClientRect(); 314 | // 可见的element拼接字符串,传递给Robotium WebClient 315 | if(rect.width > 0 && rect.height > 0 && rect.left >= 0 && rect.top >= 0){ 316 | prompt(id + ';,' + text + ';,' + name + ";," + className + ";," + tagName + ";," + rect.left + ';,' + rect.top + ';,' + rect.width + ';,' + rect.height + ';,' + attributes); 317 | } 318 | } 319 | // 按照range信息构造内容返回给Robotium WebClient 320 | function promptText(element, range) { 321 | // 获取Elemet的text内容 322 | var text = element.textContent; 323 | if(text.trim().length>0) { 324 | // 设置range的范围为当前Element 325 | range.selectNodeContents(element); 326 | // 获取尺寸信息 327 | var rect = range.getBoundingClientRect(); 328 | // 只返回可见的 Element 329 | if(rect.width > 0 && rect.height > 0 && rect.left >= 0 && rect.top >= 0){ 330 | var id = element.parentNode.id; 331 | var name = element.parentNode.getAttribute('name'); 332 | var className = element.parentNode.className; 333 | var tagName = element.parentNode.tagName; 334 | prompt(id + ';,' + text + ';,' + name + ";," + className + ";," + tagName + ";," + rect.left + ';,' + rect.top + ';,' + rect.width + ';,' + rect.height); 335 | } 336 | } 337 | } 338 | 339 | // js执行完毕,通知Robotium WebClient 完成了 340 | function finished(){ 341 | prompt('robotium-finished'); 342 | } 343 | -------------------------------------------------------------------------------- /com/robotium/solo/RobotiumWebClient.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.List; 4 | import android.app.Instrumentation; 5 | import android.graphics.Bitmap; 6 | import android.os.Message; 7 | import android.view.View; 8 | import android.webkit.ConsoleMessage; 9 | import android.webkit.GeolocationPermissions; 10 | import android.webkit.JsPromptResult; 11 | import android.webkit.JsResult; 12 | import android.webkit.ValueCallback; 13 | import android.webkit.WebChromeClient; 14 | import android.webkit.WebStorage; 15 | import android.webkit.WebView; 16 | 17 | /** 18 | * Robotium WebView操作工具类,扩展WebChromeClient 19 | * Robotium需要操作WebView,因此需要劫持WebView的 JS执行,重写onJsPrompt,获取WebView中相关元素 20 | * WebChromeClient used to get information on web elements by injections of JavaScript. 21 | * 22 | * @author Renas Reda, renas.reda@robotium.com 23 | * 24 | */ 25 | 26 | class RobotiumWebClient extends WebChromeClient{ 27 | // 用于构造WebElement的工具类 28 | WebElementCreator webElementCreator; 29 | // Instrument,用于发送各种事件 30 | private Instrumentation inst; 31 | // robotium扩展的client 32 | private WebChromeClient robotiumWebClient; 33 | // 原生的client 34 | private WebChromeClient originalWebChromeClient = null; 35 | 36 | 37 | /** 38 | * 构造函数 39 | * Constructs this object. 40 | * 41 | * @param instrumentation the {@code Instrumentation} instance 42 | * @param webElementCreator the {@code WebElementCreator} instance 43 | */ 44 | 45 | public RobotiumWebClient(Instrumentation inst, WebElementCreator webElementCreator){ 46 | this.inst = inst; 47 | this.webElementCreator = webElementCreator; 48 | robotiumWebClient = this; 49 | } 50 | 51 | /** 52 | * 设置WebView可执行javaScript,各种WebView操作都要靠JavaScript完成. 53 | * Enables JavaScript in the given {@code WebViews} objects. 54 | * 55 | * @param webViews the {@code WebView} objects to enable JavaScript in 56 | */ 57 | 58 | public void enableJavascriptAndSetRobotiumWebClient(List webViews, WebChromeClient originalWebChromeClient){ 59 | // 保留原有的ChromeClient.用作需要原生调用时使用 60 | this.originalWebChromeClient = originalWebChromeClient; 61 | 62 | for(final WebView webView : webViews){ 63 | 64 | if(webView != null){ 65 | inst.runOnMainSync(new Runnable() { 66 | public void run() { 67 | // 设置可执行js 68 | webView.getSettings().setJavaScriptEnabled(true); 69 | // 设置使用 Robotium定制的WebClient 70 | webView.setWebChromeClient(robotiumWebClient); 71 | 72 | } 73 | }); 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * 重写js执行处理函数,robotium使用的通过js的prompt也解析所有元素信息,因此重写改方法 80 | * Overrides onJsPrompt in order to create {@code WebElement} objects based on the web elements attributes prompted by the injections of JavaScript 81 | */ 82 | 83 | @Override 84 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult r) { 85 | // 对于robotium执行的js进行特殊处理,解析js执行返回信息,并构造相关的WebElement信息 86 | if(message != null && (message.contains(";,") || message.contains("robotium-finished"))){ 87 | // 执行完成则设置解析完毕 88 | if(message.equals("robotium-finished")){ 89 | webElementCreator.setFinished(true); 90 | } 91 | else{ 92 | webElementCreator.createWebElementAndAddInList(message, view); 93 | } 94 | // 直接确认掉,避免影响页面 95 | r.confirm(); 96 | return true; 97 | } 98 | // 非robotium运行的js,使用默认逻辑处理 99 | else { 100 | if(originalWebChromeClient != null) { 101 | return originalWebChromeClient.onJsPrompt(view, url, message, defaultValue, r); 102 | } 103 | return true; 104 | } 105 | 106 | } 107 | /** 108 | * 重写方法,调用原生的 109 | */ 110 | @Override 111 | public Bitmap getDefaultVideoPoster() { 112 | if (originalWebChromeClient != null) { 113 | return originalWebChromeClient.getDefaultVideoPoster(); 114 | } 115 | return null; 116 | } 117 | /** 118 | * 重写方法,调用原生的 119 | */ 120 | @Override 121 | public View getVideoLoadingProgressView() { 122 | if (originalWebChromeClient != null) { 123 | return originalWebChromeClient.getVideoLoadingProgressView(); 124 | } 125 | return null; 126 | } 127 | /** 128 | * 重写方法,调用原生的 129 | */ 130 | @Override 131 | public void getVisitedHistory(ValueCallback callback) { 132 | if (originalWebChromeClient != null) { 133 | originalWebChromeClient.getVisitedHistory(callback); 134 | } 135 | } 136 | /** 137 | * 重写方法,调用原生的 138 | */ 139 | @Override 140 | public void onCloseWindow(WebView window) { 141 | if (originalWebChromeClient != null) { 142 | originalWebChromeClient.onCloseWindow(window); 143 | } 144 | } 145 | /** 146 | * 重写方法,调用原生的 147 | */ 148 | @Override 149 | public void onConsoleMessage(String message, int lineNumber, String sourceID) { 150 | if (originalWebChromeClient != null) { 151 | originalWebChromeClient.onConsoleMessage(message, lineNumber, sourceID); 152 | } 153 | } 154 | /** 155 | * 重写方法,调用原生的 156 | */ 157 | @Override 158 | public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 159 | if (originalWebChromeClient != null) { 160 | return originalWebChromeClient.onConsoleMessage(consoleMessage); 161 | } 162 | return true; 163 | } 164 | /** 165 | * 重写方法,调用原生的 166 | */ 167 | @Override 168 | public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { 169 | if (originalWebChromeClient != null) { 170 | return originalWebChromeClient.onCreateWindow(view, isDialog, isUserGesture, resultMsg); 171 | } 172 | return true; 173 | } 174 | /** 175 | * 重写方法,调用原生的 176 | */ 177 | @Override 178 | public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, 179 | long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater) { 180 | if (originalWebChromeClient != null) { 181 | originalWebChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater); 182 | } 183 | } 184 | /** 185 | * 重写方法,调用原生的 186 | */ 187 | @Override 188 | public void onGeolocationPermissionsHidePrompt() { 189 | if (originalWebChromeClient != null) { 190 | originalWebChromeClient.onGeolocationPermissionsHidePrompt(); 191 | } 192 | } 193 | /** 194 | * 重写方法,调用原生的 195 | */ 196 | @Override 197 | public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { 198 | if (originalWebChromeClient != null) { 199 | originalWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); 200 | } 201 | } 202 | /** 203 | * 重写方法,调用原生的 204 | */ 205 | @Override 206 | public void onHideCustomView() { 207 | if (originalWebChromeClient != null) { 208 | originalWebChromeClient.onHideCustomView(); 209 | } 210 | } 211 | /** 212 | * 重写方法,调用原生的 213 | */ 214 | @Override 215 | public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 216 | if (originalWebChromeClient != null) { 217 | return originalWebChromeClient.onJsAlert(view, url, message, result); 218 | } 219 | return true; 220 | } 221 | /** 222 | * 重写方法,调用原生的 223 | */ 224 | @Override 225 | public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) { 226 | if (originalWebChromeClient.onJsBeforeUnload(view, url, message, result)) { 227 | return originalWebChromeClient.onJsBeforeUnload(view, url, message, result); 228 | } 229 | return true; 230 | } 231 | /** 232 | * 重写方法,调用原生的 233 | */ 234 | @Override 235 | public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 236 | if (originalWebChromeClient != null) { 237 | return originalWebChromeClient.onJsConfirm(view, url, message, result); 238 | } 239 | return true; 240 | } 241 | /** 242 | * 重写方法,调用原生的 243 | */ 244 | @Override 245 | public boolean onJsTimeout() { 246 | if (originalWebChromeClient != null) { 247 | return originalWebChromeClient.onJsTimeout(); 248 | } 249 | return true; 250 | } 251 | /** 252 | * 重写方法,调用原生的 253 | */ 254 | @Override 255 | public void onProgressChanged(WebView view, int newProgress) { 256 | if (originalWebChromeClient != null) { 257 | originalWebChromeClient.onProgressChanged(view, newProgress); 258 | } 259 | } 260 | /** 261 | * 重写方法,调用原生的 262 | */ 263 | @Override 264 | public void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater) { 265 | if (originalWebChromeClient != null) { 266 | originalWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); 267 | } 268 | } 269 | /** 270 | * 重写方法,调用原生的 271 | */ 272 | @Override 273 | public void onReceivedIcon(WebView view, Bitmap icon) { 274 | if (originalWebChromeClient != null) { 275 | originalWebChromeClient.onReceivedIcon(view, icon); 276 | } 277 | } 278 | /** 279 | * 重写方法,调用原生的 280 | */ 281 | @Override 282 | public void onReceivedTitle(WebView view, String title) { 283 | if (originalWebChromeClient != null) { 284 | originalWebChromeClient.onReceivedTitle(view, title); 285 | } 286 | } 287 | /** 288 | * 重写方法,调用原生的 289 | */ 290 | @Override 291 | public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) { 292 | if (originalWebChromeClient != null) { 293 | originalWebChromeClient.onReceivedTouchIconUrl(view, url, precomposed); 294 | } 295 | } 296 | /** 297 | * 重写方法,调用原生的 298 | */ 299 | @Override 300 | public void onRequestFocus(WebView view) { 301 | if (originalWebChromeClient != null) { 302 | originalWebChromeClient.onRequestFocus(view); 303 | } 304 | } 305 | /** 306 | * 重写方法,调用原生的 307 | */ 308 | @Override 309 | public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { 310 | if (originalWebChromeClient != null) { 311 | originalWebChromeClient.onShowCustomView(view, callback); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /com/robotium/solo/Rotator.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Instrumentation; 4 | import android.graphics.PointF; 5 | import android.os.SystemClock; 6 | import android.view.InputDevice; 7 | import android.view.MotionEvent; 8 | import android.view.MotionEvent.PointerCoords; 9 | import android.view.MotionEvent.PointerProperties; 10 | // 屏幕方向操作 11 | // API 要求14 12 | class Rotator 13 | { 14 | // Instrument 用于发送事件,C风格代码,带个 _ 15 | private final Instrumentation _instrument; 16 | // 10ms 17 | private static final int EVENT_TIME_INTERVAL_MS = 10; 18 | // 放大 19 | public static final int LARGE = 0; 20 | // 缩小 21 | public static final int SMALL = 1; 22 | // 构造函数 23 | public Rotator(Instrumentation inst) 24 | { 25 | this._instrument = inst; 26 | } 27 | // 发送屏幕转动模拟用户画圈 28 | // size 0 每次转动3.6角度,1每次转动36角度 29 | // center1 第一个手指的点 30 | // center2 第二个手指的点 31 | public void generateRotateGesture(int size, PointF center1, PointF center2) 32 | { 33 | double incrementFactor = 0; 34 | // 获取第一个坐标点的X 35 | float startX1 = center1.x; 36 | // 获取第一个坐标点的Y 37 | float startY1 = center1.y; 38 | // 获取第二个坐标点的X 39 | float startX2 = center2.x; 40 | // 获取第二个坐标点的Y 41 | float startY2 = center2.y; 42 | // 获取当前时间值, 43 | long downTime = SystemClock.uptimeMillis(); 44 | long eventTime = SystemClock.uptimeMillis(); 45 | 46 | // pointer 1 47 | float x1 = startX1; 48 | float y1 = startY1; 49 | 50 | // pointer 2 51 | float x2 = startX2; 52 | float y2 = startY2; 53 | // 构造坐标点集合 54 | PointerCoords[] pointerCoords = new PointerCoords[2]; 55 | PointerCoords pc1 = new PointerCoords(); 56 | PointerCoords pc2 = new PointerCoords(); 57 | pc1.x = x1; 58 | pc1.y = y1; 59 | pc1.pressure = 1; 60 | pc1.size = 1; 61 | pc2.x = x2; 62 | pc2.y = y2; 63 | pc2.pressure = 1; 64 | pc2.size = 1; 65 | pointerCoords[0] = pc1; 66 | pointerCoords[1] = pc2; 67 | // 构造坐标点集合,手指按住屏幕 68 | PointerProperties[] pointerProperties = new PointerProperties[2]; 69 | PointerProperties pp1 = new PointerProperties(); 70 | PointerProperties pp2 = new PointerProperties(); 71 | pp1.id = 0; 72 | pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 73 | pp2.id = 1; 74 | pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 75 | pointerProperties[0] = pp1; 76 | pointerProperties[1] = pp2; 77 | // 发送转动事件 78 | MotionEvent event; 79 | // send the initial touches 80 | event = MotionEvent.obtain(downTime, eventTime, 81 | MotionEvent.ACTION_DOWN, 1, pointerProperties, pointerCoords, 82 | 0, 0, // metaState, buttonState 83 | 1, // x precision 84 | 1, // y precision 85 | 0, 0, // deviceId, edgeFlags 86 | InputDevice.SOURCE_TOUCHSCREEN, 0); // source, flags 87 | _instrument.sendPointerSync(event); 88 | 89 | event = MotionEvent.obtain(downTime, eventTime, 90 | MotionEvent.ACTION_POINTER_DOWN 91 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 92 | 2, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 93 | InputDevice.SOURCE_TOUCHSCREEN, 0); 94 | _instrument.sendPointerSync(event); 95 | // 按照设定值,指定转动速率 96 | switch(size) 97 | { 98 | case 0: 99 | { 100 | incrementFactor = 0.01; 101 | } 102 | break; 103 | case 1: 104 | { 105 | incrementFactor = 0.1; 106 | } 107 | break; 108 | } 109 | // 发送手指滑动事件 110 | for (double i = 0; i < Math.PI; i += incrementFactor) 111 | { 112 | eventTime += EVENT_TIME_INTERVAL_MS; 113 | pointerCoords[0].x += Math.cos(i); 114 | pointerCoords[0].y += Math.sin(i); 115 | pointerCoords[1].x += Math.cos(i + Math.PI); 116 | pointerCoords[1].y += Math.sin(i + Math.PI); 117 | 118 | event = MotionEvent.obtain(downTime, eventTime, 119 | MotionEvent.ACTION_MOVE, 2, pointerProperties, 120 | pointerCoords, 0, 0, 1, 1, 0, 0, 121 | InputDevice.SOURCE_TOUCHSCREEN, 0); 122 | _instrument.sendPointerSync(event); 123 | } 124 | // 松开手指 125 | // and remove them fingers from the screen 126 | eventTime += EVENT_TIME_INTERVAL_MS; 127 | event = MotionEvent.obtain(downTime, eventTime, 128 | MotionEvent.ACTION_POINTER_UP 129 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 130 | 2, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 131 | InputDevice.SOURCE_TOUCHSCREEN, 0); 132 | _instrument.sendPointerSync(event); 133 | 134 | eventTime += EVENT_TIME_INTERVAL_MS; 135 | event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, 136 | 1, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 137 | InputDevice.SOURCE_TOUCHSCREEN, 0); 138 | _instrument.sendPointerSync(event); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /com/robotium/solo/ScreenshotTaker.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.text.SimpleDateFormat; 6 | import java.util.ArrayList; 7 | import java.util.Date; 8 | import java.util.concurrent.CountDownLatch; 9 | import com.robotium.solo.Solo.Config; 10 | import com.robotium.solo.Solo.Config.ScreenshotFileType; 11 | import android.graphics.Bitmap; 12 | import android.graphics.Canvas; 13 | import android.graphics.Picture; 14 | import android.opengl.GLSurfaceView; 15 | import android.opengl.GLSurfaceView.Renderer; 16 | import android.os.Handler; 17 | import android.os.HandlerThread; 18 | import android.os.Message; 19 | import android.os.SystemClock; 20 | import android.util.Log; 21 | import android.view.View; 22 | import android.webkit.WebView; 23 | 24 | /** 25 | * 截屏操作工具类 26 | * Contains screenshot methods like: takeScreenshot(final View, final String name), startScreenshotSequence(final String name, final int quality, final int frameDelay, final int maxFrames), 27 | * stopScreenshotSequence(). 28 | * 29 | * 30 | * @author Renas Reda, renas.reda@robotium.com 31 | * 32 | */ 33 | 34 | class ScreenshotTaker { 35 | // 配置文件,配置Robotium的各种属性 36 | private final Config config; 37 | // activity工具类 38 | private final ActivityUtils activityUtils; 39 | // 日志标记,标识该操作是Robotium的 40 | private final String LOG_TAG = "Robotium"; 41 | // 连续截图线程 42 | private ScreenshotSequenceThread screenshotSequenceThread = null; 43 | // 图片存储处理线程 44 | private HandlerThread screenShotSaverThread = null; 45 | // 图片保存工具类 46 | private ScreenShotSaver screenShotSaver = null; 47 | // view查找工具类 48 | private final ViewFetcher viewFetcher; 49 | // 延时等待工具类 50 | private final Sleeper sleeper; 51 | 52 | 53 | /** 54 | * 构造函数 55 | * Constructs this object. 56 | * 57 | * @param config the {@code Config} instance 58 | * @param activityUtils the {@code ActivityUtils} instance 59 | * @param viewFetcher the {@code ViewFetcher} instance 60 | * @param sleeper the {@code Sleeper} instance 61 | * 62 | */ 63 | ScreenshotTaker(Config config, ActivityUtils activityUtils, ViewFetcher viewFetcher, Sleeper sleeper) { 64 | this.config = config; 65 | this.activityUtils = activityUtils; 66 | this.viewFetcher = viewFetcher; 67 | this.sleeper = sleeper; 68 | } 69 | 70 | /** 71 | * 截图操作,要去有写SDcard权限,截图文件会存储到sdcard,如要修改储存路径,那么可以通过修改Config的screenshotSavePath属性编辑 72 | * 默认路径为/sdcard/Robotium-Screenshots/ 73 | * name 截图保存文件名 74 | * quality 截图质量0-100 75 | * Takes a screenshot and saves it in the {@link Config} objects save path. 76 | * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test. 77 | * 78 | * @param view the view to take screenshot of 这个参数已经没有了,就没必要加这个注释了 79 | * @param name the name to give the screenshot image 80 | * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). 81 | */ 82 | public void takeScreenshot(final String name, final int quality) { 83 | // 获取DecorView 84 | View decorView = getScreenshotView(); 85 | // 无法获取DecorView,直接退出 86 | if(decorView == null) 87 | return; 88 | // 初始化图片存储需要的一些事情 89 | initScreenShotSaver(); 90 | // 构造截图线程 91 | ScreenshotRunnable runnable = new ScreenshotRunnable(decorView, name, quality); 92 | // 执行截图线程 93 | activityUtils.getCurrentActivity(false).runOnUiThread(runnable); 94 | } 95 | 96 | /** 97 | * 连接截图 98 | * name 截图保存的图片名.会追加_0---maxFrames-1 99 | * quality 截图质量0-100 100 | * frameDelay 每次截图时间间隔 101 | * maxFrames 截图数量 102 | * Takes a screenshot sequence and saves the images with the name prefix in the {@link Config} objects save path. 103 | * 104 | * The name prefix is appended with "_" + sequence_number for each image in the sequence, 105 | * where numbering starts at 0. 106 | * 107 | * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in the 108 | * AndroidManifest.xml of the application under test. 109 | * 110 | * Taking a screenshot will take on the order of 40-100 milliseconds of time on the 111 | * main UI thread. Therefore it is possible to mess up the timing of tests if 112 | * the frameDelay value is set too small. 113 | * 114 | * At present multiple simultaneous screenshot sequences are not supported. 115 | * This method will throw an exception if stopScreenshotSequence() has not been 116 | * called to finish any prior sequences. 117 | * 118 | * @param name the name prefix to give the screenshot 119 | * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality) 120 | * @param frameDelay the time in milliseconds to wait between each frame 121 | * @param maxFrames the maximum number of frames that will comprise this sequence 122 | * 123 | */ 124 | public void startScreenshotSequence(final String name, final int quality, final int frameDelay, final int maxFrames) { 125 | // 初始化截图保存相关 126 | initScreenShotSaver(); 127 | // 禁止同时执行多个连续截图,当有连续截图在执行时抛出异常 128 | if(screenshotSequenceThread != null) { 129 | throw new RuntimeException("only one screenshot sequence is supported at a time"); 130 | } 131 | // 构造一个连续截图线程 132 | screenshotSequenceThread = new ScreenshotSequenceThread(name, quality, frameDelay, maxFrames); 133 | // 开始连续截图 134 | screenshotSequenceThread.start(); 135 | } 136 | 137 | /** 138 | * 停止连续截图 139 | * Causes a screenshot sequence to end. 140 | * 141 | * If this method is not called to end a sequence and a prior sequence is still in 142 | * progress, startScreenshotSequence() will throw an exception. 143 | */ 144 | public void stopScreenshotSequence() { 145 | // 当连续截图线程非空时,停止连续截图 146 | if(screenshotSequenceThread != null) { 147 | // 停止连续截图 148 | screenshotSequenceThread.interrupt(); 149 | // 释放线程对象 150 | screenshotSequenceThread = null; 151 | } 152 | } 153 | 154 | /** 155 | * 获取当前的界面显示view,并做一些Robotium定制化的操作 156 | * Gets the proper view to use for a screenshot. 157 | */ 158 | private View getScreenshotView() { 159 | // 获取当前的显示界面view 160 | View decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews()); 161 | // 设置超时时间 162 | final long endTime = SystemClock.uptimeMillis() + Timeout.getSmallTimeout(); 163 | // 如果无法获取decorView,则继续查找 164 | while (decorView == null) { 165 | // 检查是否已经超时 166 | final boolean timedOut = SystemClock.uptimeMillis() > endTime; 167 | // 已经超时直接退出 168 | if (timedOut){ 169 | return null; 170 | } 171 | // 等待300ms 172 | sleeper.sleepMini(); 173 | // 重试获取当前的decorView 174 | decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews()); 175 | } 176 | // 用Rotium的Render替换原生的Render 177 | wrapAllGLViews(decorView); 178 | 179 | return decorView; 180 | } 181 | 182 | /** 183 | * 修改 View的Render,用Robotium自定义的替换 184 | * Extract and wrap the all OpenGL ES Renderer. 185 | */ 186 | private void wrapAllGLViews(View decorView) { 187 | // 获取当前decorView中的GLSurfaceView类型的view 188 | ArrayList currentViews = viewFetcher.getCurrentViews(GLSurfaceView.class, decorView); 189 | // 锁住当前线程,避免并发引发问题 190 | final CountDownLatch latch = new CountDownLatch(currentViews.size()); 191 | // 编译所有view进行替换render 192 | for (GLSurfaceView glView : currentViews) { 193 | // 反射获取属性 194 | Object renderContainer = new Reflect(glView).field("mGLThread") 195 | .type(GLSurfaceView.class).out(Object.class); 196 | // 获取原始的renderer 197 | Renderer renderer = new Reflect(renderContainer).field("mRenderer").out(Renderer.class); 198 | // 如果获取失败,则尝试直接获取glView的属性 199 | if (renderer == null) { 200 | renderer = new Reflect(glView).field("mRenderer").out(Renderer.class); 201 | renderContainer = glView; 202 | } 203 | // 如果无法获取,则跳过当前,处理下一个 204 | if (renderer == null) { 205 | //计数器减一 206 | latch.countDown(); 207 | // 跳转到下个循环 208 | continue; 209 | } 210 | // 按照render类型进行操作,如果已经是Robotium修改过的render,那么重置下相关属性即可 211 | if (renderer instanceof GLRenderWrapper) { 212 | // 类型转成Robotium的 213 | GLRenderWrapper wrapper = (GLRenderWrapper) renderer; 214 | // 设置截图模式 215 | wrapper.setTakeScreenshot(); 216 | // 设置并发控制计数器 217 | wrapper.setLatch(latch); 218 | // 如果还不是robotium修改过的,那么就重新构造一个,并且替换原有属性 219 | } else { 220 | // 构造一个robotium修改过的Render 221 | GLRenderWrapper wrapper = new GLRenderWrapper(glView, renderer, latch); 222 | // 通过反射修改属性为定制的render 223 | new Reflect(renderContainer).field("mRenderer").in(wrapper); 224 | } 225 | } 226 | // 等待操作完成 227 | try { 228 | latch.await(); 229 | } catch (InterruptedException ex) { 230 | ex.printStackTrace(); 231 | } 232 | } 233 | 234 | 235 | /** 236 | * 获取WebView的图形内容 237 | * Returns a bitmap of a given WebView. 238 | * 239 | * @param webView the webView to save a bitmap from 240 | * @return a bitmap of the given web view 241 | * 242 | */ 243 | 244 | private Bitmap getBitmapOfWebView(final WebView webView){ 245 | // 获取WebView图形内容 246 | Picture picture = webView.capturePicture(); 247 | // 构造Bitmap对象 248 | Bitmap b = Bitmap.createBitmap( picture.getWidth(), picture.getHeight(), Bitmap.Config.ARGB_8888); 249 | // 构造Canvas 250 | Canvas c = new Canvas(b); 251 | // 把图片绘制到canvas.就是把内容搞到Bitmap中,即b中 252 | picture.draw(c); 253 | return b; 254 | } 255 | 256 | /** 257 | * 获取View的BitMap格式文件内容 258 | * Returns a bitmap of a given View. 259 | * 260 | * @param view the view to save a bitmap from 261 | * @return a bitmap of the given view 262 | * 263 | */ 264 | 265 | private Bitmap getBitmapOfView(final View view){ 266 | // 初始化缓冲,清空原有内容 267 | view.destroyDrawingCache(); 268 | view.buildDrawingCache(false); 269 | // 获取Bitmap内容 270 | Bitmap orig = view.getDrawingCache(); 271 | Bitmap.Config config = null; 272 | // 如果获取内容为null,直接返回null 273 | if(orig == null) { 274 | return null; 275 | } 276 | // 获取配置信息 277 | config = orig.getConfig(); 278 | // 如果图片类型无法获取,则默认使用ARGB_8888 279 | if(config == null) { 280 | config = Bitmap.Config.ARGB_8888; 281 | } 282 | // 构造BitMap内容 283 | Bitmap b = orig.copy(config, false); 284 | // 清空绘图缓存 285 | view.destroyDrawingCache(); 286 | return b; 287 | } 288 | 289 | /** 290 | * 按照传入文件名,构造完整文件名 291 | * Returns a proper filename depending on if name is given or not. 292 | * 293 | * @param name the given name 294 | * @return a proper filename depedning on if a name is given or not 295 | * 296 | */ 297 | 298 | private String getFileName(final String name){ 299 | // 构造日期格式 300 | SimpleDateFormat sdf = new SimpleDateFormat("ddMMyy-hhmmss"); 301 | String fileName = null; 302 | // 如果未传入名字,那么默认构造一个 303 | if(name == null){ 304 | // 按照配置构造图片类型jpg png 305 | if(config.screenshotFileType == ScreenshotFileType.JPEG){ 306 | fileName = sdf.format( new Date()).toString()+ ".jpg"; 307 | } 308 | else{ 309 | fileName = sdf.format( new Date()).toString()+ ".png"; 310 | } 311 | } 312 | // 如已传入文件名字,那么拼接文件类型后缀 313 | else { 314 | // 按照配置构造图片类型jpg png 315 | if(config.screenshotFileType == ScreenshotFileType.JPEG){ 316 | fileName = name + ".jpg"; 317 | } 318 | else { 319 | fileName = name + ".png"; 320 | } 321 | } 322 | return fileName; 323 | } 324 | 325 | /** 326 | * 初始化图片存储相关资源 327 | * This method initializes the aysnc screenshot saving logic 328 | */ 329 | private void initScreenShotSaver() { 330 | // 如果当前存储线程未初始化,则进行初始化 331 | if(screenShotSaverThread == null || screenShotSaver == null) { 332 | // 初始化一个处理线程 333 | screenShotSaverThread = new HandlerThread("ScreenShotSaver"); 334 | // 开始运行线程 335 | screenShotSaverThread.start(); 336 | // 初始化一个存储类 337 | screenShotSaver = new ScreenShotSaver(screenShotSaverThread); 338 | } 339 | } 340 | 341 | /** 342 | * 连续截图线程 343 | * _name 截图保存名,会拼接上顺序0--_maxFrames-1 344 | * _quality 截图质量0-100 345 | * _frameDelay 截图间隔时间,单位 ms 346 | * _maxFrames 截图数量 347 | * This is the thread which causes a screenshot sequence to happen 348 | * in parallel with testing. 349 | */ 350 | private class ScreenshotSequenceThread extends Thread { 351 | // 开始点设置为0 352 | private int seqno = 0; 353 | // 保存的文件名 354 | private String name; 355 | // 图片质量0-100 356 | private int quality; 357 | // 截图延时,单位 ms 358 | private int frameDelay; 359 | // 需要截图的数量 360 | private int maxFrames; 361 | 362 | private boolean keepRunning = true; 363 | // 构造函数 364 | public ScreenshotSequenceThread(String _name, int _quality, int _frameDelay, int _maxFrames) { 365 | name = _name; 366 | quality = _quality; 367 | frameDelay = _frameDelay; 368 | maxFrames = _maxFrames; 369 | } 370 | 371 | public void run() { 372 | // 截图数量未达到指定值,继续截图 373 | while(seqno < maxFrames) { 374 | // 线程结束或业务已经完成则退出循环 375 | if(!keepRunning || Thread.interrupted()) break; 376 | // 截图 377 | doScreenshot(); 378 | // 计算器+1 379 | seqno++; 380 | try { 381 | // 等待指定的时间 382 | Thread.sleep(frameDelay); 383 | } catch (InterruptedException e) { 384 | } 385 | } 386 | // 释放线程对象 387 | screenshotSequenceThread = null; 388 | } 389 | // 截图 390 | public void doScreenshot() { 391 | // 获取当前的屏幕DecorView 392 | View v = getScreenshotView(); 393 | // 如果无法获取decorView 终止当前线程 394 | if(v == null) keepRunning = false; 395 | // 拼接文件名 396 | String final_name = name+"_"+seqno; 397 | // 初始化截图线程 398 | ScreenshotRunnable r = new ScreenshotRunnable(v, final_name, quality); 399 | // 记录日志 400 | Log.d(LOG_TAG, "taking screenshot "+final_name); 401 | // 启动截图线程 402 | activityUtils.getCurrentActivity(false).runOnUiThread(r); 403 | } 404 | // 停掉当前线程 405 | public void interrupt() { 406 | // 标记为设置为false,停止截图 407 | keepRunning = false; 408 | super.interrupt(); 409 | } 410 | } 411 | 412 | /** 413 | * 抓取当前屏幕并发送给对应图片处理器进行相关图片处理和保存 414 | * Here we have a Runnable which is responsible for taking the actual screenshot, 415 | * and then posting the bitmap to a Handler which will save it. 416 | * 417 | * This Runnable is run on the UI thread. 418 | */ 419 | private class ScreenshotRunnable implements Runnable { 420 | // decorView 421 | private View view; 422 | // 文件名 423 | private String name; 424 | // 图片质量 425 | private int quality; 426 | // 构造函数 427 | public ScreenshotRunnable(final View _view, final String _name, final int _quality) { 428 | view = _view; 429 | name = _name; 430 | quality = _quality; 431 | } 432 | 433 | public void run() { 434 | // 如果decorView可以获取到,则截图 435 | if(view !=null){ 436 | Bitmap b; 437 | // 按照 View类型进行图片内容获取操作 438 | if(view instanceof WebView){ 439 | b = getBitmapOfWebView((WebView) view); 440 | } 441 | else{ 442 | b = getBitmapOfView(view); 443 | } 444 | // 如果可以获取到图片内容,则保存图片 445 | if(b != null) 446 | screenShotSaver.saveBitmap(b, name, quality); 447 | // 无法获取图片内容,打印相关日志 448 | else 449 | Log.d(LOG_TAG, "NULL BITMAP!!"); 450 | } 451 | } 452 | } 453 | 454 | /** 455 | * 保存图片,通过异步线程完成 456 | * This class is a Handler which deals with saving the screenshots on a separate thread. 457 | * 458 | * The screenshot logic by necessity has to run on the ui thread. However, in practice 459 | * it seems that saving a screenshot (with quality 100) takes approx twice as long 460 | * as taking it in the first place. 461 | * 462 | * Saving the screenshots in a separate thread like this will thus make the screenshot 463 | * process approx 3x faster as far as the main thread is concerned. 464 | * 465 | */ 466 | private class ScreenShotSaver extends Handler { 467 | // 构造函数 468 | public ScreenShotSaver(HandlerThread thread) { 469 | super(thread.getLooper()); 470 | } 471 | 472 | /** 473 | * 保存图片,通过消息推送 474 | * bitmap 要保存的图片 475 | * name 图片名 476 | * quality 图片质量0-100 477 | * This method posts a Bitmap with meta-data to the Handler queue. 478 | * 479 | * @param bitmap the bitmap to save 480 | * @param name the name of the file 481 | * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). 482 | */ 483 | public void saveBitmap(Bitmap bitmap, String name, int quality) { 484 | // 初始化构造一个消息 485 | Message message = this.obtainMessage(); 486 | // 初始化消息属性 487 | message.arg1 = quality; 488 | message.obj = bitmap; 489 | message.getData().putString("name", name); 490 | // 发送消息,等待处理器处理 491 | this.sendMessage(message); 492 | } 493 | 494 | /** 495 | * 处理收到的消息 496 | * Here we process the Handler queue and save the bitmaps. 497 | * 498 | * @param message A Message containing the bitmap to save, and some metadata. 499 | */ 500 | public void handleMessage(Message message) { 501 | // 获取图片名 502 | String name = message.getData().getString("name"); 503 | // 获取图片质量 504 | int quality = message.arg1; 505 | // 获取图片内容 506 | Bitmap b = (Bitmap)message.obj; 507 | // 处理图片内容 508 | if(b != null) { 509 | // 保存图片到指定文件 510 | saveFile(name, b, quality); 511 | // 释放图片缓存 512 | b.recycle(); 513 | } 514 | // 如果图片无内容,则打印日志信息 515 | else { 516 | Log.d(LOG_TAG, "NULL BITMAP!!"); 517 | } 518 | } 519 | 520 | /** 521 | * 保存结果文件 522 | * Saves a file. 523 | * 524 | * @param name the name of the file 525 | * @param b the bitmap to save 526 | * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). 527 | * 528 | */ 529 | private void saveFile(String name, Bitmap b, int quality){ 530 | // 写文件对象 531 | FileOutputStream fos = null; 532 | // 构造完整文件名 533 | String fileName = getFileName(name); 534 | // 获取系统设置的目录 535 | File directory = new File(config.screenshotSavePath); 536 | // 创建目录 537 | directory.mkdir(); 538 | // 获取文件对象 539 | File fileToSave = new File(directory,fileName); 540 | try { 541 | // 获取文件流写对象 542 | fos = new FileOutputStream(fileToSave); 543 | if(config.screenshotFileType == ScreenshotFileType.JPEG){ 544 | // 图片内容按照指定格式压缩,并写入指定文件,如出现异常,打印异常日志 545 | if (b.compress(Bitmap.CompressFormat.JPEG, quality, fos) == false){ 546 | Log.d(LOG_TAG, "Compress/Write failed"); 547 | } 548 | } 549 | else{ 550 | // 图片内容按照指定格式压缩,并写入指定文件,如出现异常,打印异常日志 551 | if (b.compress(Bitmap.CompressFormat.PNG, quality, fos) == false){ 552 | Log.d(LOG_TAG, "Compress/Write failed"); 553 | } 554 | } 555 | // 关闭写文件流 556 | fos.flush(); 557 | fos.close(); 558 | } catch (Exception e) { 559 | // 日常记录logcat日志,并打印异常堆栈 560 | Log.d(LOG_TAG, "Can't save the screenshot! Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test."); 561 | e.printStackTrace(); 562 | } 563 | } 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /com/robotium/solo/Scroller.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import com.robotium.solo.Solo.Config; 5 | import junit.framework.Assert; 6 | import android.app.Instrumentation; 7 | import android.os.SystemClock; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | import android.webkit.WebView; 11 | import android.widget.AbsListView; 12 | import android.widget.GridView; 13 | import android.widget.ListView; 14 | import android.widget.ScrollView; 15 | 16 | 17 | /** 18 | * 滚动条操作工具类 19 | * Contains scroll methods. Examples are scrollDown(), scrollUpList(), 20 | * scrollToSide(). 21 | * 22 | * @author Renas Reda, renas.reda@robotium.com 23 | * 24 | */ 25 | 26 | class Scroller { 27 | // 向下 28 | public static final int DOWN = 0; 29 | // 向下 30 | public static final int UP = 1; 31 | // 左右 枚举 32 | public enum Side {LEFT, RIGHT} 33 | // 是否可以拖动 34 | private boolean canScroll = false; 35 | // Instrument对象 36 | private final Instrumentation inst; 37 | // Activity工具类 38 | private final ActivityUtils activityUtils; 39 | // View获取工具类 40 | private final ViewFetcher viewFetcher; 41 | // 延时工具类 42 | private final Sleeper sleeper; 43 | // Robotium属性配置类 44 | private final Config config; 45 | 46 | 47 | /** 48 | * 构造函数 49 | * Constructs this object. 50 | * 51 | * @param inst the {@code Instrumentation} instance 52 | * @param activityUtils the {@code ActivityUtils} instance 53 | * @param viewFetcher the {@code ViewFetcher} instance 54 | * @param sleeper the {@code Sleeper} instance 55 | */ 56 | 57 | public Scroller(Config config, Instrumentation inst, ActivityUtils activityUtils, ViewFetcher viewFetcher, Sleeper sleeper) { 58 | this.config = config; 59 | this.inst = inst; 60 | this.activityUtils = activityUtils; 61 | this.viewFetcher = viewFetcher; 62 | this.sleeper = sleeper; 63 | } 64 | 65 | 66 | /** 67 | * 按住并且拖动到指定位置 68 | * fromX 起始X坐标 69 | * toX 终点X坐标 70 | * fromY 起始Y坐标 71 | * toY 终点Y坐标 72 | * stepCount 动作拆分成几步 73 | * Simulate touching a specific location and dragging to a new location. 74 | * 75 | * This method was copied from {@code TouchUtils.java} in the Android Open Source Project, and modified here. 76 | * 77 | * @param fromX X coordinate of the initial touch, in screen coordinates 78 | * @param toX Xcoordinate of the drag destination, in screen coordinates 79 | * @param fromY X coordinate of the initial touch, in screen coordinates 80 | * @param toY Y coordinate of the drag destination, in screen coordinates 81 | * @param stepCount How many move steps to include in the drag 82 | */ 83 | 84 | public void drag(float fromX, float toX, float fromY, float toY, 85 | int stepCount) { 86 | // 获取当前系统时间,构造MontionEvent使用 87 | long downTime = SystemClock.uptimeMillis(); 88 | // 获取当前系统时间,构造MontionEvent使用 89 | long eventTime = SystemClock.uptimeMillis(); 90 | float y = fromY; 91 | float x = fromX; 92 | // 计算每次增加Y坐标量 93 | float yStep = (toY - fromY) / stepCount; 94 | // 计算每次增加X坐标量 95 | float xStep = (toX - fromX) / stepCount; 96 | // 构造MotionEvent,先按住 97 | MotionEvent event = MotionEvent.obtain(downTime, eventTime,MotionEvent.ACTION_DOWN, fromX, fromY, 0); 98 | try { 99 | // 通过Instrument发送按住事件 100 | inst.sendPointerSync(event); 101 | // 抓取可能出现的异常 102 | } catch (SecurityException ignored) {} 103 | // 按照设置的步数,发送Move事件 104 | for (int i = 0; i < stepCount; ++i) { 105 | y += yStep; 106 | x += xStep; 107 | eventTime = SystemClock.uptimeMillis(); 108 | // 构造 MOVE事件 109 | event = MotionEvent.obtain(downTime, eventTime,MotionEvent.ACTION_MOVE, x, y, 0); 110 | try { 111 | // 通过Instrument发送按住事件 112 | inst.sendPointerSync(event); 113 | // 抓取可能出现的异常 114 | } catch (SecurityException ignored) {} 115 | } 116 | // 获取系统当前时间 117 | eventTime = SystemClock.uptimeMillis(); 118 | // 构造松开事件 119 | event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP,toX, toY, 0); 120 | try { 121 | inst.sendPointerSync(event); 122 | } catch (SecurityException ignored) {} 123 | } 124 | 125 | 126 | /** 127 | * 按照设定的方法拖动滚动条,已经处于顶部的,调用拖动到顶部无效 128 | * view 带滚动条的View控件 129 | * 130 | * Scrolls a ScrollView. 131 | * direction 拖动方向 0 滚动条向上拉动,1滚动条向下拉动 132 | * @param direction the direction to be scrolled 133 | * @return {@code true} if scrolling occurred, false if it did not 134 | */ 135 | 136 | private boolean scrollScrollView(final ScrollView view, int direction){ 137 | // null 检查,比较传入null参数引发异常 138 | if(view == null){ 139 | return false; 140 | } 141 | // 获取控件的高度 142 | int height = view.getHeight(); 143 | // 高度减小一个像素 144 | height--; 145 | int scrollTo = -1; 146 | // 向上拉动,设置成滚动条的高度,拉到顶部 147 | if (direction == DOWN) { 148 | scrollTo = height; 149 | } 150 | // 向下拉动,设置成负值,拉到底部 151 | else if (direction == UP) { 152 | scrollTo = -height; 153 | } 154 | // 获取当前滚动的高度位置 155 | int originalY = view.getScrollY(); 156 | final int scrollAmount = scrollTo; 157 | inst.runOnMainSync(new Runnable(){ 158 | public void run(){ 159 | view.scrollBy(0, scrollAmount); 160 | } 161 | }); 162 | // 滚动条坐标未变化,标识本次拖动动作失败.已经处于顶端了,触发无效果 163 | if (originalY == view.getScrollY()) { 164 | return false; 165 | } 166 | else{ 167 | return true; 168 | } 169 | } 170 | 171 | /** 172 | * 滚动条滑到底部或者顶部,已经处于顶部,调用该方法拖动到顶部将引发死循环 173 | * Scrolls a ScrollView to top or bottom. 174 | * 175 | * @param direction the direction to be scrolled 176 | */ 177 | 178 | private void scrollScrollViewAllTheWay(final ScrollView view, final int direction) { 179 | while(scrollScrollView(view, direction)); 180 | } 181 | 182 | /** 183 | * 拖动到顶部或者底部,0拖动到顶部,1拖动到底部 184 | * Scrolls up or down. 185 | * 186 | * @param direction the direction in which to scroll 187 | * @return {@code true} if more scrolling can be done 188 | */ 189 | 190 | public boolean scroll(int direction) { 191 | return scroll(direction, false); 192 | } 193 | 194 | /** 195 | * 拖动到顶部 196 | * Scrolls down. 197 | * 198 | * @return {@code true} if more scrolling can be done 199 | */ 200 | 201 | public boolean scrollDown() { 202 | // 如果配置设置了禁止拖动,那么将不拖动控件 203 | if(!config.shouldScroll) { 204 | return false; 205 | } 206 | // 拖动到顶部 207 | return scroll(Scroller.DOWN); 208 | } 209 | 210 | /** 211 | * 拖动当前页面的可拖动控件 212 | * direction 0拖动到顶部,1拖动到底部 213 | * Scrolls up and down. 214 | * 215 | * @param direction the direction in which to scroll 216 | * @param allTheWay true if the view should be scrolled to the beginning or end, 217 | * false to scroll one page up or down. 218 | * @return {@code true} if more scrolling can be done 219 | */ 220 | 221 | public boolean scroll(int direction, boolean allTheWay) { 222 | // 获取所有的Clicker可操作Views 223 | final ArrayList viewList = RobotiumUtils. 224 | removeInvisibleViews(viewFetcher.getAllViews(true)); 225 | // 获取所有可以拖动操作的views 226 | @SuppressWarnings("unchecked") 227 | ArrayList views = RobotiumUtils.filterViewsToSet(new Class[] { ListView.class, 228 | ScrollView.class, GridView.class, WebView.class}, viewList); 229 | // 获取所有可视view中的最新的,即当前用户选中的可拖动控件 230 | View view = viewFetcher.getFreshestView(views); 231 | // 如果无可拖动控件,则返回 232 | if (view == null) 233 | { 234 | return false; 235 | } 236 | // 是一个列表控件,则使用列表控件方法操作 237 | if (view instanceof AbsListView) { 238 | return scrollList((AbsListView)view, direction, allTheWay); 239 | } 240 | // 如果是一个可拖动控件,则按照可拖动控件方法操作 241 | if (view instanceof ScrollView) { 242 | if (allTheWay) { 243 | scrollScrollViewAllTheWay((ScrollView) view, direction); 244 | return false; 245 | } else { 246 | return scrollScrollView((ScrollView)view, direction); 247 | } 248 | } 249 | // 如果是一个WebView控件,则按照WebView方法操作 250 | if(view instanceof WebView){ 251 | return scrollWebView((WebView)view, direction, allTheWay); 252 | } 253 | // 非上述控件类型,返回false 254 | return false; 255 | } 256 | 257 | /** 258 | * WebView 控件 拖动操作. 259 | * Scrolls a WebView. 260 | * 261 | * webView 传入的WebView 262 | * direction 操作方向,0拖动到顶部,1拖动到底部 263 | * allTheWay true标识拖动到底部或顶部,false标识不拖动 264 | * 事件发送成功返回true 失败返回false 265 | * @param webView the WebView to scroll 266 | * @param direction the direction to scroll 267 | * @param allTheWay {@code true} to scroll the view all the way up or down, {@code false} to scroll one page up or down or down. 268 | * @return {@code true} if more scrolling can be done 269 | */ 270 | 271 | public boolean scrollWebView(final WebView webView, int direction, final boolean allTheWay){ 272 | 273 | if (direction == DOWN) { 274 | // 调用Instrument发送拖动事件 275 | inst.runOnMainSync(new Runnable(){ 276 | public void run(){ 277 | // 拖动到底部 278 | canScroll = webView.pageDown(allTheWay); 279 | } 280 | }); 281 | } 282 | if(direction == UP){ 283 | // 调用Instrument发送拖动事件 284 | inst.runOnMainSync(new Runnable(){ 285 | public void run(){ 286 | // 拖动到底部 287 | canScroll = webView.pageUp(allTheWay); 288 | } 289 | }); 290 | } 291 | // 返回事件发送是否成功 292 | return canScroll; 293 | } 294 | 295 | /** 296 | * 拖动一个列表 297 | * Scrolls a list. 298 | * absListView AbsListView类型的,即列表类控件 299 | * direction 拖动方向0最顶部,1最底部 300 | * @param absListView the list to be scrolled 301 | * @param direction the direction to be scrolled 302 | * @param allTheWay {@code true} to scroll the view all the way up or down, {@code false} to scroll one page up or down 303 | * @return {@code true} if more scrolling can be done 304 | */ 305 | 306 | public boolean scrollList(T absListView, int direction, boolean allTheWay) { 307 | // 非null校验 308 | if(absListView == null){ 309 | return false; 310 | } 311 | // 拖动到底部 312 | if (direction == DOWN) { 313 | // 如果是直接拖动到底部的模式 314 | if (allTheWay) { 315 | // 拖动到最大号的行,因总数据数,会大于可视行数,因此调用此方法,永久返回false 316 | scrollListToLine(absListView, absListView.getCount()-1); 317 | return false; 318 | } 319 | // 当总行数比可见行数大时.拖动到可见行数底部,返回false. 320 | if (absListView.getLastVisiblePosition() >= absListView.getCount()-1) { 321 | scrollListToLine(absListView, absListView.getLastVisiblePosition()); 322 | return false; 323 | } 324 | // 当不是一行时,拖动到最下面的行 325 | if(absListView.getFirstVisiblePosition() != absListView.getLastVisiblePosition()) 326 | scrollListToLine(absListView, absListView.getLastVisiblePosition()); 327 | 328 | else 329 | // 当可见的只有一行时,拖动到下面一行 330 | scrollListToLine(absListView, absListView.getFirstVisiblePosition()+1); 331 | // 拖动到顶部 332 | } else if (direction == UP) { 333 | // 可见行数少于1行时,直接划到第0行 334 | if (allTheWay || absListView.getFirstVisiblePosition() < 2) { 335 | scrollListToLine(absListView, 0); 336 | return false; 337 | } 338 | // 计算显示的行数.没必要设置成final,又不是子类中使用 339 | final int lines = absListView.getLastVisiblePosition() - absListView.getFirstVisiblePosition(); 340 | // 计算未显示的剩余行数全部显示多余的行 341 | int lineToScrollTo = absListView.getFirstVisiblePosition() - lines; 342 | // 如果正好可以显示行数与当前底部位置一致,则移动到当前位置 343 | if(lineToScrollTo == absListView.getLastVisiblePosition()) 344 | lineToScrollTo--; 345 | // 如果计算位置为负值,那么直接滑到顶部 346 | if(lineToScrollTo < 0) 347 | lineToScrollTo = 0; 348 | 349 | scrollListToLine(absListView, lineToScrollTo); 350 | } 351 | sleeper.sleep(); 352 | return true; 353 | } 354 | 355 | 356 | /** 357 | * 拖动列表内容到指定的行 358 | * line 对应的行号 359 | * Scroll the list to a given line 360 | * 361 | * @param view the {@link AbsListView} to scroll 362 | * @param line the line to scroll to 363 | */ 364 | 365 | public void scrollListToLine(final T view, final int line){ 366 | // 非null校验 367 | if(view == null) 368 | Assert.fail("AbsListView is null!"); 369 | 370 | final int lineToMoveTo; 371 | // 如果是gridview类型的,带标题,因此行数+1 372 | if(view instanceof GridView) 373 | lineToMoveTo = line+1; 374 | else 375 | lineToMoveTo = line; 376 | // 发送拖动事件 377 | inst.runOnMainSync(new Runnable(){ 378 | public void run(){ 379 | view.setSelection(lineToMoveTo); 380 | } 381 | }); 382 | } 383 | 384 | 385 | /** 386 | * 横向拖动,拖动默认拆分成40步操作 387 | * side 指定拖动方向 388 | * scrollPosition 拖动百分比0-1. 389 | * Scrolls horizontally. 390 | * 391 | * @param side the side to which to scroll; {@link Side#RIGHT} or {@link Side#LEFT} 392 | * @param scrollPosition the position to scroll to, from 0 to 1 where 1 is all the way. Example is: 0.55. 393 | */ 394 | 395 | @SuppressWarnings("deprecation") 396 | public void scrollToSide(Side side, float scrollPosition) { 397 | // 获取屏幕高度 398 | int screenHeight = activityUtils.getCurrentActivity().getWindowManager().getDefaultDisplay() 399 | .getHeight(); 400 | // 获取屏幕宽度 401 | int screenWidth = activityUtils.getCurrentActivity(false).getWindowManager().getDefaultDisplay() 402 | .getWidth(); 403 | // 按照宽度计算总距离 404 | float x = screenWidth * scrollPosition; 405 | // 拖动选择屏幕正中间 406 | float y = screenHeight / 2.0f; 407 | //往左拖动 408 | if (side == Side.LEFT) 409 | drag(0, x, y, y, 40); 410 | // 往右拖动 411 | else if (side == Side.RIGHT) 412 | drag(x, 0, y, y, 40); 413 | } 414 | 415 | /** 416 | * 对给定控件进行向左或向右拖动操作.默认拖动距离拆分成40步 417 | * view 需要拖动操作的控件 418 | * side 拖动方向 419 | * scrollPosition 拖动距离,按照屏幕宽度百分比计算,值为0-1 420 | * Scrolls view horizontally. 421 | * 422 | * @param view the view to scroll 423 | * @param side the side to which to scroll; {@link Side#RIGHT} or {@link Side#LEFT} 424 | * @param scrollPosition the position to scroll to, from 0 to 1 where 1 is all the way. Example is: 0.55. 425 | */ 426 | 427 | public void scrollViewToSide(View view, Side side, float scrollPosition) { 428 | // 临时变量,存储控件在手机屏幕中的相对坐标 429 | int[] corners = new int[2]; 430 | // 获取相对坐标 431 | view.getLocationOnScreen(corners); 432 | // 获取高度相对坐标 433 | int viewHeight = view.getHeight(); 434 | // 获取宽度相对坐标 435 | int viewWidth = view.getWidth(); 436 | // 计算拖动开始x坐标 437 | float x = corners[0] + viewWidth * scrollPosition; 438 | // 计算拖动开始y坐标 439 | float y = corners[1] + viewHeight / 2.0f; 440 | // 往左拖动 441 | if (side == Side.LEFT) 442 | drag(corners[0], x, y, y, 40); 443 | // 往右拖动 444 | else if (side == Side.RIGHT) 445 | drag(x, corners[0], y, y, 40); 446 | } 447 | 448 | } 449 | -------------------------------------------------------------------------------- /com/robotium/solo/Searcher.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.concurrent.Callable; 9 | import android.os.SystemClock; 10 | import android.util.Log; 11 | import android.view.View; 12 | import android.widget.TextView; 13 | 14 | 15 | /** 16 | * 控件搜索工具类,可以按照各种关键信息查找对应控件 17 | * Contains various search methods. Examples are: searchForEditTextWithTimeout(), 18 | * searchForTextWithTimeout(), searchForButtonWithTimeout(). 19 | * 20 | * @author Renas Reda, renas.reda@robotium.com 21 | * 22 | */ 23 | 24 | class Searcher { 25 | // View 操作工具类 26 | private final ViewFetcher viewFetcher; 27 | // WebView操作工具类 28 | private final WebUtils webUtils; 29 | // 滑动控件操作工具类 30 | private final Scroller scroller; 31 | // 延时操作工具类 32 | private final Sleeper sleeper; 33 | // 日志打印标签,标识这是 Robotium 34 | private final String LOG_TAG = "Robotium"; 35 | // 由于存储包含指定text正则的views,检查内容包括显示内容,错误提示信息和友好提示信息 36 | Set uniqueTextViews; 37 | // WebElement缓存 38 | List webElements; 39 | // 统计非重复View数量 40 | private int numberOfUniqueViews; 41 | // 默认超时5s 42 | private final int TIMEOUT = 5000; 43 | 44 | 45 | /** 46 | * 构造函数 47 | * Constructs this object. 48 | * 49 | * @param viewFetcher the {@code ViewFetcher} instance 50 | * @param webUtils the {@code WebUtils} instance 51 | * @param scroller the {@code Scroller} instance 52 | * @param sleeper the {@code Sleeper} instance. 53 | */ 54 | 55 | public Searcher(ViewFetcher viewFetcher, WebUtils webUtils, Scroller scroller, Sleeper sleeper) { 56 | this.viewFetcher = viewFetcher; 57 | this.webUtils = webUtils; 58 | this.scroller = scroller; 59 | this.sleeper = sleeper; 60 | webElements = new ArrayList(); 61 | uniqueTextViews = new HashSet(); 62 | } 63 | 64 | 65 | /** 66 | * 按照给定的查找条件,检查是否找到了期望的元素,找到返回true,未找到返回false 67 | * viewClass 希望的元素类型 68 | * regex text正则 69 | * expectedMinimumNumberOfMatches 期望符合该条件元素数量,数量相等返回true,不相等返回false 70 | * scroll 是否需要拖动查找,如列表未显示全部内容,拖动可以刷新内容 71 | * onlyVisible true只查找可见的,false查找全部的 72 | * Searches for a {@code View} with the given regex string and returns {@code true} if the 73 | * searched {@code Button} is found a given number of times. Will automatically scroll when needed. 74 | * 75 | * @param viewClass what kind of {@code View} to search for, e.g. {@code Button.class} or {@code TextView.class} 76 | * @param regex the text to search for. The parameter will be interpreted as a regular expression. 77 | * @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more 78 | * matches are expected to be found 79 | * @param scroll whether scrolling should be performed 80 | * @param onlyVisible {@code true} if only texts visible on the screen should be searched 81 | * 82 | * @return {@code true} if a {@code View} of the specified class with the given text is found a given number of 83 | * times, and {@code false} if it is not found 84 | */ 85 | 86 | public boolean searchWithTimeoutFor(Class viewClass, String regex, int expectedMinimumNumberOfMatches, boolean scroll, boolean onlyVisible) { 87 | // 设定超时时间,当前时间加上5s 88 | final long endTime = SystemClock.uptimeMillis() + TIMEOUT; 89 | // 初始化临时变量为null 90 | TextView foundAnyMatchingView = null; 91 | // 如果还没到达指定时间还为找到则继续查找 92 | while (SystemClock.uptimeMillis() < endTime) { 93 | // 等500ms 94 | sleeper.sleep(); 95 | // 按照给定的条件调用查询方法,超时设置为0 96 | foundAnyMatchingView = searchFor(viewClass, regex, expectedMinimumNumberOfMatches, 0, scroll, onlyVisible); 97 | // 找到则直接返回 98 | if (foundAnyMatchingView !=null){ 99 | return true; 100 | } 101 | } 102 | return false; 103 | } 104 | 105 | 106 | /** 107 | * 按照给定的条件查找TextView类型的View 108 | * viewClass 设置的class类型 109 | * regex text正则表达式 110 | * expectedMinimumNumberOfMatches 期望的View数量,如果总数不是期望的那么返回null,总数一致返回第expectedMinimumNumberOfMatches个 111 | * timeout 超时时间 112 | * scroll 是否需要拖动查找,如列表未显示全部内容,拖动可以刷新内容 113 | * onlyVisible true只查找可见的,false查找全部的 114 | * Searches for a {@code View} with the given regex string and returns {@code true} if the 115 | * searched {@code View} is found a given number of times. 116 | * 117 | * @param viewClass what kind of {@code View} to search for, e.g. {@code Button.class} or {@code TextView.class} 118 | * @param regex the text to search for. The parameter will be interpreted as a regular expression. 119 | * @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more 120 | * matches are expected to be found. 121 | * @param timeout the amount of time in milliseconds to wait 122 | * @param scroll whether scrolling should be performed 123 | * @param onlyVisible {@code true} if only texts visible on the screen should be searched 124 | * 125 | * @return {@code true} if a view of the specified class with the given text is found a given number of times. 126 | * {@code false} if it is not found. 127 | */ 128 | 129 | public T searchFor(final Class viewClass, final String regex, int expectedMinimumNumberOfMatches, final long timeout, final boolean scroll, final boolean onlyVisible) { 130 | // 如果设置的期望配匹次数小于1次,则默认配置为1次 131 | if(expectedMinimumNumberOfMatches < 1) { 132 | expectedMinimumNumberOfMatches = 1; 133 | } 134 | // 构造可在子线程中调用的集合类 135 | final Callable> viewFetcherCallback = new Callable>() { 136 | @SuppressWarnings("unchecked") 137 | public Collection call() throws Exception { 138 | // 等待500ms 139 | sleeper.sleep(); 140 | // 获取当前屏幕的所有views,类型为viewClass所指定的 141 | ArrayList viewsToReturn = viewFetcher.getCurrentViews(viewClass); 142 | // 如果配置了只查找可见view中的内容,那么过滤掉所有非可见的 143 | if(onlyVisible){ 144 | viewsToReturn = RobotiumUtils.removeInvisibleViews(viewsToReturn); 145 | } 146 | // 检查是否是TextView类型的,如果是查找TextView类型的,且当前屏幕内容包含WebView.那么也把WebView中的相关TextView类元素全部加入返回列表 147 | if(viewClass.isAssignableFrom(TextView.class)) { 148 | viewsToReturn.addAll((Collection) webUtils.getTextViewsFromWebView()); 149 | } 150 | // 返回找到的views 151 | return viewsToReturn; 152 | } 153 | }; 154 | 155 | try { 156 | // 查找相关view 157 | return searchFor(viewFetcherCallback, regex, expectedMinimumNumberOfMatches, timeout, scroll); 158 | } catch (Exception e) { 159 | throw new RuntimeException(e); 160 | } 161 | } 162 | 163 | /** 164 | * 对应class类型的View数量是否<=index 165 | * 166 | * unqiueViews 给定的views 167 | * viewClass 指定的view类型 168 | * index 指定的数量从0开始计数 169 | * Searches for a view class. 170 | * 171 | * @param uniqueViews the set of unique views 172 | * @param viewClass the view class to search for 173 | * @param index the index of the view class 174 | * @return true if view class if found a given number of times 175 | */ 176 | 177 | public boolean searchFor(Set uniqueViews, Class viewClass, final int index) { 178 | // 获取当前界面的所有给定 class类型的可见View 179 | ArrayList allViews = RobotiumUtils.removeInvisibleViews(viewFetcher.getCurrentViews(viewClass)); 180 | // 返回不重复的view数量,并把allViews加入到uniqueViews集合中 181 | int uniqueViewsFound = (getNumberOfUniqueViews(uniqueViews, allViews)); 182 | // index位置在总数量中,返回true,可以获取 183 | if(uniqueViewsFound > 0 && index < uniqueViewsFound) { 184 | return true; 185 | } 186 | // 上面的条件满足,这句代码执行不到 187 | if(uniqueViewsFound > 0 && index == 0) { 188 | return true; 189 | } 190 | // index值大于等于总数,返回false,越界了 191 | return false; 192 | } 193 | 194 | /** 195 | * 检查view是否在当前屏幕中.是返回true.不在返回false 196 | * Searches for a given view. 197 | * 198 | * @param view the view to search 199 | * @param scroll true if scrolling should be performed 200 | * @return true if view is found 201 | */ 202 | 203 | public boolean searchFor(View view) { 204 | // 获取当前屏幕中所有的可见view 205 | ArrayList views = viewFetcher.getAllViews(true); 206 | for(View v : views){ 207 | // 判断view是否在当前屏幕中,是返回true 208 | if(v.equals(view)){ 209 | return true; 210 | } 211 | } 212 | // 不存在返回false 213 | return false; 214 | } 215 | 216 | /** 217 | * 按照调用方法传入的class类型,text需要匹配的正则,第几个匹配的元素,查找符合条件的 View,找到的元素总数与期望的不一致,那么返回null,符合期望的,那么返回列表中的最后一个元素 218 | * regex 字符串正则 219 | * expectedMinimumNumberOfMatches 查找第几个 220 | * timeout 查找超时时间 221 | * scroll 是否需要滑动,如列表中的不可见元素,滑动一下就可见了,设置了true就可以找到 222 | * Searches for a {@code View} with the given regex string and returns {@code true} if the 223 | * searched {@code View} is found a given number of times. Will not scroll, because the caller needs to find new 224 | * {@code View}s to evaluate after scrolling, and call this method again. 225 | * 226 | * @param viewFetcherCallback callback which should return an updated collection of views to search 227 | * @param regex the text to search for. The parameter will be interpreted as a regular expression. 228 | * @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more 229 | * matches are expected to be found. 230 | * @param timeout the amount of time in milliseconds to wait 231 | * @param scroll whether scrolling should be performed 232 | * 233 | * @return {@code true} if a view of the specified class with the given text is found a given number of times. 234 | * {@code false} if it is not found. 235 | * 236 | * @throws Exception not really, it's just the signature of {@code Callable} 237 | */ 238 | 239 | public T searchFor(Callable> viewFetcherCallback, String regex, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) throws Exception { 240 | // 设置超时时间点 241 | final long endTime = SystemClock.uptimeMillis() + timeout; 242 | Collection views; 243 | 244 | while (true) { 245 | // 检查是否已过设定的超时点 246 | final boolean timedOut = timeout > 0 && SystemClock.uptimeMillis() > endTime; 247 | // 已经超时则直接退出查询,并打印相关日志记录 248 | if(timedOut){ 249 | logMatchesFound(regex); 250 | return null; 251 | } 252 | // 获取给定条件过滤后的所有Views 253 | views = viewFetcherCallback.call(); 254 | 255 | for(T view : views){ 256 | // 检查是否找到了期望的数量,如果找到了期望数量的元素,那么清空缓存,返回找到的对应View 257 | if (RobotiumUtils.getNumberOfMatches(regex, view, uniqueTextViews) == expectedMinimumNumberOfMatches) { 258 | uniqueTextViews.clear(); 259 | return view; 260 | } 261 | } 262 | // 如果配置了可拖动,但是当前不允许拖动,那么记录异常日志,返回null,由Config中配置是否可拖动,默认为true 263 | if(scroll && !scroller.scrollDown()){ 264 | logMatchesFound(regex); 265 | return null; 266 | } 267 | // 如果未设置可拖动,记录异常日志,返回null 268 | if(!scroll){ 269 | logMatchesFound(regex); 270 | return null; 271 | } 272 | } 273 | } 274 | 275 | /** 276 | * 按照给定的By条件,查找 WebView中的WebElement,minimumNumberOfMatches 指定需要返回第几个 277 | * Searches for a web element. 278 | * 279 | * @param by the By object e.g. By.id("id"); 280 | * @param minimumNumberOfMatches the minimum number of matches that are expected to be shown. {@code 0} means any number of matches 281 | * @return the web element or null if not found 282 | */ 283 | 284 | public WebElement searchForWebElement(final By by, int minimumNumberOfMatches){ 285 | // 如果传入数量小于1,那么默认设置为1 286 | if(minimumNumberOfMatches < 1){ 287 | minimumNumberOfMatches = 1; 288 | } 289 | // 使用by作为过滤条件.获取当前的所有WebElement 290 | List viewsFromScreen = webUtils.getCurrentWebElements(by); 291 | // viewsFromScreen中的元素合并到webElement中,并且去重,text,xy坐标一致作为重复判定条件 292 | addViewsToList (webElements, viewsFromScreen); 293 | // 返回指定的WebElement 294 | return getViewFromList(webElements, minimumNumberOfMatches); 295 | } 296 | 297 | /** 298 | * 列表合并,webElementsOnScreen加入到allWebElements中 299 | * 使用 text,xy坐标位置作为2个列表中元素是否重复判断条件 300 | * Adds views to a given list. 301 | * 302 | * @param allWebElements the list of all views 303 | * @param webTextViewsOnScreen the list of views shown on screen 304 | */ 305 | 306 | private void addViewsToList(List allWebElements, List webElementsOnScreen){ 307 | // 缓存allWebElements中元素的xy坐标 308 | int[] xyViewFromSet = new int[2]; 309 | // 缓存webZElementOnScreen元素的位置xy坐标 310 | int[] xyViewFromScreen = new int[2]; 311 | // 遍历 312 | for(WebElement textFromScreen : webElementsOnScreen){ 313 | boolean foundView = false; 314 | // 获取屏幕xy坐标 315 | textFromScreen.getLocationOnScreen(xyViewFromScreen); 316 | 317 | for(WebElement textFromList : allWebElements){ 318 | // 获取屏幕对应的xy坐标 319 | textFromList.getLocationOnScreen(xyViewFromSet); 320 | // allWebElements中已存在的则不重复加入,按照text和xy坐标作为是否相等的条件 321 | if(textFromScreen.getText().equals(textFromList.getText()) && xyViewFromScreen[0] == xyViewFromSet[0] && xyViewFromScreen[1] == xyViewFromSet[1]) { 322 | foundView = true; 323 | } 324 | } 325 | // 不符合重复判断条件的加入allWebElements列表 326 | if(!foundView){ 327 | allWebElements.add(textFromScreen); 328 | } 329 | } 330 | 331 | } 332 | 333 | /** 334 | * 获取列表中指定位置的元素,数组越界则返回null 335 | * Returns a text view with a given match. 336 | * 337 | * @param webElements the list of views 338 | * @param match the match of the view to return 339 | * @return the view with a given match 340 | */ 341 | 342 | private WebElement getViewFromList(List webElements, int match){ 343 | 344 | WebElement webElementToReturn = null; 345 | // 检查是否超过列表中WebElement总数,超过则返回null 346 | if(webElements.size() >= match){ 347 | 348 | try{ 349 | // 获取对应位置元素 350 | webElementToReturn = webElements.get(--match); 351 | }catch(Exception ignored){} 352 | } 353 | //找到元素则清空缓存 354 | if(webElementToReturn != null) 355 | webElements.clear(); 356 | // 返回元素 357 | return webElementToReturn; 358 | } 359 | 360 | /** 361 | * 把views中的View 加入到uniqueViews集合中,并且去重,返回unqiueViews中的View总数量 362 | * Returns the number of unique views. 363 | * 364 | * @param uniqueViews the set of unique views 365 | * @param views the list of all views 366 | * @return number of unique views 367 | */ 368 | 369 | public int getNumberOfUniqueViews(SetuniqueViews, ArrayList views){ 370 | // 把view加入set中,set类型保证了不会存在重复的view 371 | for(int i = 0; i < views.size(); i++){ 372 | uniqueViews.add(views.get(i)); 373 | } 374 | // 获取uniqueViews中的View总数 375 | numberOfUniqueViews = uniqueViews.size(); 376 | return numberOfUniqueViews; 377 | } 378 | 379 | /** 380 | * 获取unqiueViews中的View数量 381 | * Returns the number of unique views. 382 | * 383 | * @return the number of unique views 384 | */ 385 | 386 | public int getNumberOfUniqueViews(){ 387 | return numberOfUniqueViews; 388 | } 389 | 390 | /** 391 | * 打印搜索失败日志.并清空缓存内容 392 | * Logs a (searchFor failed) message. 393 | * 394 | * @param regex the search string to log 395 | */ 396 | 397 | public void logMatchesFound(String regex){ 398 | // 打印当前TextView总数和搜索条件 399 | if (uniqueTextViews.size() > 0) { 400 | Log.d(LOG_TAG, " There are only " + uniqueTextViews.size() + " matches of '" + regex + "'"); 401 | } 402 | // 打印当前WebView中的Element数量和搜索条件 403 | else if(webElements.size() > 0){ 404 | Log.d(LOG_TAG, " There are only " + webElements.size() + " matches of '" + regex + "'"); 405 | } 406 | // 清理缓存内容 407 | uniqueTextViews.clear(); 408 | webElements.clear(); 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /com/robotium/solo/Sender.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | 4 | import junit.framework.Assert; 5 | import android.app.Instrumentation; 6 | import android.view.KeyEvent; 7 | 8 | /** 9 | * 用于发送各类按键事件 10 | * Contains send key event methods. Examples are: 11 | * sendKeyCode(), goBack() 12 | * 13 | * @author Renas Reda, renas.reda@robotium.com 14 | * 15 | */ 16 | 17 | class Sender { 18 | // Instrument,用于发送各类事件 19 | private final Instrumentation inst; 20 | // 等待工具类 21 | private final Sleeper sleeper; 22 | 23 | /** 24 | * 构造函数 25 | * Constructs this object. 26 | * 27 | * @param inst the {@code Instrumentation} instance 28 | * @param sleeper the {@code Sleeper} instance 29 | */ 30 | 31 | Sender(Instrumentation inst, Sleeper sleeper) { 32 | this.inst = inst; 33 | this.sleeper = sleeper; 34 | } 35 | 36 | /** 37 | * 发送各类按键事件. 38 | * Tells Robotium to send a key code: Right, Left, Up, Down, Enter or other. 39 | * 40 | * @param keycode the key code to be sent. Use {@link KeyEvent#KEYCODE_ENTER}, {@link KeyEvent#KEYCODE_MENU}, {@link KeyEvent#KEYCODE_DEL}, {@link KeyEvent#KEYCODE_DPAD_RIGHT} and so on 41 | */ 42 | 43 | public void sendKeyCode(int keycode) 44 | { 45 | sleeper.sleep(); 46 | try{ 47 | inst.sendCharacterSync(keycode); 48 | // 捕获可能遇到的权限问题 49 | }catch(SecurityException e){ 50 | // 日志提醒,该操作无权和相关错误日志 51 | Assert.fail("Can not complete action! ("+(e != null ? e.getClass().getName()+": "+e.getMessage() : "null")+")"); 52 | } 53 | } 54 | 55 | /** 56 | * 发送返回事件,即点击一下返回按钮 57 | * Simulates pressing the hardware back key. 58 | */ 59 | 60 | public void goBack() { 61 | // 等待500ms 62 | sleeper.sleep(); 63 | try { 64 | // 发送返回事件 65 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 66 | // 等待500ms 67 | sleeper.sleep(); 68 | } catch (Throwable ignored) {} 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /com/robotium/solo/Setter.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.widget.DatePicker; 4 | import android.widget.ProgressBar; 5 | import android.widget.SlidingDrawer; 6 | import android.widget.TimePicker; 7 | 8 | 9 | /** 10 | * 设置类控件操作工具类 11 | * Contains set methods. Examples are setDatePicker(), 12 | * setTimePicker(). 13 | * 14 | * @author Renas Reda, renas.reda@robotium.com 15 | * 16 | */ 17 | 18 | class Setter{ 19 | // 关闭为0 20 | private final int CLOSED = 0; 21 | // 打开为1 22 | private final int OPENED = 1; 23 | // activity操作工具类 24 | private final ActivityUtils activityUtils; 25 | 26 | /** 27 | * 构造函数 28 | * Constructs this object. 29 | * 30 | * @param activityUtils the {@code ActivityUtils} instance 31 | */ 32 | 33 | public Setter(ActivityUtils activityUtils) { 34 | this.activityUtils = activityUtils; 35 | } 36 | 37 | 38 | /** 39 | * 设置控件日期 40 | * datePicker 需要设置日期的控件 41 | * year 年 42 | * monthOfYear 月 43 | * dayOfMonth 日 44 | * Sets the date in a given {@link DatePicker}. 45 | * 46 | * @param datePicker the {@code DatePicker} object. 47 | * @param year the year e.g. 2011 48 | * @param monthOfYear the month which is starting from zero e.g. 03 49 | * @param dayOfMonth the day e.g. 10 50 | */ 51 | 52 | public void setDatePicker(final DatePicker datePicker, final int year, final int monthOfYear, final int dayOfMonth) { 53 | // 非空判断 54 | if(datePicker != null){ 55 | // 在当前activity的Ui线程中执行,直接调用会引发跨线程权限异常 56 | activityUtils.getCurrentActivity(false).runOnUiThread(new Runnable() 57 | { 58 | public void run() 59 | { 60 | try{ 61 | // 设置年月日属性 62 | datePicker.updateDate(year, monthOfYear, dayOfMonth); 63 | }catch (Exception ignored){} 64 | } 65 | }); 66 | } 67 | } 68 | 69 | 70 | /** 71 | * 设置时间属性 72 | * timePicker 需要设置属性的TimePicker控件 73 | * hour 时 74 | * minute 分 75 | * Sets the time in a given {@link TimePicker}. 76 | * 77 | * @param timePicker the {@code TimePicker} object. 78 | * @param hour the hour e.g. 15 79 | * @param minute the minute e.g. 30 80 | */ 81 | 82 | public void setTimePicker(final TimePicker timePicker, final int hour, final int minute) { 83 | // 非空检查 84 | if(timePicker != null){ 85 | // 在当前activity的Ui线程中执行,直接调用会引发跨线程权限异常 86 | activityUtils.getCurrentActivity(false).runOnUiThread(new Runnable() 87 | { 88 | public void run() 89 | { 90 | try{ 91 | // 设置时 92 | timePicker.setCurrentHour(hour); 93 | // 设置分 94 | timePicker.setCurrentMinute(minute); 95 | }catch (Exception ignored){} 96 | } 97 | }); 98 | } 99 | } 100 | 101 | 102 | /** 103 | * 设置进度条控件属性 104 | * progressBar 需要设置的进度条 105 | * progress 设置的值 106 | * Sets the progress of a given {@link ProgressBar}. Examples are SeekBar and RatingBar. 107 | * @param progressBar the {@code ProgressBar} 108 | * @param progress the progress that the {@code ProgressBar} should be set to 109 | */ 110 | 111 | public void setProgressBar(final ProgressBar progressBar,final int progress) { 112 | // 非空检查 113 | if(progressBar != null){ 114 | // 在当前activity的Ui线程中执行,直接调用会引发跨线程权限异常 115 | activityUtils.getCurrentActivity(false).runOnUiThread(new Runnable() 116 | { 117 | public void run() 118 | { 119 | try{ 120 | // 设置进度属性 121 | progressBar.setProgress(progress); 122 | }catch (Exception ignored){} 123 | } 124 | }); 125 | } 126 | } 127 | 128 | 129 | /** 130 | * 设置选择开关属性,开 关 131 | * slidingDrawer 需要设置的选择开关 132 | * status Solo.CLOSED Solo.OPENED 133 | * Sets the status of a given SlidingDrawer. Examples are Solo.CLOSED and Solo.OPENED. 134 | * 135 | * @param slidingDrawer the {@link SlidingDrawer} 136 | * @param status the status that the {@link SlidingDrawer} should be set to 137 | */ 138 | 139 | public void setSlidingDrawer(final SlidingDrawer slidingDrawer, final int status){ 140 | // 非空判断 141 | if(slidingDrawer != null){ 142 | // 在当前activity的Ui线程中执行,直接调用会引发跨线程权限异常 143 | activityUtils.getCurrentActivity(false).runOnUiThread(new Runnable() 144 | { 145 | public void run() 146 | { 147 | try{ 148 | // 按照给定值,设定状态 149 | switch (status) { 150 | case CLOSED: 151 | slidingDrawer.close(); 152 | break; 153 | case OPENED: 154 | slidingDrawer.open(); 155 | break; 156 | } 157 | }catch (Exception ignored){} 158 | } 159 | }); 160 | } 161 | 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /com/robotium/solo/Sleeper.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | /** 3 | * 延时等待工具类 4 | * 5 | */ 6 | class Sleeper { 7 | // 常量500ms 8 | private final int PAUSE = 500; 9 | // 常量300ms 10 | private final int MINIPAUSE = 300; 11 | 12 | /** 13 | * 延时500ms 14 | * Sleeps the current thread for a default pause length. 15 | */ 16 | 17 | public void sleep() { 18 | sleep(PAUSE); 19 | } 20 | 21 | 22 | /** 23 | * 延时300ms 24 | * Sleeps the current thread for a default mini pause length. 25 | */ 26 | 27 | public void sleepMini() { 28 | sleep(MINIPAUSE); 29 | } 30 | 31 | 32 | /** 33 | * 延时指定数值的ms 34 | * Sleeps the current thread for time milliseconds. 35 | * 36 | * @param time the length of the sleep in milliseconds 37 | */ 38 | 39 | public void sleep(int time) { 40 | try { 41 | Thread.sleep(time); 42 | } catch (InterruptedException ignored) {} 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /com/robotium/solo/Swiper.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Instrumentation; 4 | import android.graphics.PointF; 5 | import android.os.SystemClock; 6 | import android.view.MotionEvent; 7 | import android.view.MotionEvent.PointerCoords; 8 | import android.view.MotionEvent.PointerProperties; 9 | // 划屏工具类 10 | class Swiper 11 | { 12 | // Instrument 用于发送事件 13 | private final Instrumentation _instrument; 14 | // 手势动作间隔1s 15 | public static final int GESTURE_DURATION_MS = 1000; 16 | // 事件间隔10ms 17 | public static final int EVENT_TIME_INTERVAL_MS = 10; 18 | // 构造函数 19 | public Swiper(Instrumentation inst) 20 | { 21 | this._instrument = inst; 22 | } 23 | // 发送划屏手势动作,2个手指点击,模拟多点触控 24 | // startPoint1 开始的第一个坐标点 25 | // startPoint2 开始的第二个坐标点 26 | // endPoint1 结束的第一个坐标点 27 | // endPoint2 结束的第二个坐标点 28 | public void generateSwipeGesture(PointF startPoint1, PointF startPoint2, 29 | PointF endPoint1, PointF endPoint2) 30 | { 31 | // 构造时间值 32 | long downTime = SystemClock.uptimeMillis(); 33 | long eventTime = SystemClock.uptimeMillis(); 34 | // 初始化开始坐标点 35 | float startX1 = startPoint1.x; 36 | float startY1 = startPoint1.y; 37 | float startX2 = startPoint2.x; 38 | float startY2 = startPoint2.y; 39 | // 初始化结束坐标点 40 | float endX1 = endPoint1.x; 41 | float endY1 = endPoint1.y; 42 | float endX2 = endPoint2.x; 43 | float endY2 = endPoint2.y; 44 | 45 | // pointer 1 46 | float x1 = startX1; 47 | float y1 = startY1; 48 | 49 | // pointer 2 50 | float x2 = startX2; 51 | float y2 = startY2; 52 | // 构造坐标点数组 53 | PointerCoords[] pointerCoords = new PointerCoords[2]; 54 | PointerCoords pc1 = new PointerCoords(); 55 | PointerCoords pc2 = new PointerCoords(); 56 | pc1.x = x1; 57 | pc1.y = y1; 58 | pc1.pressure = 1; 59 | pc1.size = 1; 60 | pc2.x = x2; 61 | pc2.y = y2; 62 | pc2.pressure = 1; 63 | pc2.size = 1; 64 | pointerCoords[0] = pc1; 65 | pointerCoords[1] = pc2; 66 | 67 | PointerProperties[] pointerProperties = new PointerProperties[2]; 68 | PointerProperties pp1 = new PointerProperties(); 69 | PointerProperties pp2 = new PointerProperties(); 70 | pp1.id = 0; 71 | pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 72 | pp2.id = 1; 73 | pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 74 | pointerProperties[0] = pp1; 75 | pointerProperties[1] = pp2; 76 | // 发送按下事件 77 | MotionEvent event; 78 | // send the initial touches 79 | event = MotionEvent.obtain(downTime, eventTime, 80 | MotionEvent.ACTION_DOWN, 1, pointerProperties, pointerCoords, 81 | 0, 0, // metaState, buttonState 82 | 1, // x precision 83 | 1, // y precision 84 | 0, 0, 0, 0); // deviceId, edgeFlags, source, flags 85 | _instrument.sendPointerSync(event); 86 | 87 | event = MotionEvent.obtain(downTime, eventTime, 88 | MotionEvent.ACTION_POINTER_DOWN 89 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 90 | 2, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0); 91 | _instrument.sendPointerSync(event); 92 | // 按照时间计算操作步骤,100步 93 | int numMoves = GESTURE_DURATION_MS / EVENT_TIME_INTERVAL_MS; 94 | // 计算每一步移动的坐标值 95 | float stepX1 = (endX1 - startX1) / numMoves; 96 | float stepY1 = (endY1 - startY1) / numMoves; 97 | float stepX2 = (endX2 - startX2) / numMoves; 98 | float stepY2 = (endY2 - startY2) / numMoves; 99 | // 不断发送滑动事件 100 | // send the zoom 101 | for (int i = 0; i < numMoves; i++) 102 | { 103 | eventTime += EVENT_TIME_INTERVAL_MS; 104 | pointerCoords[0].x += stepX1; 105 | pointerCoords[0].y += stepY1; 106 | pointerCoords[1].x += stepX2; 107 | pointerCoords[1].y += stepY2; 108 | 109 | event = MotionEvent.obtain(downTime, eventTime, 110 | MotionEvent.ACTION_MOVE, 2, pointerProperties, 111 | pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0); 112 | _instrument.sendPointerSync(event); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /com/robotium/solo/Tapper.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Instrumentation; 4 | import android.graphics.PointF; 5 | import android.os.SystemClock; 6 | import android.view.InputDevice; 7 | import android.view.MotionEvent; 8 | import android.view.MotionEvent.PointerCoords; 9 | import android.view.MotionEvent.PointerProperties; 10 | // 屏幕点击工具类 11 | class Tapper 12 | { 13 | // Instrument 用于事件发送 14 | private final Instrumentation _instrument; 15 | // 1s 16 | public static final int GESTURE_DURATION_MS = 1000; 17 | // 10ms 18 | public static final int EVENT_TIME_INTERVAL_MS = 10; 19 | // 构造函数 20 | public Tapper(Instrumentation inst) 21 | { 22 | this._instrument = inst; 23 | } 24 | // 生成屏幕点击事件 25 | // numTaps 点击次数,传入负值就死循环了... 26 | // points 点击坐标点,1-2个 27 | // 1一次点一个点,2一次点击2个点 28 | public void generateTapGesture(int numTaps, PointF... points) 29 | { 30 | // MotionEvent事件 31 | MotionEvent event; 32 | // 构造开始结束时间 33 | long downTime = SystemClock.uptimeMillis(); 34 | long eventTime = SystemClock.uptimeMillis(); 35 | 36 | // 获取相关坐标点 37 | // pointer 1 38 | float x1 = points[0].x; 39 | float y1 = points[0].y; 40 | // 如果坐标点等于2个,那么按照2个处理,否则默认第二个坐标点为0 41 | float x2 = 0; 42 | float y2 = 0; 43 | if (points.length == 2) 44 | { 45 | // pointer 2 46 | x2 = points[1].x; 47 | y2 = points[1].y; 48 | } 49 | // 构造坐标点集合 50 | PointerCoords[] pointerCoords = new PointerCoords[points.length]; 51 | PointerCoords pc1 = new PointerCoords(); 52 | pc1.x = x1; 53 | pc1.y = y1; 54 | pc1.pressure = 1; 55 | pc1.size = 1; 56 | pointerCoords[0] = pc1; 57 | PointerCoords pc2 = new PointerCoords(); 58 | if (points.length == 2) 59 | { 60 | pc2.x = x2; 61 | pc2.y = y2; 62 | pc2.pressure = 1; 63 | pc2.size = 1; 64 | pointerCoords[1] = pc2; 65 | } 66 | 67 | PointerProperties[] pointerProperties = new PointerProperties[points.length]; 68 | PointerProperties pp1 = new PointerProperties(); 69 | pp1.id = 0; 70 | pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 71 | pointerProperties[0] = pp1; 72 | PointerProperties pp2 = new PointerProperties(); 73 | if (points.length == 2) 74 | { 75 | pp2.id = 1; 76 | pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 77 | pointerProperties[1] = pp2; 78 | } 79 | // 发送构造的事件 80 | int i = 0; 81 | // 发送指定数量的点击 82 | while (i != numTaps) 83 | { // 发送第一个按下事件 84 | event = MotionEvent.obtain(downTime, eventTime, 85 | MotionEvent.ACTION_DOWN, points.length, pointerProperties, 86 | pointerCoords, 0, 0, 1, 1, 0, 0, 87 | InputDevice.SOURCE_TOUCHSCREEN, 0); 88 | _instrument.sendPointerSync(event); 89 | // 如果坐标点是2个.那么发送第二个事件 90 | if (points.length == 2) 91 | { 92 | event = MotionEvent 93 | .obtain(downTime, 94 | eventTime, 95 | MotionEvent.ACTION_POINTER_DOWN 96 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 97 | points.length, pointerProperties, 98 | pointerCoords, 0, 0, 1, 1, 0, 0, 99 | InputDevice.SOURCE_TOUCHSCREEN, 0); 100 | _instrument.sendPointerSync(event); 101 | 102 | eventTime += EVENT_TIME_INTERVAL_MS; 103 | event = MotionEvent 104 | .obtain(downTime, 105 | eventTime, 106 | MotionEvent.ACTION_POINTER_UP 107 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 108 | points.length, pointerProperties, 109 | pointerCoords, 0, 0, 1, 1, 0, 0, 110 | InputDevice.SOURCE_TOUCHSCREEN, 0); 111 | _instrument.sendPointerSync(event); 112 | } 113 | // 发送松开事件 114 | eventTime += EVENT_TIME_INTERVAL_MS; 115 | event = MotionEvent.obtain(downTime, eventTime, 116 | MotionEvent.ACTION_UP, points.length, pointerProperties, 117 | pointerCoords, 0, 0, 1, 1, 0, 0, 118 | InputDevice.SOURCE_TOUCHSCREEN, 0); 119 | _instrument.sendPointerSync(event); 120 | // 计算+1 121 | i++; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /com/robotium/solo/TextEnterer.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import junit.framework.Assert; 4 | import android.app.Instrumentation; 5 | import android.text.InputType; 6 | import android.widget.EditText; 7 | 8 | 9 | /** 10 | * 文本内容输出工具类 11 | * Contains setEditText() to enter text into text fields. 12 | * 13 | * @author Renas Reda, renas.reda@robotium.com 14 | * 15 | */ 16 | 17 | class TextEnterer{ 18 | // Instrument 用于事件发送 19 | private final Instrumentation inst; 20 | // 点击操作工具类 21 | private final Clicker clicker; 22 | // 弹框操作工具类 23 | private final DialogUtils dialogUtils; 24 | 25 | /** 26 | * 构造函数 27 | * Constructs this object. 28 | * 29 | * @param inst the {@code Instrumentation} instance 30 | * @param clicker the {@code Clicker} instance 31 | * @param dialogUtils the {@code DialogUtils} instance 32 | * 33 | */ 34 | 35 | public TextEnterer(Instrumentation inst, Clicker clicker, DialogUtils dialogUtils) { 36 | this.inst = inst; 37 | this.clicker = clicker; 38 | this.dialogUtils = dialogUtils; 39 | } 40 | 41 | /** 42 | * 设置EditText内容,如设置文本不为空,则在原有内容上追加,空则清空原有内容 43 | * editText 需要设置内容的editText 44 | * text 设置的文本内容 45 | * Sets an {@code EditText} text 46 | * 47 | * @param index the index of the {@code EditText} 48 | * @param text the text that should be set 49 | */ 50 | 51 | public void setEditText(final EditText editText, final String text) { 52 | // 非空判断 53 | if(editText != null){ 54 | // 获取原有的文本内容 55 | final String previousText = editText.getText().toString(); 56 | // 在主线程中执行,避免跨线程报错 57 | inst.runOnMainSync(new Runnable() 58 | { 59 | public void run() 60 | { 61 | // 清空原有内容 62 | editText.setInputType(InputType.TYPE_NULL); 63 | // 把焦点切换到editText 64 | editText.performClick(); 65 | // 隐藏软键盘 66 | dialogUtils.hideSoftKeyboard(editText, false, false); 67 | // 如果文本内容为空,则设置为空 68 | if(text.equals("")) 69 | editText.setText(text); 70 | // 如果非空,则在原有内容上追加 71 | else{ 72 | editText.setText(previousText + text); 73 | // 移除焦点 74 | editText.setCursorVisible(false); 75 | } 76 | } 77 | }); 78 | } 79 | } 80 | 81 | /** 82 | * 录入文本内容到EditText 83 | * editText 需要设置内容的editText 84 | * text 录入的文本内容 85 | * Types text in an {@code EditText} 86 | * 87 | * @param index the index of the {@code EditText} 88 | * @param text the text that should be typed 89 | */ 90 | 91 | public void typeText(final EditText editText, final String text){ 92 | // 非空判断 93 | if(editText != null){ 94 | // 清空原有内容 95 | inst.runOnMainSync(new Runnable() 96 | { 97 | public void run() 98 | { 99 | editText.setInputType(InputType.TYPE_NULL); 100 | } 101 | }); 102 | // editText成为当前焦点 103 | clicker.clickOnScreen(editText, false, 0); 104 | // 隐藏软键盘 105 | dialogUtils.hideSoftKeyboard(editText, true, true); 106 | 107 | boolean successfull = false; 108 | int retry = 0; 109 | // 录入文本内容,如录入失败会重试,最多10次 110 | while(!successfull && retry < 10) { 111 | 112 | try{ 113 | inst.sendStringSync(text); 114 | successfull = true; 115 | // 可能由软键盘导致异常 116 | }catch(SecurityException e){ 117 | // 隐藏软键盘 118 | dialogUtils.hideSoftKeyboard(editText, true, true); 119 | // 增加重试次数 120 | retry++; 121 | } 122 | } 123 | // 录入失败,抛错 124 | if(!successfull) { 125 | Assert.fail("Text can not be typed!"); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /com/robotium/solo/Timeout.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import com.robotium.solo.Solo.Config; 4 | 5 | 6 | 7 | 8 | /** 9 | * 超时设置类 10 | * Used to get and set the default timeout lengths of the various Solo methods. 11 | * 12 | * @author Renas Reda, renas.reda@robotium.com 13 | * 14 | */ 15 | 16 | public class Timeout{ 17 | // 长超时 18 | private static int largeTimeout; 19 | // 短超时 20 | private static int smallTimeout; 21 | 22 | 23 | /** 24 | * 设置长超时时间,未设置则使用Config设置的默认超时20s 25 | * Sets the default timeout length of the waitFor methods. Will fall back to the default values set by {@link Config}. 26 | *

27 | * Timeout can also be set through adb shell (requires root access): 28 | *

29 | * 'adb shell setprop solo_large_timeout milliseconds' 30 | * 31 | * @param milliseconds the default timeout length of the waitFor methods 32 | * 33 | */ 34 | public static void setLargeTimeout(int milliseconds){ 35 | largeTimeout = milliseconds; 36 | } 37 | 38 | /** 39 | * 设置短超时时间,未设置则使用Config设置的默认超时10s 40 | * Sets the default timeout length of the get, is, set, assert, enter, type and click methods. Will fall back to the default values set by {@link Config}. 41 | *

42 | * Timeout can also be set through adb shell (requires root access): 43 | *

44 | * 'adb shell setprop solo_small_timeout milliseconds' 45 | * 46 | * @param milliseconds the default timeout length of the get, is, set, assert, enter and click methods 47 | * 48 | */ 49 | public static void setSmallTimeout(int milliseconds){ 50 | smallTimeout = milliseconds; 51 | } 52 | 53 | /** 54 | * 获取当前设置的长超时时间 55 | * Gets the default timeout length of the waitFor methods. 56 | * 57 | * @return the timeout length in milliseconds 58 | * 59 | */ 60 | public static int getLargeTimeout(){ 61 | return largeTimeout; 62 | } 63 | 64 | /** 65 | * 获取当前设置的短超时时间 66 | * Gets the default timeout length of the get, is, set, assert, enter, type and click methods. 67 | * 68 | * @return the timeout length in milliseconds 69 | * 70 | */ 71 | public static int getSmallTimeout(){ 72 | return smallTimeout; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /com/robotium/solo/ViewFetcher.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.lang.reflect.Field; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.webkit.WebView; 9 | 10 | /** 11 | * View获取操作工具类,提供大量操作获取view的方法 12 | * Contains view methods. Examples are getViews(), 13 | * getCurrentTextViews(), getCurrentImageViews(). 14 | * 15 | * @author Renas Reda, renas.reda@robotium.com 16 | * 17 | */ 18 | 19 | class ViewFetcher { 20 | // activity工具类 21 | private final ActivityUtils activityUtils; 22 | // 存储windowManager管理类的名字 23 | private String windowManagerString; 24 | 25 | /** 26 | * 构造函数,初始化ViewFetcher对象 27 | * Constructs this object. 28 | * 29 | * @param activityUtils the {@code ActivityUtils} instance 30 | * 31 | */ 32 | 33 | public ViewFetcher(ActivityUtils activityUtils) { 34 | this.activityUtils = activityUtils; 35 | // 检查系统版本,初始化WindowManager对象的属性名 36 | setWindowManagerString(); 37 | } 38 | 39 | 40 | /** 41 | * 获取view类的mParent属性 42 | * Returns the absolute top parent {@code View} in for a given {@code View}. 43 | * 44 | * @param view the {@code View} whose top parent is requested 45 | * @return the top parent {@code View} 46 | */ 47 | 48 | public View getTopParent(View view) { 49 | // 如果获取的mParent非空,而且mParent是View的实例,那么继续迭代直到为空或者非View的实例 50 | if (view.getParent() != null 51 | && view.getParent() instanceof android.view.View) { 52 | return getTopParent((View) view.getParent()); 53 | } else { 54 | return view; 55 | } 56 | } 57 | 58 | 59 | /** 60 | * 返回列表或者滚动条的mParent属性,即View的宿主容器 61 | * 一般为AbsListView ScrollView WebView 62 | * 主要为了确定控件类型 63 | * Returns the scroll or list parent view 64 | * 65 | * @param view the view who's parent should be returned 66 | * @return the parent scroll view, list view or null 67 | */ 68 | 69 | public View getScrollOrListParent(View view) { 70 | // view不是继承自 AbsListView ScrollView WebView 则继续迭代 71 | if (!(view instanceof android.widget.AbsListView) && !(view instanceof android.widget.ScrollView) && !(view instanceof WebView)) { 72 | try{ 73 | return getScrollOrListParent((View) view.getParent()); 74 | }catch(Exception e){ 75 | return null; 76 | } 77 | } else { 78 | return view; 79 | } 80 | } 81 | 82 | /** 83 | * 获取当前界面上的所有非装饰类View对象 84 | * onlySufficientlyVisible 为true则过滤所有的不可见对象,为false则不可见对象也返回 85 | * Returns views from the shown DecorViews. 86 | * 87 | * @param onlySufficientlyVisible if only sufficiently visible views should be returned 88 | * @return all the views contained in the DecorViews 89 | */ 90 | 91 | public ArrayList getAllViews(boolean onlySufficientlyVisible) { 92 | // 获取当前界面对应的mViews属性 93 | final View[] views = getWindowDecorViews(); 94 | // 构造 View数组,一般用List 95 | final ArrayList allViews = new ArrayList(); 96 | // views数组中过滤掉DecorView 97 | final View[] nonDecorViews = getNonDecorViews(views); 98 | View view = null; 99 | // 获取所有非DecorView包含的View对象 100 | if(nonDecorViews != null){ 101 | for(int i = 0; i < nonDecorViews.length; i++){ 102 | view = nonDecorViews[i]; 103 | try { 104 | // 遍历获取所有的 View 105 | addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible); 106 | } catch (Exception ignored) {} 107 | if(view != null) allViews.add(view); 108 | } 109 | } 110 | // 获取所有的DecorView包含的View 111 | if (views != null && views.length > 0) { 112 | // 获取最近选中的View 113 | view = getRecentDecorView(views); 114 | try { 115 | // 遍历获取所有的View 116 | addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible); 117 | } catch (Exception ignored) {} 118 | 119 | if(view != null) allViews.add(view); 120 | } 121 | 122 | return allViews; 123 | } 124 | 125 | /** 126 | * 过滤出views中的 DecorView对象 127 | * Returns the most recent DecorView 128 | * 129 | * @param views the views to check 130 | * @return the most recent DecorView 131 | */ 132 | 133 | public final View getRecentDecorView(View[] views) { 134 | if(views == null) 135 | return null; 136 | 137 | final View[] decorViews = new View[views.length]; 138 | int i = 0; 139 | View view; 140 | 141 | for (int j = 0; j < views.length; j++) { 142 | view = views[j]; 143 | // 获取 DecorView对象 144 | if (view != null && view.getClass().getName() 145 | .equals("com.android.internal.policy.impl.PhoneWindow$DecorView")) { 146 | decorViews[i] = view; 147 | i++; 148 | } 149 | } 150 | return getRecentContainer(decorViews); 151 | } 152 | 153 | /** 154 | * 获取当前的焦点View 155 | * Returns the most recent view container 156 | * 157 | * @param views the views to check 158 | * @return the most recent view container 159 | */ 160 | 161 | private final View getRecentContainer(View[] views) { 162 | View container = null; 163 | long drawingTime = 0; 164 | View view; 165 | 166 | for(int i = 0; i < views.length; i++){ 167 | view = views[i]; 168 | // 按照控件是否选中和绘制时间判断是否最新的 169 | if (view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime) { 170 | // 更改临时变量值 171 | container = view; 172 | // 更改临时变量值 173 | drawingTime = view.getDrawingTime(); 174 | } 175 | } 176 | return container; 177 | } 178 | 179 | /** 180 | * 过滤所有的非装饰类View 181 | * Returns all views that are non DecorViews 182 | * 183 | * @param views the views to check 184 | * @return the non DecorViews 185 | */ 186 | 187 | private final View[] getNonDecorViews(View[] views) { 188 | View[] decorViews = null; 189 | 190 | if(views != null) { 191 | decorViews = new View[views.length]; 192 | 193 | int i = 0; 194 | View view; 195 | 196 | for (int j = 0; j < views.length; j++) { 197 | view = views[j]; 198 | // 类名不是DecorView的则加入返回数组 199 | if (view != null && !(view.getClass().getName() 200 | .equals("com.android.internal.policy.impl.PhoneWindow$DecorView"))) { 201 | decorViews[i] = view; 202 | i++; 203 | } 204 | } 205 | } 206 | return decorViews; 207 | } 208 | 209 | 210 | 211 | /** 212 | * 获取给定View中的所有包含的View包含parent 213 | * parent 为空则默认返回当前界面所有的View 214 | * onlySufficientlyVisible 为true则返回所有可被Clicker点击的View,为false则不进行过滤全部返回 215 | * Extracts all {@code View}s located in the currently active {@code Activity}, recursively. 216 | * 217 | * @param parent the {@code View} whose children should be returned, or {@code null} for all 218 | * @param onlySufficientlyVisible if only sufficiently visible views should be returned 219 | * @return all {@code View}s located in the currently active {@code Activity}, never {@code null} 220 | */ 221 | 222 | public ArrayList getViews(View parent, boolean onlySufficientlyVisible) { 223 | final ArrayList views = new ArrayList(); 224 | final View parentToUse; 225 | // 传入的view为空,则按照当前界面操作 226 | if (parent == null){ 227 | return getAllViews(onlySufficientlyVisible); 228 | }else{ 229 | parentToUse = parent; 230 | // 先把自己添加了 231 | views.add(parentToUse); 232 | // 如果传入是ViewGroup类型的,那么遍历所有的View 233 | if (parentToUse instanceof ViewGroup) { 234 | addChildren(views, (ViewGroup) parentToUse, onlySufficientlyVisible); 235 | } 236 | } 237 | return views; 238 | } 239 | 240 | /** 241 | * 遍历ViewGroup中的所有View 242 | * onlySufficientlyVisible 为true则返回所有的使用 Clicker可以点击的view,为false则返回所有遍历到的View 243 | * Adds all children of {@code viewGroup} (recursively) into {@code views}. 244 | * 245 | * @param views an {@code ArrayList} of {@code View}s 246 | * @param viewGroup the {@code ViewGroup} to extract children from 247 | * @param onlySufficientlyVisible if only sufficiently visible views should be returned 248 | */ 249 | 250 | private void addChildren(ArrayList views, ViewGroup viewGroup, boolean onlySufficientlyVisible) { 251 | if(viewGroup != null){ 252 | // 遍历ViewGroup 253 | for (int i = 0; i < viewGroup.getChildCount(); i++) { 254 | final View child = viewGroup.getChildAt(i); 255 | // 添加所有Clicker可点击对象 256 | if(onlySufficientlyVisible && isViewSufficientlyShown(child)) 257 | views.add(child); 258 | // 不关注view是否可以通过Clicker点击,全部获取 259 | else if(!onlySufficientlyVisible) 260 | views.add(child); 261 | // 如果包含ViewGroup进行迭代遍历 262 | if (child instanceof ViewGroup) { 263 | addChildren(views, (ViewGroup) child, onlySufficientlyVisible); 264 | } 265 | } 266 | } 267 | } 268 | 269 | /** 270 | * 如果View是可见的,那么返回true,否则返回false 271 | * 滑动容器或者列表容易以可见面积大于等于控件面积的1/2做判断 272 | * 因 Click方法是点击View的正中心位置,因此该位置不可见会导致无法点击 273 | * Returns true if the view is sufficiently shown 274 | * 275 | * @param view the view to check 276 | * @return true if the view is sufficiently shown 277 | */ 278 | 279 | public final boolean isViewSufficientlyShown(View view){ 280 | // 存储View的xy坐标 281 | final int[] xyView = new int[2]; 282 | // 存储View容器的xy坐标 283 | final int[] xyParent = new int[2]; 284 | 285 | if(view == null) 286 | return false; 287 | // 获取View的高度,按照高度判断是否可见 288 | final float viewHeight = view.getHeight(); 289 | // 获取 View的父容器 290 | final View parent = getScrollOrListParent(view); 291 | // 获取 view的XY坐标 292 | view.getLocationOnScreen(xyView); 293 | // 如果无宿主容器,那么坐标是0 294 | if(parent == null){ 295 | xyParent[1] = 0; 296 | } 297 | // 有宿主容器,则获取宿主容器xy坐标 298 | else{ 299 | parent.getLocationOnScreen(xyParent); 300 | } 301 | // 如果view在容器中可见内容小于容易总面积的一般,那么判定为不可见,分为高度的上限和下限判断 302 | if(xyView[1] + (viewHeight/2.0f) > getScrollListWindowHeight(view)) 303 | return false; 304 | 305 | else if(xyView[1] + (viewHeight/2.0f) < xyParent[1]) 306 | return false; 307 | 308 | return true; 309 | } 310 | 311 | /** 312 | * 获取可滑动容器或者列表容器的高度坐标 313 | * Returns the height of the scroll or list view parent 314 | * @param view the view who's parents height should be returned 315 | * @return the height of the scroll or list view parent 316 | */ 317 | 318 | @SuppressWarnings("deprecation") 319 | public float getScrollListWindowHeight(View view) { 320 | final int[] xyParent = new int[2]; 321 | // 获取容器的宿主容器 322 | View parent = getScrollOrListParent(view); 323 | final float windowHeight; 324 | // 如果无宿主容器,那么直接获取当前Activity的高度 325 | if(parent == null){ 326 | windowHeight = activityUtils.getCurrentActivity(false).getWindowManager() 327 | .getDefaultDisplay().getHeight(); 328 | } 329 | // 否则高度为宿主容器+当前容器的高度 330 | else{ 331 | parent.getLocationOnScreen(xyParent); 332 | windowHeight = xyParent[1] + parent.getHeight(); 333 | } 334 | // 释放对象 335 | parent = null; 336 | return windowHeight; 337 | } 338 | 339 | 340 | /** 341 | * 按照给定的过滤类型获取所有改类型的View 342 | * classToFilterBy 过滤类 343 | * Returns an {@code ArrayList} of {@code View}s of the specified {@code Class} located in the current 344 | * {@code Activity}. 345 | * 346 | * @param classToFilterBy return all instances of this class, e.g. {@code Button.class} or {@code GridView.class} 347 | * @return an {@code ArrayList} of {@code View}s of the specified {@code Class} located in the current {@code Activity} 348 | */ 349 | 350 | public ArrayList getCurrentViews(Class classToFilterBy) { 351 | return getCurrentViews(classToFilterBy, null); 352 | } 353 | 354 | /** 355 | * 按照给定类型的class,返回View中对应的View 356 | * Returns an {@code ArrayList} of {@code View}s of the specified {@code Class} located under the specified {@code parent}. 357 | * 358 | * @param classToFilterBy return all instances of this class, e.g. {@code Button.class} or {@code GridView.class} 359 | * @param parent the parent {@code View} for where to start the traversal 360 | * @return an {@code ArrayList} of {@code View}s of the specified {@code Class} located under the specified {@code parent} 361 | */ 362 | 363 | public ArrayList getCurrentViews(Class classToFilterBy, View parent) { 364 | ArrayList filteredViews = new ArrayList(); 365 | List allViews = getViews(parent, true); 366 | for(View view : allViews){ 367 | // 按照class类型做过滤,并做类型转换 368 | if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) { 369 | filteredViews.add(classToFilterBy.cast(view)); 370 | } 371 | } 372 | // 释放对象 373 | allViews = null; 374 | return filteredViews; 375 | } 376 | 377 | 378 | /** 379 | * 返回给定views中的最新可见View 380 | * Tries to guess which view is the most likely to be interesting. Returns 381 | * the most recently drawn view, which presumably will be the one that the 382 | * user was most recently interacting with. 383 | * 384 | * @param views A list of potentially interesting views, likely a collection 385 | * of views from a set of types, such as [{@link Button}, 386 | * {@link TextView}] or [{@link ScrollView}, {@link ListView}] 387 | * @param index the index of the view 388 | * @return most recently drawn view, or null if no views were passed 389 | */ 390 | 391 | public final T getFreshestView(ArrayList views){ 392 | // 临时变量存储xy坐标 393 | final int[] locationOnScreen = new int[2]; 394 | T viewToReturn = null; 395 | long drawingTime = 0; 396 | if(views == null){ 397 | return null; 398 | } 399 | for(T view : views){ 400 | // 获取xy坐标 401 | view.getLocationOnScreen(locationOnScreen); 402 | 403 | if (locationOnScreen[0] < 0 ) 404 | continue; 405 | // 遍历找出最新的 406 | if(view.getDrawingTime() > drawingTime && view.getHeight() > 0){ 407 | drawingTime = view.getDrawingTime(); 408 | viewToReturn = view; 409 | } 410 | } 411 | views = null; 412 | return viewToReturn; 413 | } 414 | // WindowManager对象,提供大量的app界面view对象获取方法 415 | private static Class windowManager; 416 | static{ 417 | try { 418 | String windowManagerClassName; 419 | // 按照Android版本,判断对应的类名 420 | if (android.os.Build.VERSION.SDK_INT >= 17) { 421 | windowManagerClassName = "android.view.WindowManagerGlobal"; 422 | } else { 423 | windowManagerClassName = "android.view.WindowManagerImpl"; 424 | } 425 | // 通过反射获取类对象 426 | windowManager = Class.forName(windowManagerClassName); 427 | 428 | } catch (ClassNotFoundException e) { 429 | throw new RuntimeException(e); 430 | } catch (SecurityException e) { 431 | e.printStackTrace(); 432 | } 433 | } 434 | 435 | /** 436 | * 获取当前界面的所有装饰器类 437 | * Returns the WindorDecorViews shown on the screen. 438 | * 439 | * @return the WindorDecorViews shown on the screen 440 | */ 441 | 442 | @SuppressWarnings("unchecked") 443 | public View[] getWindowDecorViews() 444 | { 445 | 446 | Field viewsField; 447 | Field instanceField; 448 | try { 449 | // 反射获取mViews属性 450 | viewsField = windowManager.getDeclaredField("mViews"); 451 | // 反射获取WindowMager属性 452 | instanceField = windowManager.getDeclaredField(windowManagerString); 453 | // 修改属性声明,改成可访问 454 | viewsField.setAccessible(true); 455 | // 修改属性声明,改成可访问 456 | instanceField.setAccessible(true); 457 | // 反射获取windowManager对象 458 | Object instance = instanceField.get(null); 459 | View[] result; 460 | if (android.os.Build.VERSION.SDK_INT >= 19) { 461 | // 获取mViews属性内容,即View[] 462 | result = ((ArrayList) viewsField.get(instance)).toArray(new View[0]); 463 | } else { 464 | // 获取mViews属性内容,即View[] 465 | result = (View[]) viewsField.get(instance); 466 | } 467 | return result; 468 | } catch (Exception e) { 469 | e.printStackTrace(); 470 | } 471 | return null; 472 | } 473 | 474 | /** 475 | * 判断当前Android版本对应的WindowManager对象字段名 476 | * Sets the window manager string. 477 | */ 478 | private void setWindowManagerString(){ 479 | 480 | if (android.os.Build.VERSION.SDK_INT >= 17) { 481 | windowManagerString = "sDefaultWindowManager"; 482 | 483 | } else if(android.os.Build.VERSION.SDK_INT >= 13) { 484 | windowManagerString = "sWindowManager"; 485 | 486 | } else { 487 | windowManagerString = "mWindowManager"; 488 | } 489 | } 490 | 491 | 492 | } -------------------------------------------------------------------------------- /com/robotium/solo/ViewLocationComparator.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.view.View; 4 | import java.util.Comparator; 5 | 6 | /** 7 | * View控件比较工具,默认使用y坐标值做比较,即view按照在屏幕中的上面到下面排序 8 | * Orders {@link View}s by their location on-screen. 9 | * 10 | */ 11 | 12 | class ViewLocationComparator implements Comparator { 13 | // 存放第一个view的位置坐标 14 | private final int a[] = new int[2]; 15 | // 存放第二个view的位置坐标 16 | private final int b[] = new int[2]; 17 | private final int axis1, axis2; 18 | // 默认构造函数,高度优先排序 19 | public ViewLocationComparator() { 20 | this(true); 21 | } 22 | 23 | /** 24 | * 设置排序规则 25 | * yAxisFirst true按照高度优先比较,false 按照 x坐标,从左往右排序 26 | * @param yAxisFirst Whether the y-axis should be compared before the x-axis. 27 | */ 28 | 29 | public ViewLocationComparator(boolean yAxisFirst) { 30 | this.axis1 = yAxisFirst ? 1 : 0; 31 | this.axis2 = yAxisFirst ? 0 : 1; 32 | } 33 | // 按照构造函数设定的规则,比较2个view的位置 34 | public int compare(View lhs, View rhs) { 35 | // 获取第一个view的位置坐标信息 36 | lhs.getLocationOnScreen(a); 37 | // 获取第二个view的位置坐标信息 38 | rhs.getLocationOnScreen(b); 39 | // 首先坐标不相等,比较首先坐标大小 40 | if (a[axis1] != b[axis1]) { 41 | return a[axis1] < b[axis1] ? -1 : 1; 42 | } 43 | // 比较第二个坐标大小 44 | if (a[axis2] < b[axis2]) { 45 | return -1; 46 | } 47 | // 2个坐标相等,则返回0,第1个比第二个大则返回1 48 | return a[axis2] == b[axis2] ? 0 : 1; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /com/robotium/solo/WebElement.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.Hashtable; 4 | 5 | /** 6 | * 定义WebView中各类元素,类似input之类的 7 | * Represents an element shown in a WebView. 8 | * 9 | * @author Renas Reda, renas.reda@robotium.com 10 | * 11 | */ 12 | 13 | public class WebElement { 14 | // 对应屏幕中的该控件中间位置x坐标 15 | private int locationX = 0; 16 | // 对应屏幕中的改控件中间位置y坐标 17 | private int locationY = 0; 18 | // Web元素 id 19 | private String id; 20 | // Web元素text 21 | private String text; 22 | // Web元素name 23 | private String name; 24 | // Web元素class 25 | private String className; 26 | // Web元素tag 27 | private String tagName; 28 | // 其他额外属性 29 | private Hashtable attributes; 30 | 31 | 32 | /** 33 | * 构造函数 34 | * Constructs this object. 35 | * 36 | * @param webId the given web id 37 | * @param textContent the given text to be set 38 | * @param name the given name to be set 39 | * @param className the given class name to set 40 | * @param tagName the given tag name to be set 41 | * @param attributes the attributes to set 42 | */ 43 | 44 | public WebElement(String webId, String textContent, String name, String className, String tagName, Hashtable attributes) { 45 | 46 | this.setId(webId); 47 | this.setTextContent(textContent); 48 | this.setName(name); 49 | this.setClassName(className); 50 | this.setTagName(tagName); 51 | this.setAttributes(attributes); 52 | } 53 | 54 | /** 55 | * 获取 WebElement元素对应的屏幕坐标 56 | * Returns the WebElements location on screen. 57 | */ 58 | 59 | public void getLocationOnScreen(int[] location) { 60 | 61 | location[0] = locationX; 62 | location[1] = locationY; 63 | } 64 | 65 | /** 66 | * 设置屏幕相对x坐标 67 | * Sets the X location. 68 | * 69 | * @param locationX the X location of the {@code WebElement} 70 | */ 71 | 72 | public void setLocationX(int locationX){ 73 | this.locationX = locationX; 74 | } 75 | 76 | /** 77 | * 设置屏幕相对Y坐标 78 | * Sets the Y location. 79 | * 80 | * @param locationY the Y location of the {@code WebElement} 81 | */ 82 | 83 | public void setLocationY(int locationY){ 84 | this.locationY = locationY; 85 | } 86 | 87 | /** 88 | * 获取屏幕相对X坐标 89 | * Returns the X location. 90 | * 91 | * @return the X location 92 | */ 93 | 94 | public int getLocationX(){ 95 | return this.locationX; 96 | } 97 | 98 | /** 99 | * 获取屏幕相对Y坐标 100 | * Returns the Y location. 101 | * 102 | * @return the Y location 103 | */ 104 | 105 | public int getLocationY(){ 106 | return this.locationY; 107 | } 108 | 109 | /** 110 | * 获取id 111 | * Returns the id. 112 | * 113 | * @return the id 114 | */ 115 | 116 | public String getId() { 117 | return id; 118 | } 119 | 120 | /** 121 | * 设置id 122 | * Sets the id. 123 | * 124 | * @param id the id to set 125 | */ 126 | 127 | public void setId(String id) { 128 | this.id = id; 129 | } 130 | 131 | /** 132 | * 获取name 133 | * Returns the name. 134 | * 135 | * @return the name 136 | */ 137 | 138 | public String getName() { 139 | return name; 140 | } 141 | 142 | /** 143 | * 设置name 144 | * Sets the name. 145 | * 146 | * @param name the name to set 147 | */ 148 | 149 | public void setName(String name) { 150 | this.name = name; 151 | } 152 | 153 | /** 154 | * 获取class 155 | * Returns the class name. 156 | * 157 | * @return the class name 158 | */ 159 | 160 | public String getClassName() { 161 | return className; 162 | } 163 | 164 | /** 165 | * 设置class 166 | * Sets the class name. 167 | * 168 | * @param className the class name to set 169 | */ 170 | 171 | public void setClassName(String className) { 172 | this.className = className; 173 | } 174 | 175 | /** 176 | * 获取tag 177 | * Returns the tag name. 178 | * 179 | * @return the tag name 180 | */ 181 | 182 | public String getTagName() { 183 | return tagName; 184 | } 185 | 186 | /** 187 | * 设置tag 188 | * Sets the tag name. 189 | * 190 | * @param tagName the tag name to set 191 | */ 192 | 193 | public void setTagName(String tagName) { 194 | this.tagName = tagName; 195 | } 196 | 197 | /** 198 | * 获取text 199 | * Returns the text content. 200 | * 201 | * @return the text content 202 | */ 203 | 204 | public String getText() { 205 | return text; 206 | } 207 | 208 | /** 209 | * 设置text 210 | * Sets the text content. 211 | * 212 | * @param textContent the text content to set 213 | */ 214 | 215 | public void setTextContent(String textContent) { 216 | this.text = textContent; 217 | } 218 | 219 | /** 220 | * 获取凄然额外属性 221 | * Returns the value for the specified attribute. 222 | * 223 | * @return the value for the specified attribute 224 | */ 225 | 226 | public String getAttribute(String attributeName) { 227 | if (attributeName != null){ 228 | return this.attributes.get(attributeName); 229 | } 230 | 231 | return null; 232 | } 233 | 234 | /** 235 | * 设置额外属性 236 | * Sets the attributes. 237 | * 238 | * @param attributes the attributes to set 239 | */ 240 | 241 | public void setAttributes(Hashtable attributes) { 242 | this.attributes = attributes; 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /com/robotium/solo/WebElementCreator.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Hashtable; 5 | import java.util.List; 6 | import java.util.concurrent.CopyOnWriteArrayList; 7 | import android.os.SystemClock; 8 | import android.webkit.WebView; 9 | 10 | /** 11 | * 将WebElement信息解析成WebElement对象 12 | * Contains TextView related methods. Examples are: 13 | * getTextViewsFromWebViews(), createTextViewAndAddInList(). 14 | * 15 | * @author Renas Reda, renas.reda@robotium.com 16 | * 17 | */ 18 | 19 | class WebElementCreator { 20 | // 存储WebElement 21 | private List webElements; 22 | // 延时工具类 23 | private Sleeper sleeper; 24 | // 标识符号,用于标识WebView内容解析是否已经完成 25 | private boolean isFinished = false; 26 | 27 | /** 28 | * 构造函数 29 | * Constructs this object. 30 | * 31 | * @param sleeper the {@code Sleeper} instance 32 | * 33 | */ 34 | 35 | public WebElementCreator(Sleeper sleeper){ 36 | this.sleeper = sleeper; 37 | // 创建一个存储实例,使用copyOnweite可以保证复制list时重新构造一份新的,对原有不造成影响 38 | webElements = new CopyOnWriteArrayList(); 39 | } 40 | 41 | /** 42 | * 初始化 43 | * Prepares for start of creating {@code TextView} objects based on web elements 44 | */ 45 | 46 | public void prepareForStart(){ 47 | // 重置为false 48 | setFinished(false); 49 | // 清空已存储的WebElement 50 | webElements.clear(); 51 | } 52 | 53 | /** 54 | * 获取当前 WebView中的WebElement 55 | * Returns an {@code ArrayList} of {@code TextView} objects based on the web elements shown 56 | * 57 | * @return an {@code ArrayList} of {@code TextView} objects based on the web elements shown 58 | */ 59 | 60 | public ArrayList getWebElementsFromWebViews(){ 61 | // 等待WebView元素被解析 62 | waitForWebElementsToBeCreated(); 63 | // copy一份对象返回 64 | return new ArrayList(webElements); 65 | } 66 | 67 | /** 68 | * 获取WebView内容解析状态 69 | * true 解析已完成 70 | * false 解析还未完成 71 | * Returns true if all {@code TextView} objects based on web elements have been created 72 | * 73 | * @return true if all {@code TextView} objects based on web elements have been created 74 | */ 75 | 76 | public boolean isFinished(){ 77 | return isFinished; 78 | } 79 | 80 | 81 | /** 82 | * 设置WebView解析是否完成状态 83 | * true 已完成解析 84 | * false 解析未完成 85 | * Set to true if all {@code TextView} objects have been created 86 | * 87 | * @param isFinished true if all {@code TextView} objects have been created 88 | */ 89 | 90 | public void setFinished(boolean isFinished){ 91 | this.isFinished = isFinished; 92 | } 93 | 94 | /** 95 | * 按照指定信息,获取WebView中的WebElement并加入到webElements中 96 | * Creates a {@ WebElement} object from the given text and {@code WebView} 97 | * 98 | * @param webData the data of the web element 99 | * @param webView the {@code WebView} the text is shown in 100 | */ 101 | 102 | public void createWebElementAndAddInList(String webData, WebView webView){ 103 | // 获取WebElement 104 | WebElement webElement = createWebElementAndSetLocation(webData, webView); 105 | // 非空则加入WebElement列表 106 | if((webElement!=null)) 107 | webElements.add(webElement); 108 | } 109 | 110 | /** 111 | * 设置WebElement坐标属性 112 | * webElement 需要设置的WebElement 113 | * webView WebElement所在的WebView 114 | * 115 | * Sets the location of a {@code WebElement} 116 | * 117 | * @param webElement the {@code TextView} object to set location 118 | * @param webView the {@code WebView} the text is shown in 119 | * @param x the x location to set 120 | * @param y the y location to set 121 | * @param width the width to set 122 | * @param height the height to set 123 | */ 124 | 125 | private void setLocation(WebElement webElement, WebView webView, int x, int y, int width, int height ){ 126 | // 获取页面缩放信息 127 | float scale = webView.getScale(); 128 | // 储存屏幕坐标 129 | int[] locationOfWebViewXY = new int[2]; 130 | // 获取WebView对应手机屏幕中的坐标 131 | webView.getLocationOnScreen(locationOfWebViewXY); 132 | // 计算可以点击的x坐标,取WebElement中间位置 133 | int locationX = (int) (locationOfWebViewXY[0] + (x + (Math.floor(width / 2))) * scale); 134 | // 计算可操作的 y坐标,取WebElement中间位置 135 | int locationY = (int) (locationOfWebViewXY[1] + (y + (Math.floor(height / 2))) * scale); 136 | 137 | webElement.setLocationX(locationX); 138 | webElement.setLocationY(locationY); 139 | } 140 | 141 | /** 142 | * 按照给定信息获取WebView中对应的元素 143 | * Creates a {@code WebView} object 144 | * 145 | * @param information the data of the web element 146 | * @param webView the web view the text is shown in 147 | * 148 | * @return a {@code WebElement} object with a given text and location 149 | */ 150 | 151 | private WebElement createWebElementAndSetLocation(String information, WebView webView){ 152 | // 解析属性,按照;,划分 153 | String[] data = information.split(";,"); 154 | String[] elements = null; 155 | int x = 0; 156 | int y = 0; 157 | int width = 0; 158 | int height = 0; 159 | Hashtable attributes = new Hashtable(); 160 | try{ 161 | // 解析对应的x坐标 162 | x = Math.round(Float.valueOf(data[5])); 163 | // 解析对应的y坐标 164 | y = Math.round(Float.valueOf(data[6])); 165 | // 解析宽度信息 166 | width = Math.round(Float.valueOf(data[7])); 167 | // 解析高度信息 168 | height = Math.round(Float.valueOf(data[8])); 169 | // 解析剩余属性 170 | elements = data[9].split("\\#\\$"); 171 | }catch(Exception ignored){} 172 | // 属性为key value格式,使用::分隔 173 | if(elements != null) { 174 | for (int index = 0; index < elements.length; index++){ 175 | String[] element = elements[index].split("::"); 176 | // 对于只有key的属性,key也作为value使用 177 | if (element.length > 1) { 178 | attributes.put(element[0], element[1]); 179 | } else { 180 | attributes.put(element[0], element[0]); 181 | } 182 | } 183 | } 184 | 185 | WebElement webElement = null; 186 | 187 | try{ 188 | // 构造WebElement对象 189 | webElement = new WebElement(data[0], data[1], data[2], data[3], data[4], attributes); 190 | // 设置位置信息 191 | setLocation(webElement, webView, x, y, width, height); 192 | }catch(Exception ignored) {} 193 | 194 | return webElement; 195 | } 196 | 197 | /** 198 | * 检查WebView内容解析是否完成,默认超时5s, 199 | * 解析完成返回true,未完成返回false 200 | * Waits for {@code WebElement} objects to be created 201 | * 202 | * @return true if successfully created before timout 203 | */ 204 | 205 | private boolean waitForWebElementsToBeCreated(){ 206 | // 5s延时 207 | final long endTime = SystemClock.uptimeMillis() + 5000; 208 | // 检查是否超时 209 | while(SystemClock.uptimeMillis() < endTime){ 210 | // 已解析完成,返回true 211 | if(isFinished){ 212 | return true; 213 | } 214 | // 等待300ms 215 | sleeper.sleepMini(); 216 | } 217 | return false; 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /com/robotium/solo/WebUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.util.ArrayList; 8 | import com.robotium.solo.Solo.Config; 9 | import android.app.Instrumentation; 10 | import android.webkit.WebChromeClient; 11 | import android.webkit.WebView; 12 | import android.widget.TextView; 13 | 14 | 15 | /** 16 | * Contains web related methods. Examples are: 17 | * enterTextIntoWebElement(), getWebTexts(), getWebElements(). 18 | * 19 | * @author Renas Reda, renas.reda@robotium.com 20 | * 21 | */ 22 | 23 | class WebUtils { 24 | // View操作工具类 25 | private ViewFetcher viewFetcher; 26 | // Instrument,用于各种事件发送 27 | private Instrumentation inst; 28 | // Activity操作工具类 29 | private ActivityUtils activityUtils; 30 | // Robotium定制的WebClient 31 | RobotiumWebClient robotiumWebCLient; 32 | // WebElement构造工具方法 33 | WebElementCreator webElementCreator; 34 | // 原生WebChromeClient 保留,不需要Robotium修改的使用原生的执行 35 | WebChromeClient originalWebChromeClient = null; 36 | // 配置文件 37 | private Config config; 38 | 39 | 40 | /** 41 | * 构造函数 42 | * Constructs this object. 43 | * 44 | * @param config the {@code Config} instance 45 | * @param instrumentation the {@code Instrumentation} instance 46 | * @param activityUtils the {@code ActivityUtils} instance 47 | * @param viewFetcher the {@code ViewFetcher} instance 48 | */ 49 | 50 | public WebUtils(Config config, Instrumentation instrumentation, ActivityUtils activityUtils, ViewFetcher viewFetcher, Sleeper sleeper){ 51 | this.config = config; 52 | this.inst = instrumentation; 53 | this.activityUtils = activityUtils; 54 | this.viewFetcher = viewFetcher; 55 | webElementCreator = new WebElementCreator(sleeper); 56 | robotiumWebCLient = new RobotiumWebClient(instrumentation, webElementCreator); 57 | } 58 | 59 | /** 60 | * 调用RoBotiumWeb.js获取所有的Text的WebElement,使用NodeFilter.SHOW_TEXT过滤 61 | * Returns {@code TextView} objects based on web elements shown in the present WebViews 62 | * 63 | * @param onlyFromVisibleWebViews true if only from visible WebViews 64 | * @return an {@code ArrayList} of {@code TextViews}s created from the present {@code WebView}s 65 | */ 66 | 67 | public ArrayList getTextViewsFromWebView(){ 68 | // true标识执行完成,false标识未执行成功 69 | boolean javaScriptWasExecuted = executeJavaScriptFunction("allTexts();"); 70 | // WebElement转换成TextView 71 | return createAndReturnTextViewsFromWebElements(javaScriptWasExecuted); 72 | } 73 | 74 | /** 75 | * WebElement转换成 TextView,javaScriptWasExecuted 为true则执行转换,false不执行转换 76 | * 77 | * Creates and returns TextView objects based on WebElements 78 | * 79 | * @return an ArrayList with TextViews 80 | */ 81 | 82 | private ArrayList createAndReturnTextViewsFromWebElements(boolean javaScriptWasExecuted){ 83 | ArrayList webElementsAsTextViews = new ArrayList(); 84 | // js脚本执行成功,则遍历所有获取到的WebElement信息,并转换成TextView对象 85 | if(javaScriptWasExecuted){ 86 | // 编译所有的WebElement 87 | for(WebElement webElement : webElementCreator.getWebElementsFromWebViews()){ 88 | // 可见控件转换成TextView对象 89 | if(isWebElementSufficientlyShown(webElement)){ 90 | // 转换成TextView对象 91 | RobotiumTextView textView = new RobotiumTextView(inst.getContext(), webElement.getText(), webElement.getLocationX(), webElement.getLocationY()); 92 | // 添加到返回列表 93 | webElementsAsTextViews.add(textView); 94 | } 95 | } 96 | } 97 | return webElementsAsTextViews; 98 | } 99 | 100 | /** 101 | * 获取当前WebView中的所有WebElements 102 | * Returns an ArrayList of WebElements currently shown in the active WebView. 103 | * 104 | * @return an {@code ArrayList} of the {@link WebElement} objects currently shown in the active WebView 105 | */ 106 | 107 | public ArrayList getCurrentWebElements(){ 108 | // 执行获取所所有 WebElement的JavaScript脚本 109 | boolean javaScriptWasExecuted = executeJavaScriptFunction("allWebElements();"); 110 | // 过滤掉非可见WebElement,返回所有剩余的 111 | return getSufficientlyShownWebElements(javaScriptWasExecuted); 112 | } 113 | 114 | /** 115 | * 获取By参数指定属性的所有WebElement 116 | * Returns an ArrayList of WebElements of the specified By object currently shown in the active WebView. 117 | * 118 | * @param by the By object. Examples are By.id("id") and By.name("name") 119 | * @return an {@code ArrayList} of the {@link WebElement} objects currently shown in the active WebView 120 | */ 121 | 122 | public ArrayList getCurrentWebElements(final By by){ 123 | // 获取By属性对应的所有WebElement 124 | boolean javaScriptWasExecuted = executeJavaScript(by, false); 125 | // 该判断目前还没使用,2条路径相同业务逻辑 126 | if(config.useJavaScriptToClickWebElements){ 127 | if(!javaScriptWasExecuted){ 128 | return new ArrayList(); 129 | } 130 | return webElementCreator.getWebElementsFromWebViews(); 131 | } 132 | // 过滤掉非可见WebElement,返回所有剩余的 133 | return getSufficientlyShownWebElements(javaScriptWasExecuted); 134 | } 135 | 136 | /** 137 | * 过滤掉非可见WebElement,返回所有剩余的 138 | * Returns the sufficiently shown WebElements 139 | * 140 | * @return the sufficiently shown WebElements 141 | */ 142 | 143 | private ArrayList getSufficientlyShownWebElements(boolean javaScriptWasExecuted){ 144 | ArrayList currentWebElements = new ArrayList(); 145 | // 检查 JavaScript是否执行成功 146 | if(javaScriptWasExecuted){ 147 | for(WebElement webElement : webElementCreator.getWebElementsFromWebViews()){ 148 | if(isWebElementSufficientlyShown(webElement)){ 149 | currentWebElements.add(webElement); 150 | } 151 | } 152 | } 153 | return currentWebElements; 154 | } 155 | 156 | /** 157 | * 构造JavaScript执行环境,并返回构造好的JavaScript 158 | * Prepares for start of JavaScript execution 159 | * 160 | * @return the JavaScript as a String 161 | */ 162 | 163 | private String prepareForStartOfJavascriptExecution(){ 164 | // 初始化WebElement存储容器 165 | webElementCreator.prepareForStart(); 166 | // 获取当前版本Android对应的WebChromeClient 167 | WebChromeClient currentWebChromeClient = getCurrentWebChromeClient(); 168 | // 保存原有的WebChromeClient 169 | if(currentWebChromeClient != null && !currentWebChromeClient.getClass().isAssignableFrom(RobotiumWebClient.class)){ 170 | originalWebChromeClient = getCurrentWebChromeClient(); 171 | } 172 | // 初始化 Robotium定制版本的WebChromeClient 173 | robotiumWebCLient.enableJavascriptAndSetRobotiumWebClient(viewFetcher.getCurrentViews(WebView.class), originalWebChromeClient); 174 | // 返回读取到的RobotiumWeb.js中的内容 175 | return getJavaScriptAsString(); 176 | } 177 | 178 | /** 179 | * 获取当前的Android版本对应的WebChromeClient 180 | * Returns the current WebChromeClient through reflection 181 | * 182 | * @return the current WebChromeClient 183 | * 184 | */ 185 | 186 | private WebChromeClient getCurrentWebChromeClient(){ 187 | WebChromeClient currentWebChromeClient = null; 188 | // 获取当前最新的WebView 189 | Object currentWebView = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(WebView.class)); 190 | // 高版本才用反射获取 191 | if (android.os.Build.VERSION.SDK_INT >= 16) { 192 | try{ 193 | currentWebView = new Reflect(currentWebView).field("mProvider").out(Object.class); 194 | }catch(IllegalArgumentException ignored) {} 195 | } 196 | 197 | try{ 198 | // 反射获取相关对象 199 | Object mCallbackProxy = new Reflect(currentWebView).field("mCallbackProxy").out(Object.class); 200 | // 获取属性并转化成WebChromeClient对象 201 | currentWebChromeClient = new Reflect(mCallbackProxy).field("mWebChromeClient").out(WebChromeClient.class); 202 | }catch(Exception ignored){} 203 | 204 | return currentWebChromeClient; 205 | } 206 | 207 | /** 208 | * 对指定条件的WebElement输入文本 209 | * Enters text into a web element using the given By method 210 | * 211 | * @param by the By object e.g. By.id("id"); 212 | * @param text the text to enter 213 | */ 214 | 215 | public void enterTextIntoWebElement(final By by, final String text){ 216 | // 按照 Id查找WebElement对象输入 217 | if(by instanceof By.Id){ 218 | executeJavaScriptFunction("enterTextById(\""+by.getValue()+"\", \""+text+"\");"); 219 | } 220 | // 按照 Xpath查找WebElement对象输入 221 | else if(by instanceof By.Xpath){ 222 | executeJavaScriptFunction("enterTextByXpath(\""+by.getValue()+"\", \""+text+"\");"); 223 | } 224 | // 按照 CssSelector查找WebElement对象输入 225 | else if(by instanceof By.CssSelector){ 226 | executeJavaScriptFunction("enterTextByCssSelector(\""+by.getValue()+"\", \""+text+"\");"); 227 | } 228 | // 按照 Name查找WebElement对象输入 229 | else if(by instanceof By.Name){ 230 | executeJavaScriptFunction("enterTextByName(\""+by.getValue()+"\", \""+text+"\");"); 231 | } 232 | // 按照 ClassName查找WebElement对象输入 233 | else if(by instanceof By.ClassName){ 234 | executeJavaScriptFunction("enterTextByClassName(\""+by.getValue()+"\", \""+text+"\");"); 235 | } 236 | // 按照 Text查找WebElement对象输入 237 | else if(by instanceof By.Text){ 238 | executeJavaScriptFunction("enterTextByTextContent(\""+by.getValue()+"\", \""+text+"\");"); 239 | } 240 | // 按照 TagName查找WebElement对象输入 241 | else if(by instanceof By.TagName){ 242 | executeJavaScriptFunction("enterTextByTagName(\""+by.getValue()+"\", \""+text+"\");"); 243 | } 244 | } 245 | 246 | /** 247 | * 运行JavaScript.按照by类型对相应的 WebElement进行操作 248 | * shouldClick 为true标识点击对应的WebElement,否则获取响应的Element信息 249 | * 返回true标识执行成,false执行异常 250 | * Executes JavaScript determined by the given By object 251 | * 252 | * @param by the By object e.g. By.id("id"); 253 | * @param shouldClick true if click should be performed 254 | * @return true if JavaScript function was executed 255 | */ 256 | 257 | public boolean executeJavaScript(final By by, boolean shouldClick){ 258 | // 拼接按照Id执行的JavaScript脚本 259 | if(by instanceof By.Id){ 260 | return executeJavaScriptFunction("id(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 261 | } 262 | // 拼接按照Xpath执行的JavaScript脚本 263 | else if(by instanceof By.Xpath){ 264 | return executeJavaScriptFunction("xpath(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 265 | } 266 | // 拼接按照CssSelector执行的JavaScript脚本 267 | else if(by instanceof By.CssSelector){ 268 | return executeJavaScriptFunction("cssSelector(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 269 | } 270 | // 拼接按照Name执行的JavaScript脚本 271 | else if(by instanceof By.Name){ 272 | return executeJavaScriptFunction("name(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 273 | } 274 | // 拼接按照ClassName执行的JavaScript脚本 275 | else if(by instanceof By.ClassName){ 276 | return executeJavaScriptFunction("className(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 277 | } 278 | // 拼接按照Text执行的JavaScript脚本 279 | else if(by instanceof By.Text){ 280 | return executeJavaScriptFunction("textContent(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 281 | } 282 | // 拼接按照TagName执行的JavaScript脚本 283 | else if(by instanceof By.TagName){ 284 | return executeJavaScriptFunction("tagName(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 285 | } 286 | return false; 287 | } 288 | 289 | /** 290 | * 在WebView中执行指定的Javascript.执行成功返回true,否则返回false 291 | * Executes the given JavaScript function 292 | * 293 | * @param function the function as a String 294 | * @return true if JavaScript function was executed 295 | */ 296 | 297 | private boolean executeJavaScriptFunction(final String function){ 298 | // 获取当前时刻最新的WebView 299 | final WebView webView = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(WebView.class)); 300 | // 非null检查 301 | if(webView == null){ 302 | return false; 303 | } 304 | // 获取JavaScript资源文件,即RoboTiumWeb.js中的内容 305 | final String javaScript = prepareForStartOfJavascriptExecution(); 306 | // WebView中加载相关JavaScript 307 | activityUtils.getCurrentActivity(false).runOnUiThread(new Runnable() { 308 | public void run() { 309 | if(webView != null){ 310 | webView.loadUrl("javascript:" + javaScript + function); 311 | } 312 | } 313 | }); 314 | return true; 315 | } 316 | 317 | /** 318 | * 检查当前WebElement是否可见 319 | * Returns true if the view is sufficiently shown 320 | * 321 | * @param view the view to check 322 | * @return true if the view is sufficiently shown 323 | */ 324 | 325 | public final boolean isWebElementSufficientlyShown(WebElement webElement){ 326 | // 获取当前最新的 WebView 327 | final WebView webView = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(WebView.class)); 328 | // 存储WebView XY坐标信息 329 | final int[] xyWebView = new int[2]; 330 | 331 | if(webView != null && webElement != null){ 332 | // 获取WebView XY坐标信息 333 | webView.getLocationOnScreen(xyWebView); 334 | // WebElement在WebView外,则不可见 335 | if(xyWebView[1] + webView.getHeight() > webElement.getLocationY()) 336 | return true; 337 | } 338 | return false; 339 | } 340 | 341 | /** 342 | * 按照大写字母分割字符串,各字符串之间添加空格 ,并转换成小写 343 | * Splits a name by upper case. 344 | * 345 | * @param name the name to split 346 | * @return a String with the split name 347 | * 348 | */ 349 | 350 | public String splitNameByUpperCase(String name) { 351 | String [] texts = name.split("(?=\\p{Upper})"); 352 | StringBuilder stringToReturn = new StringBuilder(); 353 | 354 | for(String string : texts){ 355 | 356 | if(stringToReturn.length() > 0) { 357 | stringToReturn.append(" " + string.toLowerCase()); 358 | } 359 | else { 360 | stringToReturn.append(string.toLowerCase()); 361 | } 362 | } 363 | return stringToReturn.toString(); 364 | } 365 | 366 | /** 367 | * 加载Robotium.js文件 368 | * 并加载样式信息,添加\n换行符 369 | * Returns the JavaScript file RobotiumWeb.js as a String 370 | * 371 | * @return the JavaScript file RobotiumWeb.js as a {@code String} 372 | */ 373 | 374 | private String getJavaScriptAsString() { 375 | InputStream fis = getClass().getResourceAsStream("RobotiumWeb.js"); 376 | StringBuffer javaScript = new StringBuffer(); 377 | 378 | try { 379 | BufferedReader input = new BufferedReader(new InputStreamReader(fis)); 380 | String line = null; 381 | while (( line = input.readLine()) != null){ 382 | javaScript.append(line); 383 | javaScript.append("\n"); 384 | } 385 | input.close(); 386 | } catch (IOException e) { 387 | throw new RuntimeException(e); 388 | } 389 | return javaScript.toString(); 390 | } 391 | } -------------------------------------------------------------------------------- /com/robotium/solo/Zoomer.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Instrumentation; 4 | import android.os.SystemClock; 5 | import android.view.MotionEvent; 6 | import android.view.MotionEvent.PointerProperties; 7 | import android.view.MotionEvent.PointerCoords; 8 | import android.graphics.PointF; 9 | 10 | // 放大手势操作工具类 11 | class Zoomer { 12 | // Instrument 用于发送事件 13 | private final Instrumentation _instrument; 14 | // 手势持续时间1s 15 | public static final int GESTURE_DURATION_MS = 1000; 16 | // 事件间隔10ms 17 | public static final int EVENT_TIME_INTERVAL_MS = 10; 18 | // 构造函数 19 | public Zoomer(Instrumentation inst) 20 | { 21 | this._instrument = inst; 22 | } 23 | // 发送放大动作 24 | // startPoint1 开始坐标点1 25 | // startPoint2 开始坐标点2 26 | // endPoint1 结束坐标点1 27 | // endPoint2 结束坐标点2 28 | public void generateZoomGesture(PointF startPoint1, PointF startPoint2, PointF endPoint1, PointF endPoint2) 29 | { 30 | // 初始化时间变量 31 | long downTime = SystemClock.uptimeMillis(); 32 | long eventTime = SystemClock.uptimeMillis(); 33 | // 获取相关坐标值 34 | float startX1 = startPoint1.x; 35 | float startY1 = startPoint1.y; 36 | float startX2 = startPoint2.x; 37 | float startY2 = startPoint2.y; 38 | 39 | float endX1 = endPoint1.x; 40 | float endY1 = endPoint1.y; 41 | float endX2 = endPoint2.x; 42 | float endY2 = endPoint2.y; 43 | 44 | //pointer 1 45 | float x1 = startX1; 46 | float y1 = startY1; 47 | 48 | //pointer 2 49 | float x2 = startX2; 50 | float y2 = startY2; 51 | // 构造相关坐标点集合 52 | PointerCoords[] pointerCoords = new PointerCoords[2]; 53 | PointerCoords pc1 = new PointerCoords(); 54 | PointerCoords pc2 = new PointerCoords(); 55 | pc1.x = x1; 56 | pc1.y = y1; 57 | pc1.pressure = 1; 58 | pc1.size = 1; 59 | pc2.x = x2; 60 | pc2.y = y2; 61 | pc2.pressure = 1; 62 | pc2.size = 1; 63 | pointerCoords[0] = pc1; 64 | pointerCoords[1] = pc2; 65 | 66 | PointerProperties[] pointerProperties = new PointerProperties[2]; 67 | PointerProperties pp1 = new PointerProperties(); 68 | PointerProperties pp2 = new PointerProperties(); 69 | pp1.id = 0; 70 | pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 71 | pp2.id = 1; 72 | pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 73 | pointerProperties[0] = pp1; 74 | pointerProperties[1] = pp2; 75 | // 开始发送按下事件 76 | MotionEvent event; 77 | // send the initial touches 78 | event = MotionEvent.obtain( downTime, 79 | eventTime, 80 | MotionEvent.ACTION_DOWN, 81 | 1, 82 | pointerProperties, 83 | pointerCoords, 84 | 0, 0, // metaState, buttonState 85 | 1, // x precision 86 | 1, // y precision 87 | 0, 0, 0, 0 ); // deviceId, edgeFlags, source, flags 88 | _instrument.sendPointerSync(event); 89 | 90 | event = MotionEvent.obtain( downTime, 91 | eventTime, 92 | MotionEvent.ACTION_POINTER_DOWN + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 93 | 2, 94 | pointerProperties, 95 | pointerCoords, 96 | 0, 0, 97 | 1, 98 | 1, 99 | 0, 0, 0, 0 ); 100 | _instrument.sendPointerSync(event); 101 | // 计算动作步骤 100步 102 | int numMoves = GESTURE_DURATION_MS / EVENT_TIME_INTERVAL_MS; 103 | // 计算每步移动的坐标值 104 | float stepX1 = (endX1 - startX1) / numMoves; 105 | float stepY1 = (endY1 - startY1) / numMoves; 106 | float stepX2 = (endX2 - startX2) / numMoves; 107 | float stepY2 = (endY2 - startY2) / numMoves; 108 | // 发送构造好的事件 109 | // send the zoom 110 | for (int i = 0; i < numMoves; i++) 111 | { 112 | eventTime += EVENT_TIME_INTERVAL_MS; 113 | pointerCoords[0].x += stepX1; 114 | pointerCoords[0].y += stepY1; 115 | pointerCoords[1].x += stepX2; 116 | pointerCoords[1].y += stepY2; 117 | 118 | event = MotionEvent.obtain( downTime, 119 | eventTime, 120 | MotionEvent.ACTION_MOVE, 121 | 2, 122 | pointerProperties, 123 | pointerCoords, 124 | 0, 0, 125 | 1, 126 | 1, 127 | 0, 0, 0, 0 ); 128 | _instrument.sendPointerSync(event); 129 | } 130 | } 131 | } 132 | --------------------------------------------------------------------------------