PageRenderTime 42ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

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

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