implement instrumentation

This commit is contained in:
Mis012
2024-11-30 18:57:03 +01:00
parent f61f200d7b
commit d5bc4ea9a6
61 changed files with 8723 additions and 58 deletions

View File

@@ -24,15 +24,17 @@ libandroidfw_dep = [
cc.find_library('androidfw', dirs : [ '/usr' / get_option('libdir') / 'art', '/usr/local' / get_option('libdir') / 'art', get_option('prefix') / get_option('libdir') / 'art' ]),
]
if fs.is_file('/usr' / get_option('libdir') / 'java/core-all_classes.jar')
bootclasspath = '/usr' / get_option('libdir') / 'java/core-all_classes.jar'
bootclasspath_dir = '/usr' / get_option('libdir') / 'java'
elif fs.is_file('/usr/local' / get_option('libdir') / 'java/core-all_classes.jar')
bootclasspath = '/usr/local' / get_option('libdir') / 'java/core-all_classes.jar'
bootclasspath_dir = '/usr/local' / get_option('libdir') / 'java'
elif fs.is_file(get_option('prefix') / get_option('libdir') / 'java/core-all_classes.jar')
bootclasspath = get_option('prefix') / get_option('libdir') / 'java/core-all_classes.jar'
bootclasspath_dir = get_option('prefix') / get_option('libdir') / 'java'
else
error('bootclasspath "core-all_classes.jar" not found')
endif
bootclasspath = bootclasspath_dir / 'core-all_classes.jar' + ':' + bootclasspath_dir / 'core-junit_classes.jar' + ':' + bootclasspath_dir / 'junit-runner_classes.jar'
marshal_files = gnome.genmarshal('marshal',
sources: 'src/api-impl-jni/widgets/marshal.list',
valist_marshallers: true,
@@ -194,6 +196,14 @@ custom_target('api-impl.jar', build_by_default: true, input: [hax_jar], output:
install_dir : get_option('libdir') / 'java/dex/android_translation_layer',
command: ['dx', '--dex', '--output='+join_paths(builddir_base, 'api-impl.jar'), hax_jar.full_path()])
# test-runner.jar
subdir('src/test-runner')
custom_target('test_runner.jar', build_by_default: true, input: [test_runner_jar], output: ['test_runner.jar'],
install: true,
install_dir : get_option('libdir') / 'java/dex/android_translation_layer',
command: ['dx', '--dex', '--output='+join_paths(builddir_base, 'test_runner.jar'), test_runner_jar.full_path()])
#framework-res.apk
subdir('res')

View File

@@ -166,8 +166,11 @@ void set_up_handle_cache(JNIEnv *env)
handle_cache.drawable.setBounds = _METHOD(handle_cache.drawable.class, "setBounds", "(IIII)V");
handle_cache.intent.class = _REF((*env)->FindClass(env, "android/content/Intent"));
handle_cache.intent.constructor = _METHOD(handle_cache.intent.class, "<init>", "()V");
handle_cache.intent.putExtraCharSequence = _METHOD(handle_cache.intent.class, "putExtra", "(Ljava/lang/String;Ljava/lang/CharSequence;)Landroid/content/Intent;");
handle_cache.instrumentation.class = _REF((*env)->FindClass(env, "android/app/Instrumentation"));
handle_cache.webview.class = _REF((*env)->FindClass(env, "android/webkit/WebView"));
handle_cache.webview.internalGetAssetManager = _METHOD(handle_cache.webview.class, "internalGetAssetManager", "()Landroid/content/res/AssetManager;");
handle_cache.webview.internalLoadChanged = _METHOD(handle_cache.webview.class, "internalLoadChanged", "(ILjava/lang/String;)V");

View File

@@ -115,6 +115,7 @@ struct handle_cache {
} drawable;
struct {
jclass class;
jmethodID constructor;
jmethodID putExtraCharSequence;
} intent;
struct {
@@ -122,6 +123,11 @@ struct handle_cache {
jmethodID internalGetAssetManager;
jmethodID internalLoadChanged;
} webview;
struct {
jclass class;
jmethodID onCreate;
jmethodID start;
} instrumentation;
};
extern struct handle_cache handle_cache;

View File

@@ -0,0 +1,381 @@
package android.app;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageParser;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.MessageQueue;
import android.util.Slog;
import android.view.KeyEvent;
import dalvik.system.DexClassLoader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
/* for hacky classloader patching */
import dalvik.system.DexFile;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
public class Instrumentation {
private static final String TAG = "Instrumentation";
public static final String REPORT_KEY_IDENTIFIER = "id";
public static final String REPORT_KEY_STREAMRESULT = "stream";
private static Instrumentation create(String className, Intent arguments) throws Exception {
Thread.setUncaughtExceptionPreHandler(new ExceptionHandler());
try {
String target_package = null;
for (PackageParser.Instrumentation instrumentation : Context.pkg.instrumentation) {
if(className.equals(instrumentation.className)) {
target_package = instrumentation.info.targetPackage;
break;
}
}
System.out.println("targetPackage: " + target_package);
String target_path = android.os.Environment.getExternalStorageDirectory()+"/../_installed_apks_/"+target_package+".apk";
Context.this_application.getAssets().addAssetPath(target_path);
patchClassLoader(DexClassLoader.getSystemClassLoader(), new File(target_path));
Class<? extends Instrumentation> cls = Class.forName(className).asSubclass(Instrumentation.class);
Constructor<? extends Instrumentation> constructor = cls.getConstructor();
Instrumentation i = constructor.newInstance();
i.onCreate(arguments.getExtras());
return i;
} catch (Exception e) {
/* there is no global handler for exceptions on the main thread */
Thread.getUncaughtExceptionPreHandler().uncaughtException(Thread.currentThread(), e);
}
return null; // we will never get here
}
public Instrumentation() {
}
public void start() {
Thread t = new Thread(new Runnable() {
public void run() {
//Looper.prepare();
onStart();
}
});
t.start();
}
public void onCreate(Bundle arguments) {
}
public boolean onException(Object obj, Throwable e) {
return false;
}
public void onStart() {
}
public Context getContext() {
return new Context();
}
public Context getTargetContext() {
return new Context();
}
public void setAutomaticPerformanceSnapshots() {
}
public void setInTouchMode(boolean inTouch) {
Slog.w(TAG, "FIXME: Instrumentation.setInTouchMode: " + inTouch);
}
public void sendKeySync(KeyEvent event) {
validateNotAppThread();
/*long downTime = event.getDownTime();
long eventTime = event.getEventTime();
int source = event.getSource();
if (source == InputDevice.SOURCE_UNKNOWN) {
source = InputDevice.SOURCE_KEYBOARD;
}
if (eventTime == 0) {
eventTime = SystemClock.uptimeMillis();
}
if (downTime == 0) {
downTime = eventTime;
}
KeyEvent newEvent = new KeyEvent(event);
newEvent.setTime(downTime, eventTime);
newEvent.setSource(source);
newEvent.setFlags(event.getFlags() | KeyEvent.FLAG_FROM_SYSTEM);
setDisplayIfNeeded(newEvent);
InputManagerGlobal.getInstance().injectInputEvent(newEvent,
InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);*/
Slog.w(TAG, "FIXME: Instrumentation.sendKeySync: " + event);
}
public void sendKeyDownUpSync(int keyCode) {
sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
}
static public Application newApplication(Class<?> clazz, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return Context.this_application; // we don't (currently?) support multiple applications in a single process
}
public Activity newActivity(Class<?> clazz, Context context, IBinder token, Application application,
Intent intent, ActivityInfo info, CharSequence title, Activity parent,
String id, Object lastNonConfigurationInstance) throws InstantiationException, IllegalAccessException {
Activity activity = (Activity)clazz.newInstance();
activity.getWindow().native_window = Context.this_application.native_window;
Slog.i(TAG, "activity.getWindow().native_window >"+activity.getWindow().native_window+"<");
return activity;
}
/* Copyright (C) 2006 The Android Open Source Project */
public void callActivityOnCreate(Activity activity, Bundle savedState) {
//prePerformCreate(activity);
runOnMainSync(new Runnable() {
public void run() {
activity.onCreate(savedState);
}
});
//postPerformCreate(activity);
}
public Activity startActivitySync(Intent intent) {
return startActivitySync(intent, null);
}
/* TODO - deduplicate with startActivityForResult? */
public Activity startActivitySync(Intent intent, Bundle options) {
Slog.i(TAG, "startActivitySync(" + intent + ", " + options + ") called");
if (intent.getComponent() != null) {
try {
Class<? extends Activity> cls = Class.forName(intent.getComponent().getClassName()).asSubclass(Activity.class);
Constructor<? extends Activity> constructor = cls.getConstructor();
final Activity activity = constructor.newInstance();
activity.intent = intent;
activity.getWindow().native_window = Context.this_application.native_window;
runOnMainSync(new Runnable() {
@Override
public void run() {
Activity.nativeStartActivity(activity);
}
});
return activity;
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
/* not sure what to do here */
}
} /*else if (FILE_CHOOSER_ACTIONS.contains(intent.getAction())) { // not sure what to do here either
nativeFileChooser(FILE_CHOOSER_ACTIONS.indexOf(intent.getAction()), intent.getType(), intent.getStringExtra("android.intent.extra.TITLE"), requestCode);
} */else {
Slog.i(TAG, "startActivityForResult: intent was not handled.");
}
return null;
}
public void runOnMainSync(Runnable runner) {
validateNotAppThread();
SyncRunnable sr = new SyncRunnable(runner);
new Handler(Looper.getMainLooper()).post(sr);
sr.waitForComplete();
}
public void sendStatus(int resultCode, Bundle results) {
if (results != null) {
for (String key : sorted(results.keySet())) {
System.out.println("INSTRUMENTATION_STATUS: " + key + "=" + results.get(key));
}
}
System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode);
}
public void finish(int resultCode, Bundle results) {
boolean need_hack = false;
if (results != null) {
for (String key : sorted(results.keySet())) {
System.out.println("INSTRUMENTATION_RESULT: " + key + "=" + results.get(key));
/* HACK: no idea why this isn't recognized as an error otherwise */
if(((String)results.get(key)).contains("Test run aborted due to unexpected exception"))
need_hack = true;
}
}
if(need_hack) {
System.out.println("INSTRUMENTATION_STATUS: shortMsg=ugly hack: Test run aborted due to unexpected exception");
System.out.println("INSTRUMENTATION_STATUS_CODE: -1");
}
System.out.println("INSTRUMENTATION_CODE: " + resultCode);
System.exit(0);
}
/* Copyright (C) 2006 The Android Open Source Project */
private static Collection<String> sorted(Collection<String> list) {
final ArrayList<String> copy = new ArrayList<>(list);
Collections.sort(copy);
return copy;
}
/* Copyright (C) 2006 The Android Open Source Project */
private static final class SyncRunnable implements Runnable {
private final Runnable mTarget;
private boolean mComplete;
public SyncRunnable(Runnable target) {
mTarget = target;
}
public void run() {
mTarget.run();
synchronized (this) {
mComplete = true;
notifyAll();
}
}
public void waitForComplete() {
synchronized (this) {
while (!mComplete) {
try {
wait();
} catch (InterruptedException e) {}
}
}
}
}
/* Copyright (C) 2006 The Android Open Source Project */
private static final class EmptyRunnable implements Runnable {
public void run() {}
}
/* Copyright (C) 2006 The Android Open Source Project */
private static final class Idler implements MessageQueue.IdleHandler {
private final Runnable mCallback;
private boolean mIdle;
public Idler(Runnable callback) {
mCallback = callback;
mIdle = false;
}
public final boolean queueIdle() {
if (mCallback != null) {
mCallback.run();
}
synchronized (this) {
mIdle = true;
notifyAll();
}
return false;
}
public void waitForIdle() {
synchronized (this) {
while (!mIdle) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
}
}
/* Copyright (C) 2006 The Android Open Source Project */
public void waitForIdleSync() {
/*validateNotAppThread();
Idler idler = new Idler(null);
Looper.myLooper().myQueue().addIdleHandler(idler);
new Handler(Looper.myLooper()).post(new EmptyRunnable());
idler.waitForIdle();*/
}
/* Copyright (C) 2006 The Android Open Source Project */
private final void validateNotAppThread() {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("This method can not be called from the main application thread");
}
}
private static class ExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
//onException(null /*FIXME?*/, e);
System.out.print("INSTRUMENTATION_RESULT: shortMsg=");
e.printStackTrace();
System.out.println("INSTRUMENTATION_STATUS_CODE: -1");
System.exit(1);
}
}
/* -- a hacky method to patch in a classpath entry (there should be a better way *in theory*, but other approaches didn't work) -- */
private static Object getFieldObject(Class cls, Object obj, String field_name) {
try {
Field field = cls.getDeclaredField(field_name);
field.setAccessible(true);
Object ret = field.get(obj);
field.setAccessible(false);
return ret;
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
private static void setFieldObject(Class cls, Object obj, String field_name, Object value) {
try {
Field field = cls.getDeclaredField(field_name);
field.setAccessible(true);
field.set(obj, value);
field.setAccessible(false);
} catch(Exception e) {
e.printStackTrace();
}
}
private static Object createObject(Class cls, Class[] type_array, Object[] value_array) {
try {
Constructor ctor = cls.getDeclaredConstructor(type_array);
ctor.setAccessible(true);
Object ret = ctor.newInstance(value_array);
ctor.setAccessible(false);
return ret;
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
private static void patchClassLoader(ClassLoader cl, File apk_path) throws IOException {
// get cl.pathList
Object pathList = getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");
// get pathList.dexElements
Object[] dexElements = (Object[]) getFieldObject(pathList.getClass(), pathList, "dexElements");
// Element type
Class<?> Element_class = dexElements.getClass().getComponentType();
// Create an array to replace the original array
Object[] DexElements_new = (Object[]) Array.newInstance(Element_class, dexElements.length + 1);
// use this constructor: ElementDexFile.class(DexFile dexFile, File file)
Class[] type_array = {DexFile.class, File.class};
Object[] value_array = {DexFile.loadDex(apk_path.getCanonicalPath(), null, 0), apk_path};
Object new_element = createObject(Element_class, type_array, value_array);
Object[] new_element_wrapper_array = new Object[] {new_element};
// Copy the original elements
System.arraycopy(dexElements, 0, DexElements_new, 0, dexElements.length);
// The element of the plugin is copied in
System.arraycopy(new_element_wrapper_array, 0, DexElements_new, dexElements.length, new_element_wrapper_array.length);
// replace
setFieldObject(pathList.getClass(), pathList, "dexElements", DexElements_new);
}
}

View File

@@ -25,7 +25,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable.ConstantState;
import android.os.Build;
import android.os.Bundle;
// import android.os.IBinder;
import android.os.IBinder;
import android.os.Trace;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
@@ -48,8 +48,6 @@ import org.xmlpull.v1.XmlPullParserException;
class Movie {}
class IBinder {}
/**
* Class for accessing an application's resources. This sits on top of the
* asset manager of the application (accessible through {@link #getAssets}) and
@@ -701,7 +699,10 @@ public class Resources {
}
getValue(id, value, true);
}
Drawable res = loadDrawable(value, id);
Drawable res = null;
try {
res = loadDrawable(value, id);
} catch (NotFoundException e) { e.printStackTrace(); }
synchronized (mAccessLock) {
if (mTmpValue == null) {
mTmpValue = value;

View File

@@ -35,6 +35,7 @@ hax_jar = jar('hax', [
'android/app/Fragment.java',
'android/app/FragmentManager.java',
'android/app/FragmentTransaction.java',
'android/app/Instrumentation.java',
'android/app/IntentService.java',
'android/app/KeyguardManager.java',
'android/app/ListActivity.java',

View File

@@ -5,7 +5,6 @@
#include <gtk/gtk.h>
#include <libportal/portal.h>
#include "../api-impl-jni/defines.h"
#include "../api-impl-jni/util.h"
#include "../api-impl-jni/app/android_app_Activity.h"
@@ -78,7 +77,7 @@ char *construct_classpath(char *prefix, char **cp_array, size_t len)
#define JDWP_ARG "-XjdwpOptions:transport=dt_socket,server=y,suspend=y,address="
JNIEnv *create_vm(char *api_impl_jar, char *apk_classpath, char *microg_apk, char *framework_res_apk, char *api_impl_natives_dir, char *app_lib_dir, char **extra_jvm_options)
JNIEnv *create_vm(char *api_impl_jar, char *apk_classpath, char *microg_apk, char *framework_res_apk, char *test_runner_jar, char *api_impl_natives_dir, char *app_lib_dir, char **extra_jvm_options)
{
JavaVM *jvm;
JNIEnv *env;
@@ -106,7 +105,7 @@ JNIEnv *create_vm(char *api_impl_jar, char *apk_classpath, char *microg_apk, cha
options[0].optionString = construct_classpath("-Djava.library.path=", (char *[]){api_impl_natives_dir, app_lib_dir}, 2);
}
options[1].optionString = construct_classpath("-Djava.class.path=", (char *[]){api_impl_jar, apk_classpath, microg_apk, framework_res_apk}, 4);
options[1].optionString = construct_classpath("-Djava.class.path=", (char *[]){api_impl_jar, apk_classpath, microg_apk, framework_res_apk, test_runner_jar}, 5);
options[2].optionString = "-Xcheck:jni";
if (jdwp_port) {
strncat(jdwp_option_string, jdwp_port, 5); // 5 chars is enough for a port number, and won't overflow our array
@@ -182,19 +181,23 @@ void dl_parse_library_path(const char *path, char *delim);
#define REL_DEX_INSTALL_PATH "/../java/dex"
#define REL_API_IMPL_JAR_INSTALL_PATH "/android_translation_layer/api-impl.jar"
#define REL_TEST_RUNNER_JAR_INSTALL_PATH "/android_translation_layer/test_runner.jar"
#define REL_API_IMPL_NATIVES_INSTALL_PATH "/android_translation_layer/natives"
#define REL_MICROG_APK_INSTALL_PATH "/microg/com.google.android.gms.apk"
#define REL_FRAMEWORK_RES_INSTALL_PATH "/android_translation_layer/framework-res.apk"
#define API_IMPL_JAR_PATH_LOCAL "./api-impl.jar"
#define TEST_RUNNER_JAR_PATH_LOCAL "./test_runner.jar"
#define MICROG_APK_PATH_LOCAL "./com.google.android.gms.apk"
#define FRAMEWORK_RES_PATH_LOCAL "./res/framework-res.apk"
struct jni_callback_data {
char *apk_main_activity_class;
char *apk_instrumentation_class;
uint32_t window_width;
uint32_t window_height;
gboolean install;
gboolean install_internal;
char *prgname;
char **extra_jvm_options;
char **extra_string_keys;
@@ -202,6 +205,27 @@ struct jni_callback_data {
static char *uri_option = NULL;
static void parse_string_extras(JNIEnv *env, char **extra_string_keys, jobject intent)
{
GError *error = NULL;
GRegex *regex = g_regex_new("(?<!\\\\)=", 0, 0, &error);
if (!regex) {
fprintf(stderr, "g_regex_new error: '%s'\n", error->message);
exit(1);
}
for (char **arg = extra_string_keys; *arg; arg++) {
gchar **keyval = g_regex_split_full(regex, *arg, -1, 0, 0, 2, NULL);
if (!keyval || !keyval[0] || !keyval[1]) {
fprintf(stderr, "extra string arg not in 'key=value' format: '%s'\n", *arg);
exit(1);
}
(*env)->CallObjectMethod(env, intent, handle_cache.intent.putExtraCharSequence, _JSTRING(keyval[0]), _JSTRING(keyval[1]));
g_strfreev(keyval);
}
g_regex_unref(regex);
}
static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *hint, struct jni_callback_data *d)
{
// TODO: pass all files to classpath
@@ -213,6 +237,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
*/
char *dex_install_dir;
char *api_impl_jar;
char *test_runner_jar = NULL;
char *microg_apk = NULL;
char *framework_res_apk = NULL;
const char *package_name;
@@ -231,7 +256,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
}
if (access(apk_classpath, F_OK) < 0) {
printf("error: the specified file path doesn't seem to exist (%m)\n");
printf("error: the specified file path (%s) doesn't seem to exist (%m)\n", apk_classpath);
exit(1);
}
@@ -353,6 +378,30 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
}
}
if(d->apk_instrumentation_class) {
ret = stat(TEST_RUNNER_JAR_PATH_LOCAL, &dont_care);
errno_localdir = errno;
if (!ret) {
test_runner_jar = strdup(TEST_RUNNER_JAR_PATH_LOCAL); // for running out of builddir; using strdup so we can always safely call free on this
} else {
char *test_runner_jar_install_dir = malloc(strlen(dex_install_dir) + strlen(REL_TEST_RUNNER_JAR_INSTALL_PATH) + 1); // +1 for NULL
strcpy(test_runner_jar_install_dir, dex_install_dir);
strcat(test_runner_jar_install_dir, REL_TEST_RUNNER_JAR_INSTALL_PATH);
ret = stat(test_runner_jar_install_dir, &dont_care);
errno_libdir = errno;
if (!ret) {
test_runner_jar = test_runner_jar_install_dir;
} else {
printf("warning: can't stat test_runner.jar; tried:\n"
"\t\"" TEST_RUNNER_JAR_PATH_LOCAL "\", got - %s\n"
"\t\"%s\", got - %s\n",
strerror(errno_localdir),
test_runner_jar_install_dir, strerror(errno_libdir));
}
}
}
char *api_impl_natives_dir = malloc(strlen(dex_install_dir) + strlen(REL_API_IMPL_NATIVES_INSTALL_PATH) + 1); // +1 for NULL
strcpy(api_impl_natives_dir, dex_install_dir);
strcat(api_impl_natives_dir, REL_API_IMPL_NATIVES_INSTALL_PATH);
@@ -371,7 +420,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
dl_parse_library_path(ld_path, ":");
g_free(ld_path);
JNIEnv *env = create_vm(api_impl_jar, apk_classpath, microg_apk, framework_res_apk, api_impl_natives_dir, app_lib_dir, d->extra_jvm_options);
JNIEnv *env = create_vm(api_impl_jar, apk_classpath, microg_apk, framework_res_apk, test_runner_jar, api_impl_natives_dir, app_lib_dir, d->extra_jvm_options);
free(app_lib_dir);
@@ -443,7 +492,29 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
if ((*env)->ExceptionCheck(env))
(*env)->ExceptionDescribe(env);
if (d->apk_instrumentation_class) {
if (d->apk_main_activity_class) {
fprintf(stderr, "error: both --instrument and --launch-activity supplied, exiting\n");
exit(1);
}
jobject intent = NULL;
if (d->extra_string_keys) {
intent = (*env)->NewObject(env, handle_cache.intent.class, handle_cache.intent.constructor);
parse_string_extras(env, d->extra_string_keys, intent);
}
(*env)->CallStaticObjectMethod(env, handle_cache.instrumentation.class,
_STATIC_METHOD(handle_cache.instrumentation.class, "create", "(Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Instrumentation;"),
_JSTRING(d->apk_instrumentation_class), intent);
if ((*env)->ExceptionCheck(env))
(*env)->ExceptionDescribe(env);
}
// construct main Activity
if (!d->apk_instrumentation_class && !d->install_internal) {
activity_object = (*env)->CallStaticObjectMethod(env, handle_cache.activity.class,
_STATIC_METHOD(handle_cache.activity.class, "createMainActivity", "(Ljava/lang/String;JLjava/lang/String;)Landroid/app/Activity;"),
_JSTRING(d->apk_main_activity_class), _INTPTR(window), (uri_option && *uri_option) ? _JSTRING(uri_option) : NULL);
@@ -453,41 +524,31 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
g_free(uri_option);
if (d->extra_string_keys) {
GError *error = NULL;
GRegex *regex = g_regex_new("(?<!\\\\)=", 0, 0, &error);
if (!regex) {
fprintf(stderr, "g_regex_new error: '%s'\n", error->message);
exit(1);
}
jobject intent = _GET_OBJ_FIELD(activity_object, "intent", "Landroid/content/Intent;");
for (char **arg = d->extra_string_keys; *arg; arg++) {
gchar **keyval = g_regex_split_full(regex, *arg, -1, 0, 0, 2, NULL);
if (!keyval || !keyval[0] || !keyval[1]) {
fprintf(stderr, "extra string arg not in 'key=value' format: '%s'\n", *arg);
exit(1);
}
(*env)->CallObjectMethod(env, intent, handle_cache.intent.putExtraCharSequence, _JSTRING(keyval[0]), _JSTRING(keyval[1]));
g_strfreev(keyval);
}
g_regex_unref(regex);
parse_string_extras(env, d->extra_string_keys, intent);
g_strfreev(d->extra_string_keys);
}
}
/* -- set the window title and app icon -- */
jstring package_name_jstr = (*env)->CallObjectMethod(env, activity_object, handle_cache.context.get_package_name);
if (!d->apk_instrumentation_class) {
jstring package_name_jstr = (*env)->CallObjectMethod(env, application_object, handle_cache.context.get_package_name);
package_name = package_name_jstr ? _CSTRING(package_name_jstr) : NULL;
if ((*env)->ExceptionCheck(env))
(*env)->ExceptionDescribe(env);
}
jstring app_icon_path_jstr = (*env)->CallObjectMethod(env, application_object, handle_cache.application.get_app_icon_path);
const char *app_icon_path = app_icon_path_jstr ? _CSTRING(app_icon_path_jstr) : NULL;
if ((*env)->ExceptionCheck(env))
(*env)->ExceptionDescribe(env);
if (d->install) {
if (d->install || d->install_internal) {
if (d->apk_instrumentation_class) {
fprintf(stderr, "error: --instrument supplied together with --install, exiting\n");
exit(1);
}
XdpPortal *portal = xdp_portal_new();
const char *app_label = _CSTRING((*env)->CallObjectMethod(env, application_object, _METHOD(handle_cache.application.class, "get_app_label", "()Ljava/lang/String;")));
@@ -495,7 +556,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
(*env)->ExceptionDescribe(env);
GVariant *icon_serialized = NULL;
if (app_icon_path) {
if (app_icon_path && !d->install_internal) {
extract_from_apk(app_icon_path, app_icon_path);
char *app_icon_path_full = g_strdup_printf("%s/%s", app_data_dir, app_icon_path);
GMappedFile *icon_file = g_mapped_file_new(app_icon_path_full, FALSE, NULL);
@@ -507,10 +568,19 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
g_mapped_file_unref(icon_file);
g_free(app_icon_path_full);
}
GFile *dest = g_file_new_build_filename(app_data_dir_base, "_installed_apks_", apk_name, NULL);
gchar *dest_name = g_strdup_printf("%s.apk", package_name);
GFile *dest = g_file_new_build_filename(app_data_dir_base, "_installed_apks_", d->install_internal ? dest_name : apk_name, NULL);
free(dest_name);
printf("installing %s to %s\n", apk_name, g_file_get_path(dest));
g_file_make_directory(g_file_get_parent(dest), NULL, NULL);
g_file_copy(files[0], dest, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, NULL);
GError *err = NULL;
g_file_copy(files[0], dest, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &err);
if(err)
printf("error copying apk: %s\n", err->message);
if(d->install_internal)
exit(0);
jmethodID get_supported_mime_types = _METHOD(handle_cache.application.class, "get_supported_mime_types", "()Ljava/lang/String;");
jstring supported_mime_types_jstr = (*env)->CallObjectMethod(env, application_object, get_supported_mime_types);
@@ -550,6 +620,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
return;
}
if (!d->apk_instrumentation_class)
gtk_window_set_title(GTK_WINDOW(window), package_name);
gtk_window_set_default_size(GTK_WINDOW(window), d->window_width, d->window_height);
g_signal_connect(window, "close-request", G_CALLBACK(app_exit), env);
@@ -563,7 +634,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
// set package name as application id for window icon on Wayland. Needs a {package_name}.desktop file defining the icon
GdkToplevel *toplevel = GDK_TOPLEVEL(gtk_native_get_surface(GTK_NATIVE(window)));
if (GDK_IS_WAYLAND_TOPLEVEL(toplevel)) {
if (GDK_IS_WAYLAND_TOPLEVEL(toplevel) && !d->apk_instrumentation_class) {
gdk_wayland_toplevel_set_application_id(GDK_WAYLAND_TOPLEVEL(toplevel), package_name);
}
GdkMonitor *monitor = gdk_display_get_monitor_at_surface(gdk_display_get_default(), GDK_SURFACE(toplevel));
@@ -593,9 +664,11 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
g_signal_connect_after(window, "realize", G_CALLBACK(icon_override), icon_list);
}
if (!d->apk_instrumentation_class) {
activity_start(env, activity_object);
g_timeout_add(10, G_SOURCE_FUNC(hacky_on_window_focus_changed_callback), env);
}
jobject input_queue_callback = g_object_get_data(G_OBJECT(window), "input_queue_callback");
if (input_queue_callback) {
@@ -626,9 +699,11 @@ void init_cmd_parameters(GApplication *app, struct jni_callback_data *d)
const GOptionEntry cmd_params[] = {
/* long_name | short_name | flags | arg | arg_data | description | arg_desc */
{ "launch-activity", 'l', 0, G_OPTION_ARG_STRING, &d->apk_main_activity_class, "the fully qualifed name of the activity you wish to launch (usually the apk's main activity)", "ACTIVITY_NAME" },
{ "instrument", 0, 0, G_OPTION_ARG_STRING, &d->apk_instrumentation_class, "the fully qualifed name of the instrumentation you wish to launch", "CLASS_NAME" },
{ "window-width", 'w', 0, G_OPTION_ARG_INT, &d->window_width, "window width to launch with (some apps react poorly to runtime window size adjustments)", "WIDTH" },
{ "window-height", 'h', 0, G_OPTION_ARG_INT, &d->window_height, "window height to launch with (some apps react poorly to runtime window size adjustments)", "HEIGHT" },
{ "install", 'i', 0, G_OPTION_ARG_NONE, &d->install, "install .desktop file for the given apk", NULL },
{ "install-internal", 0 , 0, G_OPTION_ARG_NONE, &d->install_internal, "copy an apk to _installed_apks_ but don't create a desktop entry", NULL },
{ "extra-jvm-option", 'X', 0, G_OPTION_ARG_STRING_ARRAY, &d->extra_jvm_options, "pass an additional option directly to art (e.g -X \"-verbose:jni\")", "\"OPTION\"" },
{ "extra-string-key", 'e', 0, G_OPTION_ARG_STRING_ARRAY, &d->extra_string_keys, "pass a string extra (-e key=value)", "\"KEY=VALUE\"" },
{ "uri", 'u', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, option_uri_cb, "open the given URI inside the application", "URI" },
@@ -655,9 +730,11 @@ int main(int argc, char **argv)
struct jni_callback_data *callback_data = malloc(sizeof(struct jni_callback_data));
callback_data->apk_main_activity_class = NULL;
callback_data->apk_instrumentation_class = NULL;
callback_data->window_width = 960;
callback_data->window_height = 540;
callback_data->install = FALSE;
callback_data->install_internal = FALSE;
callback_data->prgname = argv[0];
callback_data->extra_jvm_options = NULL;
callback_data->extra_string_keys = NULL;

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.app.Activity;
/**
* This class provides functional testing of a single activity. The activity under test will
* be created using the system infrastructure (by calling InstrumentationTestCase.launchActivity())
* and you will then be able to manipulate your Activity directly. Most of the work is handled
* automatically here by {@link #setUp} and {@link #tearDown}.
*
* <p>If you prefer an isolated unit test, see {@link android.test.ActivityUnitTestCase}.
*
* @deprecated new tests should be written using
* {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for
* configuring the Activity under test
*/
@Deprecated
public abstract class ActivityInstrumentationTestCase<T extends Activity>
extends ActivityTestCase {
String mPackage;
Class<T> mActivityClass;
boolean mInitialTouchMode = false;
/**
* Creates an {@link ActivityInstrumentationTestCase} in non-touch mode.
*
* @param pkg ignored - no longer in use.
* @param activityClass The activity to test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
*/
public ActivityInstrumentationTestCase(String pkg, Class<T> activityClass) {
this(pkg, activityClass, false);
}
/**
* Creates an {@link ActivityInstrumentationTestCase}.
*
* @param pkg ignored - no longer in use.
* @param activityClass The activity to test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* @param initialTouchMode true = in touch mode
*/
public ActivityInstrumentationTestCase(String pkg, Class<T> activityClass,
boolean initialTouchMode) {
mActivityClass = activityClass;
mInitialTouchMode = initialTouchMode;
}
@Override
public T getActivity() {
return (T) super.getActivity();
}
@Override
protected void setUp() throws Exception {
super.setUp();
// set initial touch mode
getInstrumentation().setInTouchMode(mInitialTouchMode);
final String targetPackageName = getInstrumentation().getTargetContext().getPackageName();
setActivity(launchActivity(targetPackageName, mActivityClass, null));
}
@Override
protected void tearDown() throws Exception {
getActivity().finish();
setActivity(null);
// Scrub out members - protects against memory leaks in the case where someone
// creates a non-static inner class (thus referencing the test case) and gives it to
// someone else to hold onto
scrubClass(ActivityInstrumentationTestCase.class);
super.tearDown();
}
public void testActivityTestCaseSetUpProperly() throws Exception {
assertNotNull("activity should be launched successfully", getActivity());
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.app.Activity;
import android.content.Intent;
import java.lang.reflect.Method;
/**
* This class provides functional testing of a single activity. The activity under test will
* be created using the system infrastructure (by calling InstrumentationTestCase.launchActivity())
* and you will then be able to manipulate your Activity directly.
*
* <p>Other options supported by this test case include:
* <ul>
* <li>You can run any test method on the UI thread (see {@link android.test.UiThreadTest}).</li>
* <li>You can inject custom Intents into your Activity (see
* {@link #setActivityIntent(Intent)}).</li>
* </ul>
*
* <p>This class replaces {@link android.test.ActivityInstrumentationTestCase}, which is deprecated.
* New tests should be written using this base class.
*
* <p>If you prefer an isolated unit test, see {@link android.test.ActivityUnitTestCase}.
*
* @deprecated Use
* <a href="{@docRoot}reference/android/support/test/rule/ActivityTestRule.html">
* ActivityTestRule</a> instead. New tests should be written using the
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
*/
@Deprecated
public abstract class ActivityInstrumentationTestCase2<T extends Activity>
extends ActivityTestCase {
Class<T> mActivityClass;
boolean mInitialTouchMode = false;
Intent mActivityIntent = null;
/**
* Creates an {@link ActivityInstrumentationTestCase2}.
*
* @param pkg ignored - no longer in use.
* @param activityClass The activity to test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
*
* @deprecated use {@link #ActivityInstrumentationTestCase2(Class)} instead
*/
@Deprecated
public ActivityInstrumentationTestCase2(String pkg, Class<T> activityClass) {
this(activityClass);
}
/**
* Creates an {@link ActivityInstrumentationTestCase2}.
*
* @param activityClass The activity to test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
*/
public ActivityInstrumentationTestCase2(Class<T> activityClass) {
mActivityClass = activityClass;
}
/**
* Get the Activity under test, starting it if necessary.
*
* For each test method invocation, the Activity will not actually be created until the first
* time this method is called.
*
* <p>If you wish to provide custom setup values to your Activity, you may call
* {@link #setActivityIntent(Intent)} and/or {@link #setActivityInitialTouchMode(boolean)}
* before your first call to getActivity(). Calling them after your Activity has
* started will have no effect.
*
* <p><b>NOTE:</b> Activities under test may not be started from within the UI thread.
* If your test method is annotated with {@link android.test.UiThreadTest}, then your Activity
* will be started automatically just before your test method is run. You still call this
* method in order to get the Activity under test.
*
* @return the Activity under test
*/
@Override
public T getActivity() {
Activity a = super.getActivity();
if (a == null) {
// set initial touch mode
getInstrumentation().setInTouchMode(mInitialTouchMode);
final String targetPackage = getInstrumentation().getTargetContext().getPackageName();
// inject custom intent, if provided
if (mActivityIntent == null) {
a = launchActivity(targetPackage, mActivityClass, null);
} else {
a = launchActivityWithIntent(targetPackage, mActivityClass, mActivityIntent);
}
setActivity(a);
}
return (T) a;
}
/**
* Call this method before the first call to {@link #getActivity} to inject a customized Intent
* into the Activity under test.
*
* <p>If you do not call this, the default intent will be provided. If you call this after
* your Activity has been started, it will have no effect.
*
* <p><b>NOTE:</b> Activities under test may not be started from within the UI thread.
* If your test method is annotated with {@link android.test.UiThreadTest}, then you must call
* {@link #setActivityIntent(Intent)} from {@link #setUp()}.
*
* <p>The default Intent (if this method is not called) is:
* action = {@link Intent#ACTION_MAIN}
* flags = {@link Intent#FLAG_ACTIVITY_NEW_TASK}
* All other fields are null or empty.
*
* @param i The Intent to start the Activity with, or null to reset to the default Intent.
*/
public void setActivityIntent(Intent i) {
mActivityIntent = i;
}
/**
* Call this method before the first call to {@link #getActivity} to set the initial touch
* mode for the Activity under test.
*
* <p>If you do not call this, the touch mode will be false. If you call this after
* your Activity has been started, it will have no effect.
*
* <p><b>NOTE:</b> Activities under test may not be started from within the UI thread.
* If your test method is annotated with {@link android.test.UiThreadTest}, then you must call
* {@link #setActivityInitialTouchMode(boolean)} from {@link #setUp()}.
*
* @param initialTouchMode true if the Activity should be placed into "touch mode" when started
*/
public void setActivityInitialTouchMode(boolean initialTouchMode) {
mInitialTouchMode = initialTouchMode;
}
@Override
protected void setUp() throws Exception {
super.setUp();
mInitialTouchMode = false;
mActivityIntent = null;
}
@Override
protected void tearDown() throws Exception {
// Finish the Activity off (unless was never launched anyway)
Activity a = super.getActivity();
if (a != null) {
a.finish();
setActivity(null);
}
// Scrub out members - protects against memory leaks in the case where someone
// creates a non-static inner class (thus referencing the test case) and gives it to
// someone else to hold onto
scrubClass(ActivityInstrumentationTestCase2.class);
super.tearDown();
}
/**
* Runs the current unit test. If the unit test is annotated with
* {@link android.test.UiThreadTest}, force the Activity to be created before switching to
* the UI thread.
*/
@Override
protected void runTest() throws Throwable {
try {
Method method = getClass().getMethod(getName(), (Class[]) null);
if (method.isAnnotationPresent(UiThreadTest.class)) {
getActivity();
}
} catch (Exception e) {
// eat the exception here; super.runTest() will catch it again and handle it properly
}
super.runTest();
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.app.Activity;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
/**
* This is common code used to support Activity test cases. For more useful classes, please see
* {@link android.test.ActivityUnitTestCase} and
* {@link android.test.ActivityInstrumentationTestCase}.
*
* @deprecated New tests should be written using the
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
*/
@Deprecated
public abstract class ActivityTestCase extends InstrumentationTestCase {
/**
* The activity that will be set up for use in each test method.
*/
private Activity mActivity;
/**
* @return Returns the activity under test.
*/
protected Activity getActivity() {
return mActivity;
}
/**
* Set the activity under test.
* @param testActivity The activity under test
*/
protected void setActivity(Activity testActivity) {
mActivity = testActivity;
}
/**
* This function is called by various TestCase implementations, at tearDown() time, in order
* to scrub out any class variables. This protects against memory leaks in the case where a
* test case creates a non-static inner class (thus referencing the test case) and gives it to
* someone else to hold onto.
*
* @param testCaseClass The class of the derived TestCase implementation.
*
* @throws IllegalAccessException
*/
protected void scrubClass(final Class<?> testCaseClass)
throws IllegalAccessException {
final Field[] fields = getClass().getDeclaredFields();
for (Field field : fields) {
final Class<?> fieldClass = field.getDeclaringClass();
if (testCaseClass.isAssignableFrom(fieldClass) && !field.getType().isPrimitive()
&& (field.getModifiers() & Modifier.FINAL) == 0) {
try {
field.setAccessible(true);
field.set(this, null);
} catch (Exception e) {
android.util.Log.d("TestCase", "Error: Could not nullify field!");
}
if (field.get(this) != null) {
android.util.Log.d("TestCase", "Error: Could not nullify field!");
}
}
}
}
}

View File

@@ -0,0 +1,348 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.os.IBinder;
//import android.test.mock.MockApplication;
import android.view.Window;
import android.util.Log;
/**
* This class provides isolated testing of a single activity. The activity under test will
* be created with minimal connection to the system infrastructure, and you can inject mocked or
* wrappered versions of many of Activity's dependencies. Most of the work is handled
* automatically here by {@link #setUp} and {@link #tearDown}.
*
* <p>If you prefer a functional test, see {@link android.test.ActivityInstrumentationTestCase}.
*
* <p>It must be noted that, as a true unit test, your Activity will not be running in the
* normal system and will not participate in the normal interactions with other Activities.
* The following methods should not be called in this configuration - most of them will throw
* exceptions:
* <ul>
* <li>{@link android.app.Activity#createPendingResult(int, Intent, int)}</li>
* <li>{@link android.app.Activity#startActivityIfNeeded(Intent, int)}</li>
* <li>{@link android.app.Activity#startActivityFromChild(Activity, Intent, int)}</li>
* <li>{@link android.app.Activity#startNextMatchingActivity(Intent)}</li>
* <li>{@link android.app.Activity#getCallingActivity()}</li>
* <li>{@link android.app.Activity#getCallingPackage()}</li>
* <li>{@link android.app.Activity#createPendingResult(int, Intent, int)}</li>
* <li>{@link android.app.Activity#getTaskId()}</li>
* <li>{@link android.app.Activity#isTaskRoot()}</li>
* <li>{@link android.app.Activity#moveTaskToBack(boolean)}</li>
* </ul>
*
* <p>The following methods may be called but will not do anything. For test purposes, you can use
* the methods {@link #getStartedActivityIntent()} and {@link #getStartedActivityRequest()} to
* inspect the parameters that they were called with.
* <ul>
* <li>{@link android.app.Activity#startActivity(Intent)}</li>
* <li>{@link android.app.Activity#startActivityForResult(Intent, int)}</li>
* </ul>
*
* <p>The following methods may be called but will not do anything. For test purposes, you can use
* the methods {@link #isFinishCalled()} and {@link #getFinishedActivityRequest()} to inspect the
* parameters that they were called with.
* <ul>
* <li>{@link android.app.Activity#finish()}</li>
* <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
* <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
* </ul>
*
* @deprecated Write
* <a href="{@docRoot}training/testing/unit-testing/local-unit-tests.html">Local Unit Tests</a>
* instead.
*/
@Deprecated
public abstract class ActivityUnitTestCase<T extends Activity>
extends ActivityTestCase {
private static final String TAG = "ActivityUnitTestCase";
private Class<T> mActivityClass;
private Context mActivityContext;
private Application mApplication;
private MockParent mMockParent;
private boolean mAttached = false;
private boolean mCreated = false;
public ActivityUnitTestCase(Class<T> activityClass) {
mActivityClass = activityClass;
}
@Override
public T getActivity() {
return (T) super.getActivity();
}
@Override
protected void setUp() throws Exception {
super.setUp();
// default value for target context, as a default
mActivityContext = getInstrumentation().getTargetContext();
}
/**
* Start the activity under test, in the same way as if it was started by
* {@link android.content.Context#startActivity Context.startActivity()}, providing the
* arguments it supplied. When you use this method to start the activity, it will automatically
* be stopped by {@link #tearDown}.
*
* <p>This method will call onCreate(), but if you wish to further exercise Activity life
* cycle methods, you must call them yourself from your test case.
*
* <p><i>Do not call from your setUp() method. You must call this method from each of your
* test methods.</i>
*
* @param intent The Intent as if supplied to {@link android.content.Context#startActivity}.
* @param savedInstanceState The instance state, if you are simulating this part of the life
* cycle. Typically null.
* @param lastNonConfigurationInstance This Object will be available to the
* Activity if it calls {@link android.app.Activity#getLastNonConfigurationInstance()}.
* Typically null.
* @return Returns the Activity that was created
*/
protected T startActivity(Intent intent, Bundle savedInstanceState,
Object lastNonConfigurationInstance) {
assertFalse("Activity already created", mCreated);
if (!mAttached) {
assertNotNull(mActivityClass);
setActivity(null);
T newActivity = null;
try {
IBinder token = null;
if (mApplication == null) {
setApplication(Context.this_application/*new MockApplication()*/);
}
ComponentName cn = new ComponentName(mActivityClass.getPackage().getName(),
mActivityClass.getName());
intent.setComponent(cn);
ActivityInfo info = new ActivityInfo();
CharSequence title = mActivityClass.getName();
mMockParent = new MockParent();
String id = null;
newActivity = (T) getInstrumentation().newActivity(mActivityClass, mActivityContext,
token, mApplication, intent, info, title, mMockParent, id,
lastNonConfigurationInstance);
} catch (Exception e) {
Log.w(TAG, "Catching exception", e);
assertNotNull(newActivity);
}
assertNotNull(newActivity);
setActivity(newActivity);
mAttached = true;
}
T result = getActivity();
if (result != null) {
getInstrumentation().callActivityOnCreate(getActivity(), savedInstanceState);
mCreated = true;
}
return result;
}
@Override
protected void tearDown() throws Exception {
setActivity(null);
// Scrub out members - protects against memory leaks in the case where someone
// creates a non-static inner class (thus referencing the test case) and gives it to
// someone else to hold onto
scrubClass(ActivityInstrumentationTestCase.class);
super.tearDown();
}
/**
* Set the application for use during the test. You must call this function before calling
* {@link #startActivity}. If your test does not call this method,
* @param application The Application object that will be injected into the Activity under test.
*/
public void setApplication(Application application) {
mApplication = application;
}
/**
* If you wish to inject a Mock, Isolated, or otherwise altered context, you can do so
* here. You must call this function before calling {@link #startActivity}. If you wish to
* obtain a real Context, as a building block, use getInstrumentation().getTargetContext().
*/
public void setActivityContext(Context activityContext) {
mActivityContext = activityContext;
}
/**
* This method will return the value if your Activity under test calls
* {@link android.app.Activity#setRequestedOrientation}.
*/
public int getRequestedOrientation() {
if (mMockParent != null) {
return mMockParent.mRequestedOrientation;
}
return 0;
}
/**
* This method will return the launch intent if your Activity under test calls
* {@link android.app.Activity#startActivity(Intent)} or
* {@link android.app.Activity#startActivityForResult(Intent, int)}.
* @return The Intent provided in the start call, or null if no start call was made.
*/
public Intent getStartedActivityIntent() {
if (mMockParent != null) {
return mMockParent.mStartedActivityIntent;
}
return null;
}
/**
* This method will return the launch request code if your Activity under test calls
* {@link android.app.Activity#startActivityForResult(Intent, int)}.
* @return The request code provided in the start call, or -1 if no start call was made.
*/
public int getStartedActivityRequest() {
if (mMockParent != null) {
return mMockParent.mStartedActivityRequest;
}
return 0;
}
/**
* This method will notify you if the Activity under test called
* {@link android.app.Activity#finish()},
* {@link android.app.Activity#finishFromChild(Activity)}, or
* {@link android.app.Activity#finishActivity(int)}.
* @return Returns true if one of the listed finish methods was called.
*/
public boolean isFinishCalled() {
if (mMockParent != null) {
return mMockParent.mFinished;
}
return false;
}
/**
* This method will return the request code if the Activity under test called
* {@link android.app.Activity#finishActivity(int)}.
* @return The request code provided in the start call, or -1 if no finish call was made.
*/
public int getFinishedActivityRequest() {
if (mMockParent != null) {
return mMockParent.mFinishedActivityRequest;
}
return 0;
}
/**
* This mock Activity represents the "parent" activity. By injecting this, we allow the user
* to call a few more Activity methods, including:
* <ul>
* <li>{@link android.app.Activity#getRequestedOrientation()}</li>
* <li>{@link android.app.Activity#setRequestedOrientation(int)}</li>
* <li>{@link android.app.Activity#finish()}</li>
* <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
* <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
* </ul>
*
* TODO: Make this overrideable, and the unit test can look for calls to other methods
*/
private static class MockParent extends Activity {
public int mRequestedOrientation = 0;
public Intent mStartedActivityIntent = null;
public int mStartedActivityRequest = -1;
public boolean mFinished = false;
public int mFinishedActivityRequest = -1;
/**
* Implementing in the parent allows the user to call this function on the tested activity.
*/
@Override
public void setRequestedOrientation(int requestedOrientation) {
mRequestedOrientation = requestedOrientation;
}
/**
* Implementing in the parent allows the user to call this function on the tested activity.
*/
@Override
public int getRequestedOrientation() {
return mRequestedOrientation;
}
/**
* By returning null here, we inhibit the creation of any "container" for the window.
*/
@Override
public Window getWindow() {
return null;
}
/**
* By defining this in the parent, we allow the tested activity to call
* <ul>
* <li>{@link android.app.Activity#startActivity(Intent)}</li>
* <li>{@link android.app.Activity#startActivityForResult(Intent, int)}</li>
* </ul>
*/
//@Override
public void startActivityFromChild(Activity child, Intent intent, int requestCode) {
mStartedActivityIntent = intent;
mStartedActivityRequest = requestCode;
}
/**
* By defining this in the parent, we allow the tested activity to call
* <ul>
* <li>{@link android.app.Activity#finish()}</li>
* <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
* </ul>
*/
//@Override
public void finishFromChild(Activity child) {
mFinished = true;
}
/**
* By defining this in the parent, we allow the tested activity to call
* <ul>
* <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
* </ul>
*/
//@Override
public void finishActivityFromChild(Activity child, int requestCode) {
mFinished = true;
mFinishedActivityRequest = requestCode;
}
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.test.suitebuilder.annotation.Suppress;
import junit.framework.TestCase;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
/**
* Extend this if you need to access Resources or other things that depend on Activity Context.
*
* @deprecated Use
* <a href="{@docRoot}reference/android/support/test/InstrumentationRegistry.html">
* InstrumentationRegistry</a> instead. New tests should be written using the
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
*/
@Deprecated
public class AndroidTestCase extends TestCase {
protected Context mContext;
private Context mTestContext;
@Override
protected void setUp() throws Exception {
super.setUp();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
@Suppress
public void testAndroidTestCaseSetupProperly() {
assertNotNull("Context is null. setContext should be called before tests are run",
mContext);
}
public void setContext(Context context) {
mContext = context;
}
public Context getContext() {
return mContext;
}
/**
* Test context can be used to access resources from the test's own package
* as opposed to the resources from the test target package. Access to the
* latter is provided by the context set with the {@link #setContext}
* method.
*
*/
public void setTestContext(Context context) {
mTestContext = context;
}
/**
* Returns the test context that was set via {@link #setTestContext(Context)}.
*/
public Context getTestContext() {
return mTestContext;
}
/**
* Asserts that launching a given activity is protected by a particular permission by
* attempting to start the activity and validating that a {@link SecurityException}
* is thrown that mentions the permission in its error message.
*
* Note that an instrumentation isn't needed because all we are looking for is a security error
* and we don't need to wait for the activity to launch and get a handle to the activity.
*
* @param packageName The package name of the activity to launch.
* @param className The class of the activity to launch.
* @param permission The name of the permission.
*/
public void assertActivityRequiresPermission(
String packageName, String className, String permission) {
final Intent intent = new Intent();
intent.setClassName(packageName, className);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
getContext().startActivity(intent);
fail("expected security exception for " + permission);
} catch (SecurityException expected) {
assertNotNull("security exception's error message.", expected.getMessage());
assertTrue("error message should contain " + permission + ".",
expected.getMessage().contains(permission));
}
}
/**
* Asserts that reading from the content uri requires a particular permission by querying the
* uri and ensuring a {@link SecurityException} is thrown mentioning the particular permission.
*
* @param uri The uri that requires a permission to query.
* @param permission The permission that should be required.
*/
public void assertReadingContentUriRequiresPermission(Uri uri, String permission) {
try {
getContext().getContentResolver().query(uri, null, null, null, null);
fail("expected SecurityException requiring " + permission);
} catch (SecurityException expected) {
assertNotNull("security exception's error message.", expected.getMessage());
assertTrue("error message should contain " + permission + ".",
expected.getMessage().contains(permission));
}
}
/**
* Asserts that writing to the content uri requires a particular permission by inserting into
* the uri and ensuring a {@link SecurityException} is thrown mentioning the particular
* permission.
*
* @param uri The uri that requires a permission to query.
* @param permission The permission that should be required.
*/
public void assertWritingContentUriRequiresPermission(Uri uri, String permission) {
try {
getContext().getContentResolver().insert(uri, new ContentValues());
fail("expected SecurityException requiring " + permission);
} catch (SecurityException expected) {
assertNotNull("security exception's error message.", expected.getMessage());
assertTrue("error message should contain \"" + permission + "\". Got: \""
+ expected.getMessage() + "\".",
expected.getMessage().contains(permission));
}
}
/**
* This function is called by various TestCase implementations, at tearDown() time, in order
* to scrub out any class variables. This protects against memory leaks in the case where a
* test case creates a non-static inner class (thus referencing the test case) and gives it to
* someone else to hold onto.
*
* @param testCaseClass The class of the derived TestCase implementation.
*
* @throws IllegalAccessException
*/
protected void scrubClass(final Class<?> testCaseClass)
throws IllegalAccessException {
final Field[] fields = getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.getType().isPrimitive() &&
!Modifier.isStatic(field.getModifiers())) {
try {
field.setAccessible(true);
field.set(this, null);
} catch (Exception e) {
android.util.Log.d("TestCase", "Error: Could not nullify field!");
}
if (field.get(this) != null) {
android.util.Log.d("TestCase", "Error: Could not nullify field!");
}
}
}
}
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.app.Instrumentation;
import android.content.Context;
import java.util.ArrayList;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestListener;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import junit.runner.BaseTestRunner;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
/**
* @deprecated Use
* <a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">
* AndroidJUnitRunner</a> instead. New tests should be written using the
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
*/
@Deprecated
public class AndroidTestRunner extends BaseTestRunner {
private TestResult mTestResult;
private String mTestClassName;
private List<TestCase> mTestCases;
private Context mContext;
private boolean mSkipExecution = false;
private List<TestListener> mTestListeners = new ArrayList<>();
private Instrumentation mInstrumentation;
@SuppressWarnings("unchecked")
public void setTestClassName(String testClassName, String testMethodName) {
Class testClass = loadTestClass(testClassName);
if (shouldRunSingleTestMethod(testMethodName, testClass)) {
TestCase testCase = buildSingleTestMethod(testClass, testMethodName);
mTestCases = new ArrayList<>();
mTestCases.add(testCase);
mTestClassName = testClass.getSimpleName();
} else {
setTest(getTest(testClass), testClass);
}
}
public void setTest(Test test) {
setTest(test, test.getClass());
}
private void setTest(Test test, Class<? extends Test> testClass) {
mTestCases = (List<TestCase>) TestCaseUtil.getTests(test, true);
if (TestSuite.class.isAssignableFrom(testClass)) {
mTestClassName = TestCaseUtil.getTestName(test);
} else {
mTestClassName = testClass.getSimpleName();
}
}
public void clearTestListeners() {
mTestListeners.clear();
}
public void addTestListener(TestListener testListener) {
if (testListener != null) {
mTestListeners.add(testListener);
}
}
@SuppressWarnings("unchecked")
private Class<? extends Test> loadTestClass(String testClassName) {
try {
return (Class<? extends Test>) mContext.getClassLoader().loadClass(testClassName);
} catch (ClassNotFoundException e) {
runFailed("Could not find test class. Class: " + testClassName, e);
}
return null;
}
private TestCase buildSingleTestMethod(Class testClass, String testMethodName) {
try {
Constructor c = testClass.getConstructor();
return newSingleTestMethod(testClass, testMethodName, c);
} catch (NoSuchMethodException e) {
}
try {
Constructor c = testClass.getConstructor(String.class);
return newSingleTestMethod(testClass, testMethodName, c, testMethodName);
} catch (NoSuchMethodException e) {
}
return null;
}
private TestCase newSingleTestMethod(Class testClass, String testMethodName,
Constructor constructor, Object... args) {
try {
TestCase testCase = (TestCase) constructor.newInstance(args);
testCase.setName(testMethodName);
return testCase;
} catch (IllegalAccessException e) {
runFailed("Could not access test class. Class: " + testClass.getName(), e);
} catch (InstantiationException e) {
runFailed("Could not instantiate test class. Class: " + testClass.getName(), e);
} catch (IllegalArgumentException e) {
runFailed("Illegal argument passed to constructor. Class: " + testClass.getName(), e);
} catch (InvocationTargetException e) {
runFailed("Constructor threw an exception. Class: " + testClass.getName(), e);
}
return null;
}
private boolean shouldRunSingleTestMethod(String testMethodName,
Class<? extends Test> testClass) {
return testMethodName != null && TestCase.class.isAssignableFrom(testClass);
}
private Test getTest(Class clazz) {
if (TestSuiteProvider.class.isAssignableFrom(clazz)) {
try {
TestSuiteProvider testSuiteProvider =
(TestSuiteProvider) clazz.getConstructor().newInstance();
return testSuiteProvider.getTestSuite();
} catch (InstantiationException e) {
runFailed("Could not instantiate test suite provider. Class: " + clazz.getName(), e);
} catch (IllegalAccessException e) {
runFailed("Illegal access of test suite provider. Class: " + clazz.getName(), e);
} catch (InvocationTargetException e) {
runFailed("Invocation exception test suite provider. Class: " + clazz.getName(), e);
} catch (NoSuchMethodException e) {
runFailed("No such method on test suite provider. Class: " + clazz.getName(), e);
}
}
return getTest(clazz.getName());
}
protected TestResult createTestResult() {
if (mSkipExecution) {
return new NoExecTestResult();
}
return new TestResult();
}
void setSkipExecution(boolean skip) {
mSkipExecution = skip;
}
public List<TestCase> getTestCases() {
return mTestCases;
}
public String getTestClassName() {
return mTestClassName;
}
public TestResult getTestResult() {
return mTestResult;
}
public void runTest() {
runTest(createTestResult());
}
public void runTest(TestResult testResult) {
mTestResult = testResult;
for (TestListener testListener : mTestListeners) {
mTestResult.addListener(testListener);
}
Context testContext = mInstrumentation == null ? mContext : mInstrumentation.getContext();
for (TestCase testCase : mTestCases) {
setContextIfAndroidTestCase(testCase, mContext, testContext);
setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);
testCase.run(mTestResult);
}
}
private void setContextIfAndroidTestCase(Test test, Context context, Context testContext) {
if (AndroidTestCase.class.isAssignableFrom(test.getClass())) {
((AndroidTestCase) test).setContext(context);
((AndroidTestCase) test).setTestContext(testContext);
}
}
public void setContext(Context context) {
mContext = context;
}
private void setInstrumentationIfInstrumentationTestCase(
Test test, Instrumentation instrumentation) {
if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) {
((InstrumentationTestCase) test).injectInstrumentation(instrumentation);
}
}
public void setInstrumentation(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
}
/**
* @deprecated Incorrect spelling,
* use {@link #setInstrumentation(android.app.Instrumentation)} instead.
*/
@Deprecated
public void setInstrumentaiton(Instrumentation instrumentation) {
setInstrumentation(instrumentation);
}
@Override
protected Class loadSuiteClass(String suiteClassName) throws ClassNotFoundException {
return mContext.getClassLoader().loadClass(suiteClassName);
}
public void testStarted(String testName) {
}
public void testEnded(String testName) {
}
public void testFailed(int status, Test test, Throwable t) {
}
protected void runFailed(String message) {
throw new RuntimeException(message);
}
protected void runFailed(String message, Throwable cause) {
throw new RuntimeException(message, cause);
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
/**
* This test case provides a framework in which you can test Application classes in
* a controlled environment. It provides basic support for the lifecycle of a
* Application, and hooks by which you can inject various dependencies and control
* the environment in which your Application is tested.
*
* <p><b>Lifecycle Support.</b>
* Every Application is designed to be accessed within a specific sequence of
* method calls (see {@link android.app.Application} for more details).
* In order to support the lifecycle of a Application, this test case will make the
* following calls at the following times.
*
* <ul><li>The test case will not call onCreate() until your test calls
* {@link #createApplication()}. This gives you a chance
* to set up or adjust any additional framework or test logic before
* onCreate().</li>
* <li>After your test completes, the test case {@link #tearDown} method is
* automatically called, and it will stop & destroy your application by calling its
* onDestroy() method.</li>
* </ul>
*
* <p><b>Dependency Injection.</b>
* Every Application has one inherent dependency, the {@link android.content.Context Context} in
* which it runs.
* This framework allows you to inject a modified, mock, or isolated replacement for this
* dependencies, and thus perform a true unit test.
*
* <p>If simply run your tests as-is, your Application will be injected with a fully-functional
* Context.
* You can create and inject alternative types of Contexts by calling
* {@link AndroidTestCase#setContext(Context) setContext()}. You must do this <i>before</i> calling
* {@link #createApplication()}. The test framework provides a
* number of alternatives for Context, including {@link android.test.mock.MockContext MockContext},
* {@link android.test.RenamingDelegatingContext RenamingDelegatingContext}, and
* {@link android.content.ContextWrapper ContextWrapper}.
*
* @deprecated Use
* <a href="{@docRoot}reference/android/support/test/InstrumentationRegistry.html">
* InstrumentationRegistry</a> instead. New tests should be written using the
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
*/
@Deprecated
public abstract class ApplicationTestCase<T extends Application> extends AndroidTestCase {
Class<T> mApplicationClass;
private Context mSystemContext;
public ApplicationTestCase(Class<T> applicationClass) {
mApplicationClass = applicationClass;
}
private T mApplication;
private boolean mAttached = false;
private boolean mCreated = false;
/**
* @return Returns the actual Application under test.
*/
public T getApplication() {
return mApplication;
}
/**
* This will do the work to instantiate the Application under test. After this, your test
* code must also start and stop the Application.
*/
@Override
protected void setUp() throws Exception {
super.setUp();
// get the real context, before the individual tests have a chance to muck with it
mSystemContext = getContext();
}
/**
* Load and attach the application under test.
*/
private void setupApplication() {
mApplication = null;
try {
mApplication = (T) Instrumentation.newApplication(mApplicationClass, getContext());
} catch (Exception e) {
assertNotNull(mApplication);
}
mAttached = true;
}
/**
* Start the Application under test, in the same way as if it was started by the system.
* If you use this method to start the Application, it will automatically
* be stopped by {@link #tearDown}. If you wish to inject a specialized Context for your
* test, by calling {@link AndroidTestCase#setContext(Context) setContext()},
* you must do so before calling this method.
*/
final protected void createApplication() {
assertFalse(mCreated);
if (!mAttached) {
setupApplication();
}
assertNotNull(mApplication);
mApplication.onCreate();
mCreated = true;
}
/**
* This will make the necessary calls to terminate the Application under test (it will
* call onTerminate(). Ordinarily this will be called automatically (by {@link #tearDown}, but
* you can call it directly from your test in order to check for proper shutdown behaviors.
*/
final protected void terminateApplication() {
if (mCreated) {
mApplication.onTerminate();
}
}
/**
* Shuts down the Application under test. Also makes sure all resources are cleaned up and
* garbage collected before moving on to the next
* test. Subclasses that override this method should make sure they call super.tearDown()
* at the end of the overriding method.
*
* @throws Exception
*/
@Override
protected void tearDown() throws Exception {
terminateApplication();
mApplication = null;
// Scrub out members - protects against memory leaks in the case where someone
// creates a non-static inner class (thus referencing the test case) and gives it to
// someone else to hold onto
scrubClass(ApplicationTestCase.class);
super.tearDown();
}
/**
* Return a real (not mocked or instrumented) system Context that can be used when generating
* Mock or other Context objects for your Application under test.
*
* @return Returns a reference to a normal Context.
*/
public Context getSystemContext() {
return mSystemContext;
}
/**
* This test simply confirms that the Application class can be instantiated properly.
*
* @throws Exception
*/
final public void testApplicationTestCaseSetUpProperly() throws Exception {
setupApplication();
assertNotNull("Application class could not be instantiated successfully", mApplication);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
/**
* Thrown when an assertion failed.
*
* @deprecated use junit.framework.AssertionFailedError
*/
@Deprecated
public class AssertionFailedError extends Error {
/**
* It is more typical to call {@link #AssertionFailedError(String)}.
*/
public AssertionFailedError() {
}
public AssertionFailedError(String errorMessage) {
super(errorMessage);
}
}

View File

@@ -0,0 +1,262 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.util.Log;
import dalvik.system.DexFile;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
/**
* Generate {@link ClassPathPackageInfo}s by scanning apk paths.
*
* {@hide} Not needed for 1.0 SDK.
*/
@Deprecated
public class ClassPathPackageInfoSource {
private static final ClassLoader CLASS_LOADER
= ClassPathPackageInfoSource.class.getClassLoader();
private static String[] apkPaths;
private static ClassPathPackageInfoSource classPathSource;
private final SimpleCache<String, ClassPathPackageInfo> cache =
new SimpleCache<String, ClassPathPackageInfo>() {
@Override
protected ClassPathPackageInfo load(String pkgName) {
return createPackageInfo(pkgName);
}
};
// The class path of the running application
private final String[] classPath;
private final ClassLoader classLoader;
private ClassPathPackageInfoSource(ClassLoader classLoader) {
this.classLoader = classLoader;
classPath = getClassPath();
}
static void setApkPaths(String[] apkPaths) {
ClassPathPackageInfoSource.apkPaths = apkPaths;
}
public static ClassPathPackageInfoSource forClassPath(ClassLoader classLoader) {
if (classPathSource == null) {
classPathSource = new ClassPathPackageInfoSource(classLoader);
}
return classPathSource;
}
public Set<Class<?>> getTopLevelClassesRecursive(String packageName) {
ClassPathPackageInfo packageInfo = cache.get(packageName);
return packageInfo.getTopLevelClassesRecursive();
}
private ClassPathPackageInfo createPackageInfo(String packageName) {
Set<String> subpackageNames = new TreeSet<String>();
Set<String> classNames = new TreeSet<String>();
Set<Class<?>> topLevelClasses = new HashSet<>();
findClasses(packageName, classNames, subpackageNames);
for (String className : classNames) {
if (className.endsWith(".R") || className.endsWith(".Manifest")) {
// Don't try to load classes that are generated. They usually aren't in test apks.
continue;
}
try {
// We get errors in the emulator if we don't use the caller's class loader.
topLevelClasses.add(Class.forName(className, false,
(classLoader != null) ? classLoader : CLASS_LOADER));
} catch (ClassNotFoundException | NoClassDefFoundError e) {
// Should not happen unless there is a generated class that is not included in
// the .apk.
Log.w("ClassPathPackageInfoSource", "Cannot load class. "
+ "Make sure it is in your apk. Class name: '" + className
+ "'. Message: " + e.getMessage(), e);
}
}
return new ClassPathPackageInfo(packageName, subpackageNames,
topLevelClasses);
}
/**
* Finds all classes and sub packages that are below the packageName and
* add them to the respective sets. Searches the package on the whole class
* path.
*/
private void findClasses(String packageName, Set<String> classNames,
Set<String> subpackageNames) {
for (String entryName : classPath) {
File classPathEntry = new File(entryName);
// Forge may not have brought over every item in the classpath. Be
// polite and ignore missing entries.
if (classPathEntry.exists()) {
try {
if (entryName.endsWith(".apk")) {
findClassesInApk(entryName, packageName, classNames, subpackageNames);
} else {
// scan the directories that contain apk files.
for (String apkPath : apkPaths) {
File file = new File(apkPath);
scanForApkFiles(file, packageName, classNames, subpackageNames);
}
}
} catch (IOException e) {
throw new AssertionError("Can't read classpath entry " +
entryName + ": " + e.getMessage());
}
}
}
}
private void scanForApkFiles(File source, String packageName,
Set<String> classNames, Set<String> subpackageNames) throws IOException {
if (source.getPath().endsWith(".apk")) {
findClassesInApk(source.getPath(), packageName, classNames, subpackageNames);
} else {
File[] files = source.listFiles();
if (files != null) {
for (File file : files) {
scanForApkFiles(file, packageName, classNames, subpackageNames);
}
}
}
}
/**
* Finds all classes and sub packages that are below the packageName and
* add them to the respective sets. Searches the package in a single apk file.
*/
private void findClassesInApk(String apkPath, String packageName,
Set<String> classNames, Set<String> subpackageNames)
throws IOException {
DexFile dexFile = null;
try {
dexFile = new DexFile(apkPath);
Enumeration<String> apkClassNames = dexFile.entries();
while (apkClassNames.hasMoreElements()) {
String className = apkClassNames.nextElement();
if (className.startsWith(packageName)) {
String subPackageName = packageName;
int lastPackageSeparator = className.lastIndexOf('.');
if (lastPackageSeparator > 0) {
subPackageName = className.substring(0, lastPackageSeparator);
}
if (subPackageName.length() > packageName.length()) {
subpackageNames.add(subPackageName);
} else if (isToplevelClass(className)) {
classNames.add(className);
}
}
}
} catch (IOException e) {
if (false) {
Log.w("ClassPathPackageInfoSource",
"Error finding classes at apk path: " + apkPath, e);
}
} finally {
if (dexFile != null) {
// Todo: figure out why closing causes a dalvik error resulting in vm shutdown.
// dexFile.close();
}
}
}
/**
* Checks if a given file name represents a toplevel class.
*/
private static boolean isToplevelClass(String fileName) {
return fileName.indexOf('$') < 0;
}
/**
* Gets the class path from the System Property "java.class.path" and splits
* it up into the individual elements.
*/
private static String[] getClassPath() {
String classPath = System.getProperty("java.class.path");
String separator = System.getProperty("path.separator", ":");
return classPath.split(Pattern.quote(separator));
}
/**
* The Package object doesn't allow you to iterate over the contained
* classes and subpackages of that package. This is a version that does.
*/
private class ClassPathPackageInfo {
private final String packageName;
private final Set<String> subpackageNames;
private final Set<Class<?>> topLevelClasses;
private ClassPathPackageInfo(String packageName,
Set<String> subpackageNames, Set<Class<?>> topLevelClasses) {
this.packageName = packageName;
this.subpackageNames = Collections.unmodifiableSet(subpackageNames);
this.topLevelClasses = Collections.unmodifiableSet(topLevelClasses);
}
private Set<ClassPathPackageInfo> getSubpackages() {
Set<ClassPathPackageInfo> info = new HashSet<>();
for (String name : subpackageNames) {
info.add(cache.get(name));
}
return info;
}
private Set<Class<?>> getTopLevelClassesRecursive() {
Set<Class<?>> set = new HashSet<>();
addTopLevelClassesTo(set);
return set;
}
private void addTopLevelClassesTo(Set<Class<?>> set) {
set.addAll(topLevelClasses);
for (ClassPathPackageInfo info : getSubpackages()) {
info.addTopLevelClassesTo(set);
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ClassPathPackageInfo) {
ClassPathPackageInfo that = (ClassPathPackageInfo) obj;
return (this.packageName).equals(that.packageName);
}
return false;
}
@Override
public int hashCode() {
return packageName.hashCode();
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
/**
* Thrown when an assert equals for Strings failed.
*
* @deprecated use org.junit.ComparisonFailure
*/
@Deprecated
public class ComparisonFailure extends AssertionFailedError {
private junit.framework.ComparisonFailure mComparison;
public ComparisonFailure(String message, String expected, String actual) {
mComparison = new junit.framework.ComparisonFailure(message, expected, actual);
}
public String getMessage() {
return mComparison.getMessage();
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
/**
* This annotation can be used on an {@link android.test.InstrumentationTestCase}'s
* test methods. When the annotation is present, the test method is re-executed if
* the test fails. The total number of executions is specified by the tolerance and
* defaults to 1.
*
* @deprecated Use
* <a href="{@docRoot}reference/android/support/test/filters/FlakyTest.html">
* FlakyTest</a> instead. New tests should be written using the
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
*/
@Deprecated
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FlakyTest {
/**
* Indicates how many times a test can run and fail before being reported
* as a failed test. If the tolerance factor is less than 1, the test runs
* only once.
*
* @return The total number of allowed run, the default is 1.
*/
int tolerance() default 1;
}

View File

@@ -0,0 +1,369 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.test;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import junit.framework.TestCase;
/**
* A test case that has access to {@link Instrumentation}.
*
* @deprecated Use
* <a href="{@docRoot}reference/androidx/test/platform/app/InstrumentationRegistry.html">
* InstrumentationRegistry</a> instead. New tests should be written using the
* <a href="{@docRoot}training/testing/index.html">AndroidX Test Library</a>.
*/
@Deprecated
public class InstrumentationTestCase extends TestCase {
private Instrumentation mInstrumentation;
/**
* Injects instrumentation into this test case. This method is
* called by the test runner during test setup.
*
* @param instrumentation the instrumentation to use with this instance
*/
public void injectInstrumentation(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
}
/**
* Injects instrumentation into this test case. This method is
* called by the test runner during test setup.
*
* @param instrumentation the instrumentation to use with this instance
*
* @deprecated Incorrect spelling,
* use {@link #injectInstrumentation(android.app.Instrumentation)} instead.
*/
@Deprecated
public void injectInsrumentation(Instrumentation instrumentation) {
injectInstrumentation(instrumentation);
}
/**
* Inheritors can access the instrumentation using this.
* @return instrumentation
*/
public Instrumentation getInstrumentation() {
return mInstrumentation;
}
/**
* Utility method for launching an activity.
*
* <p>The {@link Intent} used to launch the Activity is:
* action = {@link Intent#ACTION_MAIN}
* extras = null, unless a custom bundle is provided here
* All other fields are null or empty.
*
* <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the
* package hosting the activity to be launched, which is specified in the AndroidManifest.xml
* file. This is not necessarily the same as the java package name.
*
* @param pkg The package hosting the activity to be launched.
* @param activityCls The activity class to launch.
* @param extras Optional extra stuff to pass to the activity.
* @return The activity, or null if non launched.
*/
public final <T extends Activity> T launchActivity(
String pkg,
Class<T> activityCls,
Bundle extras) {
Intent intent = new Intent(Intent.ACTION_MAIN);
if (extras != null) {
intent.putExtras(extras);
}
return launchActivityWithIntent(pkg, activityCls, intent);
}
/**
* Utility method for launching an activity with a specific Intent.
*
* <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the
* package hosting the activity to be launched, which is specified in the AndroidManifest.xml
* file. This is not necessarily the same as the java package name.
*
* @param pkg The package hosting the activity to be launched.
* @param activityCls The activity class to launch.
* @param intent The intent to launch with
* @return The activity, or null if non launched.
*/
@SuppressWarnings("unchecked")
public final <T extends Activity> T launchActivityWithIntent(
String pkg,
Class<T> activityCls,
Intent intent) {
intent.setClassName(pkg, activityCls.getName());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
T activity = (T) getInstrumentation().startActivitySync(intent);
getInstrumentation().waitForIdleSync();
return activity;
}
/**
* Helper for running portions of a test on the UI thread.
*
* Note, in most cases it is simpler to annotate the test method with
* {@link android.test.UiThreadTest}, which will run the entire test method on the UI thread.
* Use this method if you need to switch in and out of the UI thread to perform your test.
*
* @param r runnable containing test code in the {@link Runnable#run()} method
*/
public void runTestOnUiThread(final Runnable r) throws Throwable {
final Throwable[] exceptions = new Throwable[1];
getInstrumentation().runOnMainSync(new Runnable() {
public void run() {
try {
r.run();
} catch (Throwable throwable) {
exceptions[0] = throwable;
}
}
});
if (exceptions[0] != null) {
throw exceptions[0];
}
}
/**
* Runs the current unit test. If the unit test is annotated with
* {@link android.test.UiThreadTest}, the test is run on the UI thread.
*/
@Override
protected void runTest() throws Throwable {
String fName = getName();
assertNotNull(fName);
Method method = null;
try {
// use getMethod to get all public inherited
// methods. getDeclaredMethods returns all
// methods of this class but excludes the
// inherited ones.
method = getClass().getMethod(fName, (Class[]) null);
} catch (NoSuchMethodException e) {
fail("Method \""+fName+"\" not found");
}
if (!Modifier.isPublic(method.getModifiers())) {
fail("Method \""+fName+"\" should be public");
}
int runCount = 1;
boolean isRepetitive = false;
if (method.isAnnotationPresent(FlakyTest.class)) {
runCount = method.getAnnotation(FlakyTest.class).tolerance();
} else if (method.isAnnotationPresent(RepetitiveTest.class)) {
runCount = method.getAnnotation(RepetitiveTest.class).numIterations();
isRepetitive = true;
}
if (method.isAnnotationPresent(UiThreadTest.class)) {
final int tolerance = runCount;
final boolean repetitive = isRepetitive;
final Method testMethod = method;
final Throwable[] exceptions = new Throwable[1];
getInstrumentation().runOnMainSync(new Runnable() {
public void run() {
try {
runMethod(testMethod, tolerance, repetitive);
} catch (Throwable throwable) {
exceptions[0] = throwable;
}
}
});
if (exceptions[0] != null) {
throw exceptions[0];
}
} else {
runMethod(method, runCount, isRepetitive);
}
}
// For backwards-compatibility after adding isRepetitive
private void runMethod(Method runMethod, int tolerance) throws Throwable {
runMethod(runMethod, tolerance, false);
}
private void runMethod(Method runMethod, int tolerance, boolean isRepetitive) throws Throwable {
Throwable exception = null;
int runCount = 0;
do {
try {
runMethod.invoke(this, (Object[]) null);
exception = null;
} catch (InvocationTargetException e) {
e.fillInStackTrace();
exception = e.getTargetException();
} catch (IllegalAccessException e) {
e.fillInStackTrace();
exception = e;
} finally {
runCount++;
// Report current iteration number, if test is repetitive
if (isRepetitive) {
Bundle iterations = new Bundle();
iterations.putInt("currentiterations", runCount);
getInstrumentation().sendStatus(2, iterations);
}
}
} while ((runCount < tolerance) && (isRepetitive || exception != null));
if (exception != null) {
throw exception;
}
}
/**
* Sends a series of key events through instrumentation and waits for idle. The sequence
* of keys is a string containing the key names as specified in KeyEvent, without the
* KEYCODE_ prefix. For instance: sendKeys("DPAD_LEFT A B C DPAD_CENTER"). Each key can
* be repeated by using the N* prefix. For instance, to send two KEYCODE_DPAD_LEFT, use
* the following: sendKeys("2*DPAD_LEFT").
*
* @param keysSequence The sequence of keys.
*/
public void sendKeys(String keysSequence) {
final String[] keys = keysSequence.split(" ");
final int count = keys.length;
final Instrumentation instrumentation = getInstrumentation();
for (int i = 0; i < count; i++) {
String key = keys[i];
int repeater = key.indexOf('*');
int keyCount;
try {
keyCount = repeater == -1 ? 1 : Integer.parseInt(key.substring(0, repeater));
} catch (NumberFormatException e) {
Log.w("ActivityTestCase", "Invalid repeat count: " + key);
continue;
}
if (repeater != -1) {
key = key.substring(repeater + 1);
}
for (int j = 0; j < keyCount; j++) {
try {
final Field keyCodeField = KeyEvent.class.getField("KEYCODE_" + key);
final int keyCode = keyCodeField.getInt(null);
try {
instrumentation.sendKeyDownUpSync(keyCode);
} catch (SecurityException e) {
// Ignore security exceptions that are now thrown
// when trying to send to another app, to retain
// compatibility with existing tests.
}
} catch (NoSuchFieldException e) {
Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
break;
} catch (IllegalAccessException e) {
Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
break;
}
}
}
instrumentation.waitForIdleSync();
}
/**
* Sends a series of key events through instrumentation and waits for idle. For instance:
* sendKeys(KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER).
*
* @param keys The series of key codes to send through instrumentation.
*/
public void sendKeys(int... keys) {
final int count = keys.length;
final Instrumentation instrumentation = getInstrumentation();
for (int i = 0; i < count; i++) {
try {
instrumentation.sendKeyDownUpSync(keys[i]);
} catch (SecurityException e) {
// Ignore security exceptions that are now thrown
// when trying to send to another app, to retain
// compatibility with existing tests.
}
}
instrumentation.waitForIdleSync();
}
/**
* Sends a series of key events through instrumentation and waits for idle. Each key code
* must be preceded by the number of times the key code must be sent. For instance:
* sendRepeatedKeys(1, KEYCODE_DPAD_CENTER, 2, KEYCODE_DPAD_LEFT).
*
* @param keys The series of key repeats and codes to send through instrumentation.
*/
public void sendRepeatedKeys(int... keys) {
final int count = keys.length;
if ((count & 0x1) == 0x1) {
throw new IllegalArgumentException("The size of the keys array must "
+ "be a multiple of 2");
}
final Instrumentation instrumentation = getInstrumentation();
for (int i = 0; i < count; i += 2) {
final int keyCount = keys[i];
final int keyCode = keys[i + 1];
for (int j = 0; j < keyCount; j++) {
try {
instrumentation.sendKeyDownUpSync(keyCode);
} catch (SecurityException e) {
// Ignore security exceptions that are now thrown
// when trying to send to another app, to retain
// compatibility with existing tests.
}
}
}
instrumentation.waitForIdleSync();
}
/**
* Make sure all resources are cleaned up and garbage collected before moving on to the next
* test. Subclasses that override this method should make sure they call super.tearDown()
* at the end of the overriding method.
*
* @throws Exception
*/
@Override
protected void tearDown() throws Exception {
Runtime.getRuntime().gc();
Runtime.getRuntime().runFinalization();
Runtime.getRuntime().gc();
super.tearDown();
}
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More