PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/robotium-solo/src/main/java/com/robotium/solo/ScreenshotTaker.java

https://github.com/RobotiumTech/robotium
Java | 483 lines | 299 code | 56 blank | 128 comment | 54 complexity | 8c76da068e9927900e947b6c190926e6 MD5 | raw file
Possible License(s): Apache-2.0
  1. package com.robotium.solo;
  2. import java.io.File;
  3. import java.io.FileOutputStream;
  4. import java.text.SimpleDateFormat;
  5. import java.util.ArrayList;
  6. import java.util.Date;
  7. import java.util.concurrent.CountDownLatch;
  8. import java.util.concurrent.TimeUnit;
  9. import com.robotium.solo.Solo.Config;
  10. import com.robotium.solo.Solo.Config.ScreenshotFileType;
  11. import android.app.Activity;
  12. import android.app.Instrumentation;
  13. import android.graphics.Bitmap;
  14. import android.graphics.Canvas;
  15. import android.graphics.Picture;
  16. import android.opengl.GLSurfaceView;
  17. import android.opengl.GLSurfaceView.Renderer;
  18. import android.os.Handler;
  19. import android.os.HandlerThread;
  20. import android.os.Message;
  21. import android.os.SystemClock;
  22. import android.util.Log;
  23. import android.view.View;
  24. import android.webkit.WebView;
  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. class ScreenshotTaker {
  34. private static final long TIMEOUT_SCREENSHOT_MUTEX = TimeUnit.SECONDS.toMillis(2);
  35. private final Object screenshotMutex = new Object();
  36. private final Config config;
  37. private final Instrumentation instrumentation;
  38. private final ActivityUtils activityUtils;
  39. private final String LOG_TAG = "Robotium";
  40. private ScreenshotSequenceThread screenshotSequenceThread = null;
  41. private HandlerThread screenShotSaverThread = null;
  42. private ScreenShotSaver screenShotSaver = null;
  43. private final ViewFetcher viewFetcher;
  44. private final Sleeper sleeper;
  45. /**
  46. * Constructs this object.
  47. *
  48. * @param config the {@code Config} instance
  49. * @param instrumentation the {@code Instrumentation} instance.
  50. * @param activityUtils the {@code ActivityUtils} instance
  51. * @param viewFetcher the {@code ViewFetcher} instance
  52. * @param sleeper the {@code Sleeper} instance
  53. *
  54. */
  55. ScreenshotTaker(Config config, Instrumentation instrumentation, ActivityUtils activityUtils, ViewFetcher viewFetcher, Sleeper sleeper) {
  56. this.config = config;
  57. this.instrumentation = instrumentation;
  58. this.activityUtils = activityUtils;
  59. this.viewFetcher = viewFetcher;
  60. this.sleeper = sleeper;
  61. }
  62. /**
  63. * Takes a screenshot and saves it in the {@link Config} objects save path.
  64. * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test.
  65. *
  66. * @param name the name to give the screenshot image
  67. * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
  68. */
  69. public void takeScreenshot(final String name, final int quality) {
  70. View decorView = getScreenshotView();
  71. if(decorView == null)
  72. return;
  73. initScreenShotSaver();
  74. ScreenshotRunnable runnable = new ScreenshotRunnable(decorView, name, quality);
  75. synchronized (screenshotMutex) {
  76. Activity activity = activityUtils.getCurrentActivity(false);
  77. if(activity != null)
  78. activity.runOnUiThread(runnable);
  79. else
  80. instrumentation.runOnMainSync(runnable);
  81. try {
  82. screenshotMutex.wait(TIMEOUT_SCREENSHOT_MUTEX);
  83. } catch (InterruptedException ignored) {
  84. }
  85. }
  86. }
  87. /**
  88. * Takes a screenshot sequence and saves the images with the name prefix in the {@link Config} objects save path.
  89. *
  90. * The name prefix is appended with "_" + sequence_number for each image in the sequence,
  91. * where numbering starts at 0.
  92. *
  93. * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in the
  94. * AndroidManifest.xml of the application under test.
  95. *
  96. * Taking a screenshot will take on the order of 40-100 milliseconds of time on the
  97. * main UI thread. Therefore it is possible to mess up the timing of tests if
  98. * the frameDelay value is set too small.
  99. *
  100. * At present multiple simultaneous screenshot sequences are not supported.
  101. * This method will throw an exception if stopScreenshotSequence() has not been
  102. * called to finish any prior sequences.
  103. *
  104. * @param name the name prefix to give the screenshot
  105. * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality)
  106. * @param frameDelay the time in milliseconds to wait between each frame
  107. * @param maxFrames the maximum number of frames that will comprise this sequence
  108. *
  109. */
  110. public void startScreenshotSequence(final String name, final int quality, final int frameDelay, final int maxFrames) {
  111. initScreenShotSaver();
  112. if(screenshotSequenceThread != null) {
  113. throw new RuntimeException("only one screenshot sequence is supported at a time");
  114. }
  115. screenshotSequenceThread = new ScreenshotSequenceThread(name, quality, frameDelay, maxFrames);
  116. screenshotSequenceThread.start();
  117. }
  118. /**
  119. * Causes a screenshot sequence to end.
  120. *
  121. * If this method is not called to end a sequence and a prior sequence is still in
  122. * progress, startScreenshotSequence() will throw an exception.
  123. */
  124. public void stopScreenshotSequence() {
  125. if(screenshotSequenceThread != null) {
  126. screenshotSequenceThread.interrupt();
  127. screenshotSequenceThread = null;
  128. }
  129. }
  130. /**
  131. * Gets the proper view to use for a screenshot.
  132. */
  133. private View getScreenshotView() {
  134. View decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews());
  135. final long endTime = SystemClock.uptimeMillis() + Timeout.getSmallTimeout();
  136. while (decorView == null) {
  137. final boolean timedOut = SystemClock.uptimeMillis() > endTime;
  138. if (timedOut){
  139. return null;
  140. }
  141. sleeper.sleepMini();
  142. decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews());
  143. }
  144. wrapAllGLViews(decorView);
  145. return decorView;
  146. }
  147. /**
  148. * Extract and wrap the all OpenGL ES Renderer.
  149. */
  150. private void wrapAllGLViews(View decorView) {
  151. ArrayList<GLSurfaceView> currentViews = viewFetcher.getCurrentViews(GLSurfaceView.class, true, decorView);
  152. final CountDownLatch latch = new CountDownLatch(currentViews.size());
  153. for (GLSurfaceView glView : currentViews) {
  154. Object renderContainer = new Reflect(glView).field("mGLThread")
  155. .type(GLSurfaceView.class).out(Object.class);
  156. Renderer renderer = new Reflect(renderContainer).field("mRenderer").out(Renderer.class);
  157. if (renderer == null) {
  158. renderer = new Reflect(glView).field("mRenderer").out(Renderer.class);
  159. renderContainer = glView;
  160. }
  161. if (renderer == null) {
  162. latch.countDown();
  163. continue;
  164. }
  165. if (renderer instanceof GLRenderWrapper) {
  166. GLRenderWrapper wrapper = (GLRenderWrapper) renderer;
  167. wrapper.setTakeScreenshot();
  168. wrapper.setLatch(latch);
  169. } else {
  170. GLRenderWrapper wrapper = new GLRenderWrapper(glView, renderer, latch);
  171. new Reflect(renderContainer).field("mRenderer").in(wrapper);
  172. }
  173. }
  174. try {
  175. latch.await();
  176. } catch (InterruptedException ex) {
  177. ex.printStackTrace();
  178. }
  179. }
  180. /**
  181. * Returns a bitmap of a given WebView.
  182. *
  183. * @param webView the webView to save a bitmap from
  184. * @return a bitmap of the given web view
  185. *
  186. */
  187. private Bitmap getBitmapOfWebView(final WebView webView){
  188. Picture picture = webView.capturePicture();
  189. Bitmap b = Bitmap.createBitmap( picture.getWidth(), picture.getHeight(), Bitmap.Config.ARGB_8888);
  190. Canvas c = new Canvas(b);
  191. picture.draw(c);
  192. return b;
  193. }
  194. /**
  195. * Returns a bitmap of a given View.
  196. *
  197. * @param view the view to save a bitmap from
  198. * @return a bitmap of the given view
  199. *
  200. */
  201. private Bitmap getBitmapOfView(final View view){
  202. view.destroyDrawingCache();
  203. view.buildDrawingCache(false);
  204. Bitmap orig = view.getDrawingCache();
  205. Bitmap.Config config = null;
  206. if(orig == null) {
  207. return null;
  208. }
  209. config = orig.getConfig();
  210. if(config == null) {
  211. config = Bitmap.Config.ARGB_8888;
  212. }
  213. Bitmap b = orig.copy(config, false);
  214. orig.recycle();
  215. view.destroyDrawingCache();
  216. return b;
  217. }
  218. /**
  219. * Returns a proper filename depending on if name is given or not.
  220. *
  221. * @param name the given name
  222. * @return a proper filename depedning on if a name is given or not
  223. *
  224. */
  225. private String getFileName(final String name){
  226. SimpleDateFormat sdf = new SimpleDateFormat("ddMMyy-hhmmss");
  227. String fileName = null;
  228. if(name == null){
  229. if(config.screenshotFileType == ScreenshotFileType.JPEG){
  230. fileName = sdf.format( new Date()).toString()+ ".jpg";
  231. }
  232. else{
  233. fileName = sdf.format( new Date()).toString()+ ".png";
  234. }
  235. }
  236. else {
  237. if(config.screenshotFileType == ScreenshotFileType.JPEG){
  238. fileName = name + ".jpg";
  239. }
  240. else {
  241. fileName = name + ".png";
  242. }
  243. }
  244. return fileName;
  245. }
  246. /**
  247. * This method initializes the aysnc screenshot saving logic
  248. */
  249. private void initScreenShotSaver() {
  250. if(screenShotSaverThread == null || screenShotSaver == null) {
  251. screenShotSaverThread = new HandlerThread("ScreenShotSaver");
  252. screenShotSaverThread.start();
  253. screenShotSaver = new ScreenShotSaver(screenShotSaverThread);
  254. }
  255. }
  256. /**
  257. * This is the thread which causes a screenshot sequence to happen
  258. * in parallel with testing.
  259. */
  260. private class ScreenshotSequenceThread extends Thread {
  261. private int seqno = 0;
  262. private String name;
  263. private int quality;
  264. private int frameDelay;
  265. private int maxFrames;
  266. private boolean keepRunning = true;
  267. public ScreenshotSequenceThread(String _name, int _quality, int _frameDelay, int _maxFrames) {
  268. name = _name;
  269. quality = _quality;
  270. frameDelay = _frameDelay;
  271. maxFrames = _maxFrames;
  272. }
  273. public void run() {
  274. while(seqno < maxFrames) {
  275. if(!keepRunning || Thread.interrupted()) break;
  276. doScreenshot();
  277. seqno++;
  278. try {
  279. Thread.sleep(frameDelay);
  280. } catch (InterruptedException e) {
  281. }
  282. }
  283. screenshotSequenceThread = null;
  284. }
  285. public void doScreenshot() {
  286. View v = getScreenshotView();
  287. if(v == null) keepRunning = false;
  288. String final_name = name+"_"+seqno;
  289. ScreenshotRunnable r = new ScreenshotRunnable(v, final_name, quality);
  290. Log.d(LOG_TAG, "taking screenshot "+final_name);
  291. Activity activity = activityUtils.getCurrentActivity(false);
  292. if(activity != null){
  293. activity.runOnUiThread(r);
  294. }
  295. else {
  296. instrumentation.runOnMainSync(r);
  297. }
  298. }
  299. public void interrupt() {
  300. keepRunning = false;
  301. super.interrupt();
  302. }
  303. }
  304. /**
  305. * Here we have a Runnable which is responsible for taking the actual screenshot,
  306. * and then posting the bitmap to a Handler which will save it.
  307. *
  308. * This Runnable is run on the UI thread.
  309. */
  310. private class ScreenshotRunnable implements Runnable {
  311. private View view;
  312. private String name;
  313. private int quality;
  314. public ScreenshotRunnable(final View _view, final String _name, final int _quality) {
  315. view = _view;
  316. name = _name;
  317. quality = _quality;
  318. }
  319. public void run() {
  320. if(view !=null){
  321. Bitmap b;
  322. if(view instanceof WebView){
  323. b = getBitmapOfWebView((WebView) view);
  324. }
  325. else{
  326. b = getBitmapOfView(view);
  327. }
  328. if(b != null) {
  329. screenShotSaver.saveBitmap(b, name, quality);
  330. b = null;
  331. // Return here so that the screenshotMutex is not unlocked,
  332. // since this is handled by save bitmap
  333. return;
  334. }
  335. else
  336. Log.d(LOG_TAG, "NULL BITMAP!!");
  337. }
  338. // Make sure the screenshotMutex is unlocked
  339. synchronized (screenshotMutex) {
  340. screenshotMutex.notify();
  341. }
  342. }
  343. }
  344. /**
  345. * This class is a Handler which deals with saving the screenshots on a separate thread.
  346. *
  347. * The screenshot logic by necessity has to run on the ui thread. However, in practice
  348. * it seems that saving a screenshot (with quality 100) takes approx twice as long
  349. * as taking it in the first place.
  350. *
  351. * Saving the screenshots in a separate thread like this will thus make the screenshot
  352. * process approx 3x faster as far as the main thread is concerned.
  353. *
  354. */
  355. private class ScreenShotSaver extends Handler {
  356. public ScreenShotSaver(HandlerThread thread) {
  357. super(thread.getLooper());
  358. }
  359. /**
  360. * This method posts a Bitmap with meta-data to the Handler queue.
  361. *
  362. * @param bitmap the bitmap to save
  363. * @param name the name of the file
  364. * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
  365. */
  366. public void saveBitmap(Bitmap bitmap, String name, int quality) {
  367. Message message = this.obtainMessage();
  368. message.arg1 = quality;
  369. message.obj = bitmap;
  370. message.getData().putString("name", name);
  371. this.sendMessage(message);
  372. }
  373. /**
  374. * Here we process the Handler queue and save the bitmaps.
  375. *
  376. * @param message A Message containing the bitmap to save, and some metadata.
  377. */
  378. public void handleMessage(Message message) {
  379. synchronized (screenshotMutex) {
  380. String name = message.getData().getString("name");
  381. int quality = message.arg1;
  382. Bitmap b = (Bitmap)message.obj;
  383. if(b != null) {
  384. saveFile(name, b, quality);
  385. b.recycle();
  386. }
  387. else {
  388. Log.d(LOG_TAG, "NULL BITMAP!!");
  389. }
  390. screenshotMutex.notify();
  391. }
  392. }
  393. /**
  394. * Saves a file.
  395. *
  396. * @param name the name of the file
  397. * @param b the bitmap to save
  398. * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
  399. *
  400. */
  401. private void saveFile(String name, Bitmap b, int quality){
  402. FileOutputStream fos = null;
  403. String fileName = getFileName(name);
  404. File directory = new File(config.screenshotSavePath);
  405. directory.mkdir();
  406. File fileToSave = new File(directory,fileName);
  407. try {
  408. fos = new FileOutputStream(fileToSave);
  409. if(config.screenshotFileType == ScreenshotFileType.JPEG){
  410. if (b.compress(Bitmap.CompressFormat.JPEG, quality, fos) == false){
  411. Log.d(LOG_TAG, "Compress/Write failed");
  412. }
  413. }
  414. else{
  415. if (b.compress(Bitmap.CompressFormat.PNG, quality, fos) == false){
  416. Log.d(LOG_TAG, "Compress/Write failed");
  417. }
  418. }
  419. fos.flush();
  420. fos.close();
  421. } catch (Exception e) {
  422. 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.");
  423. e.printStackTrace();
  424. }
  425. }
  426. }
  427. }