/* MonkeyTalk - a cross-platform functional testing tool Copyright (C) 2012 Gorilla Logic, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.gorillalogic.fonemonkey.automators; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManager.MemoryInfo; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.os.BatteryManager; import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.util.Base64; import android.util.DisplayMetrics; import android.view.View; import com.gorillalogic.fonemonkey.Log; import com.gorillalogic.monkeytalk.automators.AutomatorConstants; /** */ public class DeviceAutomator extends AutomatorBase { private static final int SCREENSHOT_RETRIES = 5; private static final long SCREENSHOT_RETRIES_DELAY = 2000; static { Log.log("Initializing DeviceAutomator"); } @Override public String getComponentType() { return AutomatorConstants.TYPE_DEVICE; } @Override public String play(String action, final String... args) { if (action.equalsIgnoreCase(AutomatorConstants.ACTION_BACK)) { return back(); } else if (action.equalsIgnoreCase(AutomatorConstants.ACTION_MENU)) { final Activity activity = AutomationManager.getTopActivity(); if (activity != null) { AutomationManager.runOnUIThread(new Runnable() { public void run() { activity.openOptionsMenu(); } }); // HACK: wait a little for menu to open try { Thread.sleep(500); } catch (InterruptedException ex) { // ignore } } return null; } else if (action.equalsIgnoreCase(AutomatorConstants.ACTION_DUMP)) { // Dump component tree to logcat return AutomationManager.dumpViewTree(); } else if (action.equalsIgnoreCase(AutomatorConstants.ACTION_ROTATE)) { final Activity activity = AutomationManager.getTopActivity(); if (activity != null) { assertArgCount(AutomatorConstants.ACTION_ROTATE, args, 1); if (!("landscape".equalsIgnoreCase(args[0]) || "portrait".equalsIgnoreCase(args[0]))) { throw new IllegalArgumentException( "Expected \"portrait\" or \"landscape\" but found " + args[0]); } AutomationManager.runOnUIThread(new Runnable() { public void run() { if ("landscape".equalsIgnoreCase(args[0])) { activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } else if ("portrait".equalsIgnoreCase(args[0])) { activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else { Log.log("Unexpected rotation value: " + args[0]); } } }); // wait for rotation try { Thread.sleep(250); } catch (InterruptedException ex) { // ignore } } return null; } else if (action.equalsIgnoreCase(AutomatorConstants.ACTION_SCREENSHOT)) { for (int i = 0; i < SCREENSHOT_RETRIES; i++) { printMemoryInfo(); System.gc(); try { final Activity activity = AutomationManager.getTopActivity(); if (activity != null) { // get the root view from the activity View v = activity.getWindow().getDecorView() .findViewById(android.R.id.content).getRootView(); boolean enabled = v.isDrawingCacheEnabled(); Bitmap bitmap; try { v.setDrawingCacheEnabled(true); // Android 3.2 mdpi and 4.0.3 1280x800 mdpi nullpointer Bitmap dc = v.getDrawingCache(); if (dc == null) { // throw new // IllegalStateException("No screenshot available (unable to access drawing cache)."); bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); v.draw(canvas); } else { bitmap = Bitmap.createBitmap(dc); } } catch (Exception e) { Log.log(e); throw new IllegalStateException("No screenshot available."); } finally { v.setDrawingCacheEnabled(enabled); } try { // write the bitmap to bytes ByteArrayOutputStream out = new ByteArrayOutputStream(); boolean success = bitmap.compress(Bitmap.CompressFormat.PNG, 0, out); out.flush(); out.close(); bitmap.recycle(); bitmap = null; System.gc(); if (success) { // return the base64 encoded bytes... return "{screenshot:\"" + Base64.encodeToString(out.toByteArray(), Base64.DEFAULT) + "\"}"; } else { Log.log("Screenshot unsuccessful."); throw new IllegalStateException("No screenshot available."); } } catch (IOException ex) { Log.log(ex); throw new IllegalStateException("No screenshot available. - " + ex.getMessage()); } } Log.log("No root activity for screenshot"); throw new IllegalStateException("No screenshot available."); } catch (Exception e) { if (i + 1 == SCREENSHOT_RETRIES) { throw new RuntimeException(e); } else { try { Thread.sleep(SCREENSHOT_RETRIES_DELAY); } catch (InterruptedException e1) { } } } } } return super.play(action, args); } public static String back() { final Activity activity = AutomationManager.getTopActivity(); if (activity != null) { AutomationManager.runOnUIThread(new Runnable() { public void run() { activity.onBackPressed(); } }); } return null; } @Override public String getValue() { return getOs(); } @Override public String getValue(String path) { // Devices have no "native" properties. Call super to throw exception return super.getProperty(path); } public String getOs() { return "Android"; } public String getVersion() { return Build.VERSION.RELEASE; } public String getResolution() { final Activity activity = AutomationManager.getTopActivity(); if (activity != null) { DisplayMetrics metrics = new DisplayMetrics(); activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); return metrics.widthPixels + "x" + metrics.heightPixels; } return "unknown"; } public String getDensityDpi() { final Activity activity = AutomationManager.getTopActivity(); if (activity != null) { DisplayMetrics metrics = new DisplayMetrics(); activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); return String.valueOf(metrics.densityDpi); } return "unknown"; } public String getName() { return Build.MODEL; } public String getOrientation() { Activity activity = AutomationManager.getTopActivity(); return (activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? "landscape" : "portrait"); } @Override protected String getProperty(String prop) { if ("os".equals(prop)) { return getOs(); } else if ("version".equals(prop)) { return getVersion(); } else if ("resolution".equals(prop)) { return getResolution(); } else if ("name".equals(prop)) { return getName(); } else if ("orientation".equals(prop)) { return getOrientation(); } else if ("battery".equals(prop)) { return getBattery(); } else if ("memory".equals(prop)) { return getMemory(false); } else if ("cpu".equals(prop)) { return getCPU(); } else if ("diskspace".equals(prop)) { return getDiskSpace(); } else if ("allinfo".equals(prop)) { return getMemory(false) + "," + getCPU() + "," + getDiskSpace() + "," + getBattery(); } else if ("totalDiskSpace".equals(prop)) { return getTotalDiskSpace(); } else if ("totalMemory".equals(prop)) { return getMemory(true); // } else if ("densityDpi".equals(prop)) { // return getDensityDpi(); } return super.getProperty(prop); } /** * @return the percentage of the disk (internal storage, possibly sdcard) that is in use */ private String getDiskSpace() { // get the root directory to measure memory from StatFs statFs = new StatFs(Environment.getRootDirectory().getAbsolutePath()); // available blocks are blocks that are available to anything - free blocks can be reserved int usedMem = (statFs.getBlockCount() - statFs.getAvailableBlocks()); return (usedMem * 100) / statFs.getBlockCount() + "%"; } /** * Get the total disk space in the system * * @return the total disk space in kB */ private String getTotalDiskSpace() { StatFs statFs = new StatFs(Environment.getRootDirectory().getAbsolutePath()); return (statFs.getBlockCount() * statFs.getBlockSize()) + " bytes"; } /** * Gets the percentage of the battery (0% discarged, 100% fully charged) * * @return a string representing the percentage of the battery that is charged */ private String getBattery() { Context context = AutomationManager.getTopActivity().getApplicationContext(); Intent batteryStatus = context.registerReceiver(null, new IntentFilter( Intent.ACTION_BATTERY_CHANGED)); int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, 100); return (level * 100) / scale + "%"; } /** * Gets the ram currently in use by the system and returns it as a percentage * * @return a string with the percentage of the ram currently in use, or the total kB of ram in * the system * @param total * whether to get a percentage or the total memory */ private String getMemory(boolean total) { // we're not using the MemoryInfo class because totalMemory is only available in SDK level // 14+ try { // read the proc meminfo file RandomAccessFile reader = new RandomAccessFile("/proc/meminfo", "r"); try { String memTotalRaw = reader.readLine(); String memFreeRaw = reader.readLine(); if (!memTotalRaw.contains("MemTotal") || !memFreeRaw.contains("MemFree")) { return "error"; } int memTotal = Integer.parseInt(memTotalRaw.split(":")[1].replaceAll(" ", "") .replaceAll("kB", "")); if (total) { return (memTotal * 1024) + " bytes"; } int memFree = Integer.parseInt(memFreeRaw.split(":")[1].replaceAll(" ", "") .replaceAll("kB", "")); return (memFree * 100) / memTotal + "%"; } catch (Exception e) { return "error"; } finally { reader.close(); } } catch (Exception e) { return "error"; } } /** * @return a string with the percentage of the CPU cycles in use over a sample period of 300ms */ private String getCPU() { try { RandomAccessFile statReader = new RandomAccessFile("/proc/stat", "r"); try { String line = statReader.readLine(); // only if the line contains cpu (with a space) is it a summary // there should be 10 or eleven elements if (!line.contains("cpu ") || (line.split("\\s+").length != 10 && line.split("\\s+").length != 11)) { return "unknown-unexpected stat"; } /*- Information from proc is a aggregate since bootup * cpuComponents should be as follows: * 0 - cpu name (cpu0, cpu1 or cpu for aggregate) * 1 - user mode processor cycles * 2 - "nice" processes * 3 - system processes * 4 - idle time * 5 - time spent waiting on io * 6 - servicing interrupts * 7 - softirq */ String[] cpuComponents = line.split("\\s+"); // all time spent anywhere but 4 (idle) we will consider in use long inUse1 = Long.parseLong(cpuComponents[1]) + Long.parseLong(cpuComponents[2]) + Long.parseLong(cpuComponents[3]) + Long.parseLong(cpuComponents[5]) + Long.parseLong(cpuComponents[6]) + Long.parseLong(cpuComponents[7]); long idle1 = Long.parseLong(cpuComponents[4]); // because proc numbers are since startup, we need to sample over a timeperiod try { Thread.sleep(360); } catch (Exception e) { return "error sleeping"; } // second sample statReader.seek(0); line = statReader.readLine(); cpuComponents = line.split("\\s+"); long inUse2 = Long.parseLong(cpuComponents[1]) + Long.parseLong(cpuComponents[2]) + Long.parseLong(cpuComponents[3]) + Long.parseLong(cpuComponents[5]) + Long.parseLong(cpuComponents[6]) + Long.parseLong(cpuComponents[7]); long idle2 = Long.parseLong(cpuComponents[4]); return ((inUse2 - inUse1) * 100) / ((inUse2 + idle2) - (inUse1 + idle1)) + "%"; } catch (Exception e) { return "unknown-error parsing" + e; } finally { statReader.close(); } } catch (Exception e) { return "unknown-error reading proc"; } } private void printMemoryInfo() { ActivityManager am = (ActivityManager) AutomationManager.getTopActivity().getSystemService( Service.ACTIVITY_SERVICE); Log.log("Memory Class: " + am.getMemoryClass()); MemoryInfo mi = new MemoryInfo(); am.getMemoryInfo(mi); Log.log("Available memory: " + mi.availMem); Log.log("Low memory: " + mi.lowMemory); } @Override protected Rect getBoundingRectangle() { int x = 0; int y = 0; int w = 1; int h = 1; final Activity activity = AutomationManager.getTopActivity(); if (activity != null) { DisplayMetrics metrics = new DisplayMetrics(); activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); w = metrics.widthPixels; h = metrics.heightPixels; } return new Rect(0, 0, w, h); } }