package com.robotium.solo;
import android.app.Activity;
import android.app.Instrumentation;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.Rect;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.SystemClock;
import android.view.View;
import android.webkit.WebView;
import com.robotium.solo.Solo.Config;
import com.robotium.solo.Solo.Config.ScreenshotFileType;
import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Contains screenshot methods like: takeScreenshot(final View, final String name), startScreenshotSequence(final String name, final int quality, final int frameDelay, final int maxFrames),
* stopScreenshotSequence().
*
*
* @author Renas Reda, renas.reda@robotium.com
*
*/
class ScreenshotTaker {
private static final long TIMEOUT_SCREENSHOT_MUTEX = TimeUnit.SECONDS.toMillis(2);
private final Object screenshotMutex = new Object();
private final Config config;
private static Instrumentation instrumentation;
private final ActivityUtils activityUtils;
private final String LOG_TAG = "Robotium";
private ScreenshotSequenceThread screenshotSequenceThread = null;
private HandlerThread screenShotSaverThread = null;
private ScreenShotSaver screenShotSaver = null;
private final ViewFetcher viewFetcher;
private final Sleeper sleeper;
/**
* Constructs this object.
*
* @param config the {@code Config} instance
* @param instrumentation the {@code Instrumentation} instance.
* @param activityUtils the {@code ActivityUtils} instance
* @param viewFetcher the {@code ViewFetcher} instance
* @param sleeper the {@code Sleeper} instance
*
*/
ScreenshotTaker(Config config, Instrumentation instrumentation, ActivityUtils activityUtils, ViewFetcher viewFetcher, Sleeper sleeper) {
this.config = config;
ScreenshotTaker.instrumentation = instrumentation;
this.activityUtils = activityUtils;
this.viewFetcher = viewFetcher;
this.sleeper = sleeper;
}
/**
* Takes a screenshot and saves it in the {@link Config} objects save path.
* Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test.
*
* @param name the name to give the screenshot image
* @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
*/
public void takeScreenshot(final String name, final int quality) {
View decorView = getScreenshotView();
if(decorView == null)
return;
initScreenShotSaver();
ScreenshotRunnable runnable = new ScreenshotRunnable(decorView, name, quality, -1, -1);
synchronized (screenshotMutex) {
Activity activity = activityUtils.getCurrentActivity(false);
if(activity != null)
activity.runOnUiThread(runnable);
else
instrumentation.runOnMainSync(runnable);
try {
screenshotMutex.wait(TIMEOUT_SCREENSHOT_MUTEX);
} catch (InterruptedException ignored) {
}
}
}
/**
* Takes a screenshot and saves it in the {@link Config} objects save path.
* Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test.
*
* @param name the name to give the screenshot image
* @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
* @param watermark_x x
* @param watermark_y y
*/
public void takeScreenshot(final String name, final int quality, float watermark_x, float watermark_y) {
View decorView = getScreenshotView();
if(decorView == null)
return;
initScreenShotSaver();
ScreenshotRunnable runnable = new ScreenshotRunnable(decorView, name, quality, watermark_x, watermark_y);
synchronized (screenshotMutex) {
Activity activity = activityUtils.getCurrentActivity(false);
if(activity != null)
activity.runOnUiThread(runnable);
else
instrumentation.runOnMainSync(runnable);
try {
screenshotMutex.wait(TIMEOUT_SCREENSHOT_MUTEX);
} catch (InterruptedException ignored) {
}
}
}
/**
* Takes a screenshot sequence and saves the images with the name prefix in the {@link Config} objects save path.
*
* The name prefix is appended with "_" + sequence_number for each image in the sequence,
* where numbering starts at 0.
*
* Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in the
* AndroidManifest.xml of the application under test.
*
* Taking a screenshot will take on the order of 40-100 milliseconds of time on the
* main UI thread. Therefore it is possible to mess up the timing of tests if
* the frameDelay value is set too small.
*
* At present multiple simultaneous screenshot sequences are not supported.
* This method will throw an exception if stopScreenshotSequence() has not been
* called to finish any prior sequences.
*
* @param name the name prefix to give the screenshot
* @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality)
* @param frameDelay the time in milliseconds to wait between each frame
* @param maxFrames the maximum number of frames that will comprise this sequence
*
*/
public void startScreenshotSequence(final String name, final int quality, final int frameDelay, final int maxFrames) {
initScreenShotSaver();
if(screenshotSequenceThread != null) {
throw new RuntimeException("only one screenshot sequence is supported at a time");
}
screenshotSequenceThread = new ScreenshotSequenceThread(name, quality, frameDelay, maxFrames);
screenshotSequenceThread.start();
}
/**
* Causes a screenshot sequence to end.
*
* If this method is not called to end a sequence and a prior sequence is still in
* progress, startScreenshotSequence() will throw an exception.
*/
public void stopScreenshotSequence() {
if(screenshotSequenceThread != null) {
screenshotSequenceThread.interrupt();
screenshotSequenceThread = null;
}
}
/**
* Gets the proper view to use for a screenshot.
*/
private View getScreenshotView() {
View decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews());
final long endTime = SystemClock.uptimeMillis() + Timeout.getSmallTimeout();
while (decorView == null) {
final boolean timedOut = SystemClock.uptimeMillis() > endTime;
if (timedOut){
return null;
}
sleeper.sleepMini();
decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews());
}
wrapAllGLViews(decorView);
return decorView;
}
/**
* Extract and wrap the all OpenGL ES Renderer.
*/
private void wrapAllGLViews(View decorView) {
ArrayList<GLSurfaceView> currentViews = viewFetcher.getCurrentViews(GLSurfaceView.class, true, decorView);
final CountDownLatch latch = new CountDownLatch(currentViews.size());
for (GLSurfaceView glView : currentViews) {
Object renderContainer = new Reflect(glView).field("mGLThread")
.type(GLSurfaceView.class).out(Object.class);
Renderer renderer = new Reflect(renderContainer).field("mRenderer").out(Renderer.class);
if (renderer == null) {
renderer = new Reflect(glView).field("mRenderer").out(Renderer.class);
renderContainer = glView;
}
if (renderer == null) {
latch.countDown();
continue;
}
if (renderer instanceof GLRenderWrapper) {
GLRenderWrapper wrapper = (GLRenderWrapper) renderer;
wrapper.setTakeScreenshot();
wrapper.setLatch(latch);
} else {
GLRenderWrapper wrapper = new GLRenderWrapper(glView, renderer, latch);
new Reflect(renderContainer).field("mRenderer").in(wrapper);
}
}
try {
latch.await();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
/**
* Returns a bitmap of a given WebView.
*
* @param webView the webView to save a bitmap from
* @return a bitmap of the given web view
*
*/
private Bitmap getBitmapOfWebView(final WebView webView){
Picture picture = webView.capturePicture();
Bitmap b = Bitmap.createBitmap( picture.getWidth(), picture.getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
picture.draw(c);
return b;
}
/**
* Returns a bitmap of a given View.
*
* @param view the view to save a bitmap from
* @return a bitmap of the given view
*
*/
private Bitmap getBitmapOfView(final View view){
view.destroyDrawingCache();
view.buildDrawingCache(false);
Bitmap orig = view.getDrawingCache();
Bitmap.Config config = null;
if(orig == null) {
return null;
}
config = orig.getConfig();
if(config == null) {
config = Bitmap.Config.ARGB_8888;
}
Bitmap b = orig.copy(config, false);
orig.recycle();
view.destroyDrawingCache();
return b;
}
private Rect getRect(String string) {
ArrayList<Integer> arrayList = new ArrayList<>();
Pattern p = Pattern.compile("\\d+");
Matcher matcher = p.matcher(string);
while (matcher.find()) {
arrayList.add(Integer.parseInt(matcher.group()));
}
if (arrayList.size() == 4) return new Rect(arrayList.get(0), arrayList.get(1),arrayList.get(2),arrayList.get(3));
return new Rect(0, 0, 0, 0);
}
private Bitmap watermarkForRect(Rect rect) {
Bitmap bitmap = instrumentation.getUiAutomation().takeScreenshot();
if (bitmap != null) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
Paint paint = new Paint();
paint.setAlpha(150);
paint.setAntiAlias(true);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(DesignUtils.px2dip(instrumentation.getContext(), 25));
Bitmap newb = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas cv = new Canvas(newb);
cv.drawBitmap(bitmap, 0, 0, null);
cv.drawRect(rect, paint);
cv.save(Canvas.ALL_SAVE_FLAG);
cv.restore();
return newb;
}
return null;
}
public void takeScreenshotForUiAutomation(String rectInfo, String activity){
Bitmap b = watermarkForRect(getRect(rectInfo));
if (b != null) {
saveFile(activity + "/" + TimeUtils.getDate(), b, 60);
b.recycle();
}
}
/**
* Saves a file.
*
* @param name the name of the file
* @param b the bitmap to save
* @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
*
*/
private void saveFile(String name, Bitmap b, int quality){
FileOutputStream fos = null;
String fileName = getFileName(name);
File directory = null;
String pkg = PackageSingleton.getInstance().getPkg();
String path = String.format(Solo.Config.screenshotSavePath, pkg);
if (fileName.contains("/")) {
directory = new File(path, fileName.split("/")[0]);
fileName = fileName.split("/")[1];
} else {
directory = new File(path);
}
directory.mkdirs();
File fileToSave = new File(directory,fileName);
try {
fos = new FileOutputStream(fileToSave);
if(config.screenshotFileType == ScreenshotFileType.JPEG){
if (!b.compress(Bitmap.CompressFormat.JPEG, quality, fos)){
Log.d(LOG_TAG, "Compress/Write failed");
}
}
else{
if (!b.compress(Bitmap.CompressFormat.PNG, quality, fos)){
Log.d(LOG_TAG, "Compress/Write failed");
}
}
fos.flush();
fos.close();
} catch (Exception e) {
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.");
e.printStackTrace();
}
}
/**
*
* Add a watermark to the image
* @param bitmap image
* @param watermark_x the watermark x coordinate
* @param watermark_y the watermark y coordinate
* @return
*/
public static Bitmap watermark(Bitmap bitmap, float watermark_x, float watermark_y) {
if (bitmap == null) {
return null;
}
int w = bitmap.getWidth();
int h = bitmap.getHeight();
if ((int)watermark_x > w) {
watermark_x = w;
}
if ((int)watermark_y > h) {
watermark_y = h;
}
Paint paint = new Paint();
paint.setAlpha(150);
paint.setAntiAlias(true);
paint.setColor(Color.RED);
Bitmap newb = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas cv = new Canvas(newb);
cv.drawBitmap(bitmap, 0, 0, null);
cv.drawCircle(watermark_x, watermark_y, DesignUtils.px2dip(instrumentation.getContext(), 120), paint);
paint.setAlpha(100);
cv.drawCircle(watermark_x, watermark_y, DesignUtils.px2dip(instrumentation.getContext(), 200), paint);
cv.save(Canvas.ALL_SAVE_FLAG);
cv.restore();
return newb;
}
/**
* Returns a proper filename depending on if name is given or not.
*
* @param name the given name
* @return a proper filename depedning on if a name is given or not
*
*/
private String getFileName(final String name){
SimpleDateFormat sdf = new SimpleDateFormat("ddMMyy-hhmmss");
String fileName = null;
if(name == null){
if(config.screenshotFileType == ScreenshotFileType.JPEG){
fileName = sdf.format(new Date()) + ".jpg";
}
else{
fileName = sdf.format(new Date()) + ".png";
}
}
else {
if(config.screenshotFileType == ScreenshotFileType.JPEG){
fileName = name + ".jpg";
}
else {
fileName = name + ".png";
}
}
return fileName;
}
/**
* This method initializes the aysnc screenshot saving logic
*/
private void initScreenShotSaver() {
if(screenShotSaverThread == null || screenShotSaver == null) {
screenShotSaverThread = new HandlerThread("ScreenShotSaver");
screenShotSaverThread.start();
screenShotSaver = new ScreenShotSaver(screenShotSaverThread);
}
}
/**
* This is the thread which causes a screenshot sequence to happen
* in parallel with testing.
*/
private class ScreenshotSequenceThread extends Thread {
private int seqno = 0;
private String name;
private int quality;
private int frameDelay;
private int maxFrames;
private boolean keepRunning = true;
public ScreenshotSequenceThread(String _name, int _quality, int _frameDelay, int _maxFrames) {
name = _name;
quality = _quality;
frameDelay = _frameDelay;
maxFrames = _maxFrames;
}
public void run() {
while(seqno < maxFrames) {
if(!keepRunning || Thread.interrupted()) break;
doScreenshot();
seqno++;
try {
Thread.sleep(frameDelay);
} catch (InterruptedException e) {
}
}
screenshotSequenceThread = null;
}
public void doScreenshot() {
View v = getScreenshotView();
if(v == null) keepRunning = false;
String final_name = name+"_"+seqno;
ScreenshotRunnable r = new ScreenshotRunnable(v, final_name, quality, -1, -1);
Log.d(LOG_TAG, "taking screenshot "+final_name);
Activity activity = activityUtils.getCurrentActivity(false);
if(activity != null){
activity.runOnUiThread(r);
}
else {
instrumentation.runOnMainSync(r);
}
}
public void interrupt() {
keepRunning = false;
super.interrupt();
}
}
/**
* Here we have a Runnable which is responsible for taking the actual screenshot,
* and then posting the bitmap to a Handler which will save it.
*
* This Runnable is run on the UI thread.
*/
private class ScreenshotRunnable implements Runnable {
private View view;
private String name;
private int quality;
private float watermark_x;
private float watermark_y;
public ScreenshotRunnable(final View _view, final String _name, final int _quality, final float _watermark_x,
final float _watermark_y) {
view = _view;
name = _name;
quality = _quality;
watermark_x = _watermark_x;
watermark_y = _watermark_y;
}
public void run() {
if(view !=null){
Bitmap b;
if(view instanceof WebView){
b = getBitmapOfWebView((WebView) view);
}
else{
b = getBitmapOfView(view);
}
if(b != null) {
screenShotSaver.saveBitmap(b, name, quality, watermark_x, watermark_y);
b = null;
// Return here so that the screenshotMutex is not unlocked,
// since this is handled by save bitmap
return;
}
else
Log.d(LOG_TAG, "NULL BITMAP!!");
}
// Make sure the screenshotMutex is unlocked
synchronized (screenshotMutex) {
screenshotMutex.notify();
}
}
}
/**
* This class is a Handler which deals with saving the screenshots on a separate thread.
*
* The screenshot logic by necessity has to run on the ui thread. However, in practice
* it seems that saving a screenshot (with quality 100) takes approx twice as long
* as taking it in the first place.
*
* Saving the screenshots in a separate thread like this will thus make the screenshot
* process approx 3x faster as far as the main thread is concerned.
*
*/
private class ScreenShotSaver extends Handler {
public ScreenShotSaver(HandlerThread thread) {
super(thread.getLooper());
}
/**
* This method posts a Bitmap with meta-data to the Handler queue.
*
* @param bitmap the bitmap to save
* @param name the name of the file
* @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
*/
public void saveBitmap(Bitmap bitmap, String name, int quality) {
Message message = this.obtainMessage();
message.arg1 = quality;
message.obj = bitmap;
message.getData().putString("name", name);
this.sendMessage(message);
}
/**
* This method posts a Bitmap with meta-data to the Handler queue.
*
* @param bitmap the bitmap to save
* @param name the name of the file
* @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
* @param watermark_x the watermark x coordinate
* @param watermark_y the watermark y coordinate
*/
public void saveBitmap(Bitmap bitmap, String name, int quality, float watermark_x, float watermark_y) {
Message message = this.obtainMessage();
message.arg1 = quality;
message.obj = bitmap;
message.getData().putString("name", name);
message.getData().putFloat("watermark_x", watermark_x);
message.getData().putFloat("watermark_y", watermark_y);
this.sendMessage(message);
}
/**
* Here we process the Handler queue and save the bitmaps.
*
* @param message A Message containing the bitmap to save, and some metadata.
*/
public void handleMessage(Message message) {
synchronized (screenshotMutex) {
String name = message.getData().getString("name");
float watermark_x = message.getData().getFloat("watermark_x");
float watermark_y = message.getData().getFloat("watermark_y");
int quality = message.arg1;
Bitmap b = (Bitmap)message.obj;
if(b != null) {
if (watermark_x >= 0 && watermark_y >= 0) {
Bitmap watermark_b = watermark(b, watermark_x, watermark_y);
if (watermark_b != null) {
saveFile(name, watermark_b, quality);
watermark_b.recycle();
}
} else {
saveFile(name, b, quality);
}
b.recycle();
}
else {
Log.d(LOG_TAG, "NULL BITMAP!!");
}
screenshotMutex.notify();
}
}
}
}