PageRenderTime 53ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

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

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