diff --git a/pythonforandroid/bootstraps/service_only/__init__.py b/pythonforandroid/bootstraps/service_only/__init__.py new file mode 100644 index 0000000000..4a00e78c73 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/__init__.py @@ -0,0 +1,121 @@ +from pythonforandroid.toolchain import Bootstrap, shprint, current_directory, info, warning, ArchARM, info_main +from os.path import join, exists, curdir, abspath +from os import walk +import glob +import sh + +class ServiceOnlyBootstrap(Bootstrap): + name = 'service_only' + + recipe_depends = ['genericndkbuild', ('python2', 'python3crystax')] + + def run_distribute(self): + info_main('# Creating Android project from build and {} bootstrap'.format( + self.name)) + + info('This currently just copies the build stuff straight from the build dir.') + shprint(sh.rm, '-rf', self.dist_dir) + shprint(sh.cp, '-r', self.build_dir, self.dist_dir) + with current_directory(self.dist_dir): + with open('local.properties', 'w') as fileh: + fileh.write('sdk.dir={}'.format(self.ctx.sdk_dir)) + fileh.write('ndk.dir={}'.format(self.ctx.ndk_dir)) + + arch = self.ctx.archs[0] + if len(self.ctx.archs) > 1: + raise ValueError('built for more than one arch, but bootstrap cannot handle that yet') + info('Bootstrap running with arch {}'.format(arch)) + + with current_directory(self.dist_dir): + info('Copying python distribution') + + if not exists('private') and not self.ctx.python_recipe.from_crystax: + shprint(sh.mkdir, 'private') + if not exists('crystax_python') and self.ctx.python_recipe.from_crystax: + shprint(sh.mkdir, 'crystax_python') + shprint(sh.mkdir, 'crystax_python/crystax_python') + if not exists('assets'): + shprint(sh.mkdir, 'assets') + + hostpython = sh.Command(self.ctx.hostpython) + if not self.ctx.python_recipe.from_crystax: + try: + shprint(hostpython, '-OO', '-m', 'compileall', + self.ctx.get_python_install_dir(), + _tail=10, _filterout="^Listing") + except sh.ErrorReturnCode: + pass + if not exists('python-install'): + shprint(sh.cp, '-a', self.ctx.get_python_install_dir(), './python-install') + + self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) + self.distribute_aars(arch) + self.distribute_javaclasses(self.ctx.javaclass_dir) + + if not self.ctx.python_recipe.from_crystax: + info('Filling private directory') + if not exists(join('private', 'lib')): + info('private/lib does not exist, making') + shprint(sh.cp, '-a', join('python-install', 'lib'), 'private') + shprint(sh.mkdir, '-p', join('private', 'include', 'python2.7')) + + # AND: Copylibs stuff should go here + if exists(join('libs', arch.arch, 'libpymodules.so')): + shprint(sh.mv, join('libs', arch.arch, 'libpymodules.so'), 'private/') + shprint(sh.cp, join('python-install', 'include' , 'python2.7', 'pyconfig.h'), join('private', 'include', 'python2.7/')) + + info('Removing some unwanted files') + shprint(sh.rm, '-f', join('private', 'lib', 'libpython2.7.so')) + shprint(sh.rm, '-rf', join('private', 'lib', 'pkgconfig')) + + libdir = join(self.dist_dir, 'private', 'lib', 'python2.7') + site_packages_dir = join(libdir, 'site-packages') + with current_directory(libdir): + # shprint(sh.xargs, 'rm', sh.grep('-E', '*\.(py|pyx|so\.o|so\.a|so\.libs)$', sh.find('.'))) + removes = [] + for dirname, something, filens in walk('.'): + for filename in filens: + for suffix in ('py', 'pyc', 'so.o', 'so.a', 'so.libs'): + if filename.endswith(suffix): + removes.append(filename) + shprint(sh.rm, '-f', *removes) + + info('Deleting some other stuff not used on android') + # To quote the original distribute.sh, 'well...' + # shprint(sh.rm, '-rf', 'ctypes') + shprint(sh.rm, '-rf', 'lib2to3') + shprint(sh.rm, '-rf', 'idlelib') + for filename in glob.glob('config/libpython*.a'): + shprint(sh.rm, '-f', filename) + shprint(sh.rm, '-rf', 'config/python.o') + # shprint(sh.rm, '-rf', 'lib-dynload/_ctypes_test.so') + # shprint(sh.rm, '-rf', 'lib-dynload/_testcapi.so') + + else: # Python *is* loaded from crystax + ndk_dir = self.ctx.ndk_dir + py_recipe = self.ctx.python_recipe + python_dir = join(ndk_dir, 'sources', 'python', py_recipe.version, + 'libs', arch.arch) + + shprint(sh.cp, '-r', join(python_dir, 'stdlib.zip'), 'crystax_python/crystax_python') + shprint(sh.cp, '-r', join(python_dir, 'modules'), 'crystax_python/crystax_python') + shprint(sh.cp, '-r', self.ctx.get_python_install_dir(), 'crystax_python/crystax_python/site-packages') + + info('Renaming .so files to reflect cross-compile') + site_packages_dir = 'crystax_python/crystax_python/site-packages' + filens = shprint(sh.find, site_packages_dir, '-iname', '*.so').stdout.decode( + 'utf-8').split('\n')[:-1] + for filen in filens: + parts = filen.split('.') + if len(parts) <= 2: + continue + shprint(sh.mv, filen, filen.split('.')[0] + '.so') + site_packages_dir = join(abspath(curdir), + site_packages_dir) + + + self.strip_libraries(arch) + self.fry_eggs(site_packages_dir) + super(ServiceOnlyBootstrap, self).run_distribute() + +bootstrap = ServiceOnlyBootstrap() \ No newline at end of file diff --git a/pythonforandroid/bootstraps/service_only/build/blacklist.txt b/pythonforandroid/bootstraps/service_only/build/blacklist.txt new file mode 100644 index 0000000000..d220d2a2ae --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/blacklist.txt @@ -0,0 +1,90 @@ +# prevent user to include invalid extensions +*.apk +*.pxd + +# eggs +*.egg-info + +# unit test +unittest/* + +# python config +config/makesetup + +# unused kivy files (platform specific) +kivy/input/providers/wm_* +kivy/input/providers/mactouch* +kivy/input/providers/probesysfs* +kivy/input/providers/mtdev* +kivy/input/providers/hidinput* +kivy/core/camera/camera_videocapture* +kivy/core/spelling/*osx* +kivy/core/video/video_pyglet* +kivy/tools +kivy/tests/* +kivy/*/*.h +kivy/*/*.pxi + +# unused encodings +lib-dynload/*codec* +encodings/cp*.pyo +encodings/tis* +encodings/shift* +encodings/bz2* +encodings/iso* +encodings/undefined* +encodings/johab* +encodings/p* +encodings/m* +encodings/euc* +encodings/k* +encodings/unicode_internal* +encodings/quo* +encodings/gb* +encodings/big5* +encodings/hp* +encodings/hz* + +# unused python modules +bsddb/* +wsgiref/* +hotshot/* +pydoc_data/* +tty.pyo +anydbm.pyo +nturl2path.pyo +LICENCE.txt +macurl2path.pyo +dummy_threading.pyo +audiodev.pyo +antigravity.pyo +dumbdbm.pyo +sndhdr.pyo +__phello__.foo.pyo +sunaudio.pyo +os2emxpath.pyo +multiprocessing/dummy* + +# unused binaries python modules +lib-dynload/termios.so +lib-dynload/_lsprof.so +lib-dynload/*audioop.so +lib-dynload/mmap.so +lib-dynload/_hotshot.so +lib-dynload/_heapq.so +lib-dynload/_json.so +lib-dynload/grp.so +lib-dynload/resource.so +lib-dynload/pyexpat.so +lib-dynload/_ctypes_test.so +lib-dynload/_testcapi.so + +# odd files +plat-linux3/regen + +#>sqlite3 +# conditionnal include depending if some recipes are included or not. +sqlite3/* +lib-dynload/_sqlite3.so +#[0-9\.]*<', +# '"private_version">{}<'.format( +# str(time.time())), lines)) + + +def parse_args(args=None): + + global BLACKLIST_PATTERNS, WHITELIST_PATTERNS + default_android_api = 12 + import argparse + ap = argparse.ArgumentParser(description='''\ +Package a Python application for Android. + +For this to work, Java and Ant need to be in your path, as does the +tools directory of the Android SDK. +''') + + ap.add_argument('--private', dest='private', + help='the dir of user files', + required=True) + ap.add_argument('--package', dest='package', + help=('The name of the java package the project will be' + ' packaged under.'), + required=True) +# ap.add_argument('--name', dest='name', +# help=('The human-readable name of the project.'), +# required=True) +# ap.add_argument('--numeric-version', dest='numeric_version', +# help=('The numeric version number of the project. If not ' +# 'given, this is automatically computed from the ' +# 'version.')) +# ap.add_argument('--version', dest='version', +# help=('The version number of the project. This should ' +# 'consist of numbers and dots, and should have the ' +# 'same number of groups of numbers as previous ' +# 'versions.'), +# required=True) +# ap.add_argument('--orientation', dest='orientation', default='portrait', +# help=('The orientation that the game will display in. ' +# 'Usually one of "landscape", "portrait" or ' +# '"sensor"')) +# ap.add_argument('--icon', dest='icon', +# help='A png file to use as the icon for the application.') +# ap.add_argument('--permission', dest='permissions', action='append', +# help='The permissions to give this app.') + ap.add_argument('--meta-data', dest='meta_data', action='append', + help='Custom key=value to add in application metadata') +# ap.add_argument('--presplash', dest='presplash', +# help=('A jpeg file to use as a screen while the ' +# 'application is loading.')) + ap.add_argument('--wakelock', dest='wakelock', action='store_true', + help=('Indicate if the application needs the device ' + 'to stay on')) + ap.add_argument('--window', dest='window', action='store_false', + help='Indicate if the application will be windowed') + ap.add_argument('--blacklist', dest='blacklist', + default=join(curdir, 'blacklist.txt'), + help=('Use a blacklist file to match unwanted file in ' + 'the final APK')) + ap.add_argument('--whitelist', dest='whitelist', + default=join(curdir, 'whitelist.txt'), + help=('Use a whitelist file to prevent blacklisting of ' + 'file in the final APK')) + ap.add_argument('--add-jar', dest='add_jar', action='append', + help=('Add a Java .jar to the libs, so you can access its ' + 'classes with pyjnius. You can specify this ' + 'argument more than once to include multiple jars')) + ap.add_argument('--sdk', dest='sdk_version', default=-1, + type=int, help=('Android SDK version to use. Default to ' + 'the value of minsdk')) + ap.add_argument('--minsdk', dest='min_sdk_version', + default=default_android_api, type=int, + help=('Minimum Android SDK version to use. Default to ' + 'the value of ANDROIDAPI, or {} if not set' + .format(default_android_api))) +# ap.add_argument('--intent-filters', dest='intent_filters', +# help=('Add intent-filters xml rules to the ' +# 'AndroidManifest.xml file. The argument is a ' +# 'filename containing xml. The filename should be ' +# 'located relative to the python-for-android ' +# 'directory')) +# ap.add_argument('--with-billing', dest='billing_pubkey', +# help='If set, the billing service will be added (not implemented)') + ap.add_argument('--service', dest='services', action='append', + help='Declare a new service entrypoint: ' + 'NAME:PATH_TO_PY[:foreground]') + ap.add_argument('--add-source', dest='extra_source_dirs', action='append', + help='Include additional source dirs in Java build') + + def _read_configuration(): + # search for a .p4a configuration file in the current directory + if not exists(".p4a"): + return + print("Reading .p4a configuration") + with open(".p4a") as fd: + lines = fd.readlines() + lines = [shlex.split(line) + for line in lines if not line.startswith("#")] + for line in lines: + for arg in line: + sys.argv.append(arg) + + _read_configuration() + + if args is None: + args = sys.argv[1:] + args = ap.parse_args(args) + args.ignore_path = [] + +# if args.billing_pubkey: +# print('Billing not yet supported in sdl2 bootstrap!') +# exit(1) + + if args.sdk_version == -1: + args.sdk_version = args.min_sdk_version + +# if args.permissions is None: +# args.permissions = [] + + if args.meta_data is None: + args.meta_data = [] + + if args.services is None: + args.services = [] + + if args.blacklist: + with open(args.blacklist) as fd: + patterns = [x.strip() for x in fd.read().splitlines() + if x.strip() and not x.strip().startswith('#')] + BLACKLIST_PATTERNS += patterns + + if args.whitelist: + with open(args.whitelist) as fd: + patterns = [x.strip() for x in fd.read().splitlines() + if x.strip() and not x.strip().startswith('#')] + WHITELIST_PATTERNS += patterns + + make_package(args) + + return args + + +if __name__ == "__main__": + + parse_args() diff --git a/pythonforandroid/bootstraps/service_only/build/gradle.properties b/pythonforandroid/bootstraps/service_only/build/gradle.properties new file mode 100644 index 0000000000..24a89fbbb1 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +android.useDeprecatedNdk=true +org.gradle.jvmargs=-Xmx4096M \ No newline at end of file diff --git a/pythonforandroid/bootstraps/service_only/build/jni/Android.mk b/pythonforandroid/bootstraps/service_only/build/jni/Android.mk new file mode 100644 index 0000000000..5053e7d643 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/jni/Android.mk @@ -0,0 +1 @@ +include $(call all-subdir-makefiles) diff --git a/pythonforandroid/bootstraps/service_only/build/jni/Application.mk b/pythonforandroid/bootstraps/service_only/build/jni/Application.mk new file mode 100644 index 0000000000..e79e378f94 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/jni/Application.mk @@ -0,0 +1,7 @@ + +# Uncomment this if you're using STL in your project +# See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information +# APP_STL := stlport_static + +# APP_ABI := armeabi armeabi-v7a x86 +APP_ABI := $(ARCH) diff --git a/pythonforandroid/bootstraps/service_only/build/jni/src/Android.mk b/pythonforandroid/bootstraps/service_only/build/jni/src/Android.mk new file mode 100644 index 0000000000..018a7cadf0 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/jni/src/Android.mk @@ -0,0 +1,22 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := main + +# Add your application source files here... +LOCAL_SRC_FILES := start.c pyjniusjni.c + +LOCAL_CFLAGS += -I$(LOCAL_PATH)/../../../../other_builds/$(PYTHON2_NAME)/$(ARCH)/python2/python-install/include/python2.7 $(EXTRA_CFLAGS) + +LOCAL_SHARED_LIBRARIES := python_shared + +LOCAL_LDLIBS := -llog $(EXTRA_LDLIBS) + +LOCAL_LDFLAGS += -L$(LOCAL_PATH)/../../../../other_builds/$(PYTHON2_NAME)/$(ARCH)/python2/python-install/lib $(APPLICATION_ADDITIONAL_LDFLAGS) + +include $(BUILD_SHARED_LIBRARY) + +ifdef CRYSTAX_PYTHON_VERSION + $(call import-module,python/$(CRYSTAX_PYTHON_VERSION)) +endif diff --git a/pythonforandroid/bootstraps/service_only/build/jni/src/Android_static.mk b/pythonforandroid/bootstraps/service_only/build/jni/src/Android_static.mk new file mode 100644 index 0000000000..2de278ee00 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/jni/src/Android_static.mk @@ -0,0 +1,10 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := main + +LOCAL_SRC_FILES := YourSourceHere.c + +include $(BUILD_SHARED_LIBRARY) +$(call import-module,SDL)LOCAL_PATH := $(call my-dir) diff --git a/pythonforandroid/bootstraps/service_only/build/jni/src/pyjniusjni.c b/pythonforandroid/bootstraps/service_only/build/jni/src/pyjniusjni.c new file mode 100644 index 0000000000..d67972a4db --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/jni/src/pyjniusjni.c @@ -0,0 +1,103 @@ + +#include +#include + +#define LOGI(...) do {} while (0) +#define LOGE(...) do {} while (0) + +#include "android/log.h" + +/* These JNI management functions are taken from SDL2, but modified to refer to pyjnius */ + +/* #define LOG(n, x) __android_log_write(ANDROID_LOG_INFO, (n), (x)) */ +/* #define LOGP(x) LOG("python", (x)) */ +#define LOG_TAG "Python_android" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) + + +/* Function headers */ +JNIEnv* Android_JNI_GetEnv(void); +static void Android_JNI_ThreadDestroyed(void*); + +static pthread_key_t mThreadKey; +static JavaVM* mJavaVM; + +int Android_JNI_SetupThread(void) +{ + Android_JNI_GetEnv(); + return 1; +} + +/* Library init */ +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) +{ + JNIEnv *env; + mJavaVM = vm; + LOGI("JNI_OnLoad called"); + if ((*mJavaVM)->GetEnv(mJavaVM, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { + LOGE("Failed to get the environment using GetEnv()"); + return -1; + } + /* + * Create mThreadKey so we can keep track of the JNIEnv assigned to each thread + * Refer to http://developer.android.com/guide/practices/design/jni.html for the rationale behind this + */ + if (pthread_key_create(&mThreadKey, Android_JNI_ThreadDestroyed) != 0) { + + __android_log_print(ANDROID_LOG_ERROR, "pyjniusjni", "Error initializing pthread key"); + } + Android_JNI_SetupThread(); + + return JNI_VERSION_1_4; +} + +JNIEnv* Android_JNI_GetEnv(void) +{ + /* From http://developer.android.com/guide/practices/jni.html + * All threads are Linux threads, scheduled by the kernel. + * They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then + * attached to the JavaVM. For example, a thread started with pthread_create can be attached with the + * JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, + * and cannot make JNI calls. + * Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main" + * ThreadGroup, making it visible to the debugger. Calling AttachCurrentThread on an already-attached thread + * is a no-op. + * Note: You can call this function any number of times for the same thread, there's no harm in it + */ + + JNIEnv *env; + int status = (*mJavaVM)->AttachCurrentThread(mJavaVM, &env, NULL); + if(status < 0) { + LOGE("failed to attach current thread"); + return 0; + } + + /* From http://developer.android.com/guide/practices/jni.html + * Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward, + * in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be + * called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific + * to store the JNIEnv in thread-local-storage; that way it'll be passed into your destructor as the argument.) + * Note: The destructor is not called unless the stored value is != NULL + * Note: You can call this function any number of times for the same thread, there's no harm in it + * (except for some lost CPU cycles) + */ + pthread_setspecific(mThreadKey, (void*) env); + + return env; +} + +static void Android_JNI_ThreadDestroyed(void* value) +{ + /* The thread is being destroyed, detach it from the Java VM and set the mThreadKey value to NULL as required */ + JNIEnv *env = (JNIEnv*) value; + if (env != NULL) { + (*mJavaVM)->DetachCurrentThread(mJavaVM); + pthread_setspecific(mThreadKey, NULL); + } +} + +void *WebView_AndroidGetJNIEnv() +{ + return Android_JNI_GetEnv(); +} diff --git a/pythonforandroid/bootstraps/service_only/build/jni/src/start.c b/pythonforandroid/bootstraps/service_only/build/jni/src/start.c new file mode 100644 index 0000000000..37a9b1d90f --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/jni/src/start.c @@ -0,0 +1,317 @@ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" +#ifndef Py_PYTHON_H +#error Python headers needed to compile C extensions, please install development version of Python. +#else + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "android/log.h" + +#define ENTRYPOINT_MAXLEN 128 +#define LOG(n, x) __android_log_write(ANDROID_LOG_INFO, (n), (x)) +#define LOGP(x) LOG("python", (x)) + +static PyObject *androidembed_log(PyObject *self, PyObject *args) { + char *logstr = NULL; + if (!PyArg_ParseTuple(args, "s", &logstr)) { + return NULL; + } + LOG(getenv("PYTHON_NAME"), logstr); + Py_RETURN_NONE; +} + +static PyMethodDef AndroidEmbedMethods[] = { + {"log", androidembed_log, METH_VARARGS, "Log on android platform"}, + {NULL, NULL, 0, NULL}}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef androidembed = {PyModuleDef_HEAD_INIT, "androidembed", + "", -1, AndroidEmbedMethods}; + +PyMODINIT_FUNC initandroidembed(void) { + return PyModule_Create(&androidembed); +} +#else +PyMODINIT_FUNC initandroidembed(void) { + (void)Py_InitModule("androidembed", AndroidEmbedMethods); +} +#endif + +int dir_exists(char *filename) { + struct stat st; + if (stat(filename, &st) == 0) { + if (S_ISDIR(st.st_mode)) + return 1; + } + return 0; +} + +int file_exists(const char *filename) { + FILE *file; + if (file = fopen(filename, "r")) { + fclose(file); + return 1; + } + return 0; +} + +/* int main(int argc, char **argv) { */ +int main(int argc, char *argv[]) { + + char *env_argument = NULL; + char *env_entrypoint = NULL; + char *env_logname = NULL; + char entrypoint[ENTRYPOINT_MAXLEN]; + int ret = 0; + FILE *fd; + + /* AND: Several filepaths are hardcoded here, these must be made + configurable */ + /* AND: P4A uses env vars...not sure what's best */ + LOGP("Initialize Python for Android"); + env_argument = getenv("ANDROID_ARGUMENT"); + setenv("ANDROID_APP_PATH", env_argument, 1); + env_entrypoint = getenv("ANDROID_ENTRYPOINT"); + env_logname = getenv("PYTHON_NAME"); + + if (env_logname == NULL) { + env_logname = "python"; + setenv("PYTHON_NAME", "python", 1); + } + + LOGP("Changing directory to the one provided by ANDROID_ARGUMENT"); + LOGP(env_argument); + chdir(env_argument); + + Py_SetProgramName(L"android_python"); + +#if PY_MAJOR_VERSION >= 3 + /* our logging module for android + */ + PyImport_AppendInittab("androidembed", initandroidembed); +#endif + + LOGP("Preparing to initialize python"); + + if (dir_exists("crystax_python/")) { + LOGP("crystax_python exists"); + char paths[256]; + snprintf(paths, 256, + "%s/crystax_python/stdlib.zip:%s/crystax_python/modules", + env_argument, env_argument); + /* snprintf(paths, 256, "%s/stdlib.zip:%s/modules", env_argument, + * env_argument); */ + LOGP("calculated paths to be..."); + LOGP(paths); + +#if PY_MAJOR_VERSION >= 3 + wchar_t *wchar_paths = Py_DecodeLocale(paths, NULL); + Py_SetPath(wchar_paths); +#else + char *wchar_paths = paths; + LOGP("Can't Py_SetPath in python2, so crystax python2 doesn't work yet"); + exit(1); +#endif + + LOGP("set wchar paths..."); + } else { + LOGP("crystax_python does not exist"); + } + + Py_Initialize(); + +#if PY_MAJOR_VERSION < 3 + PySys_SetArgv(argc, argv); +#endif + + LOGP("Initialized python"); + + /* ensure threads will work. + */ + LOGP("AND: Init threads"); + PyEval_InitThreads(); + +#if PY_MAJOR_VERSION < 3 + initandroidembed(); +#endif + + PyRun_SimpleString("import androidembed\nandroidembed.log('testing python " + "print redirection')"); + + /* inject our bootstrap code to redirect python stdin/stdout + * replace sys.path with our path + */ + PyRun_SimpleString("import sys, posix\n"); + if (dir_exists("lib")) { + /* If we built our own python, set up the paths correctly */ + LOGP("Setting up python from ANDROID_PRIVATE"); + PyRun_SimpleString("private = posix.environ['ANDROID_PRIVATE']\n" + "argument = posix.environ['ANDROID_ARGUMENT']\n" + "sys.path[:] = [ \n" + " private + '/lib/python27.zip', \n" + " private + '/lib/python2.7/', \n" + " private + '/lib/python2.7/lib-dynload/', \n" + " private + '/lib/python2.7/site-packages/', \n" + " argument ]\n"); + } + + if (dir_exists("crystax_python")) { + char add_site_packages_dir[256]; + snprintf(add_site_packages_dir, 256, + "sys.path.append('%s/crystax_python/site-packages')", + env_argument); + + PyRun_SimpleString("import sys\n" + "sys.argv = ['notaninterpreterreally']\n" + "from os.path import realpath, join, dirname"); + PyRun_SimpleString(add_site_packages_dir); + /* "sys.path.append(join(dirname(realpath(__file__)), 'site-packages'))") */ + PyRun_SimpleString("sys.path = ['.'] + sys.path"); + } + + PyRun_SimpleString( + "class LogFile(object):\n" + " def __init__(self):\n" + " self.buffer = ''\n" + " def write(self, s):\n" + " s = self.buffer + s\n" + " lines = s.split(\"\\n\")\n" + " for l in lines[:-1]:\n" + " androidembed.log(l)\n" + " self.buffer = lines[-1]\n" + " def flush(self):\n" + " return\n" + "sys.stdout = sys.stderr = LogFile()\n" + "print('Android path', sys.path)\n" + "import os\n" + "print('os.environ is', os.environ)\n" + "print('Android kivy bootstrap done. __name__ is', __name__)"); + +#if PY_MAJOR_VERSION < 3 + PyRun_SimpleString("import site; print site.getsitepackages()\n"); +#endif + + LOGP("AND: Ran string"); + + /* run it ! + */ + LOGP("Run user program, change dir and execute entrypoint"); + + /* Get the entrypoint, search the .pyo then .py + */ + char *dot = strrchr(env_entrypoint, '.'); + if (dot <= 0) { + LOGP("Invalid entrypoint, abort."); + return -1; + } + if (strlen(env_entrypoint) > ENTRYPOINT_MAXLEN - 2) { + LOGP("Entrypoint path is too long, try increasing ENTRYPOINT_MAXLEN."); + return -1; + } + if (!strcmp(dot, ".pyo")) { + if (!file_exists(env_entrypoint)) { + /* fallback on .py */ + strcpy(entrypoint, env_entrypoint); + entrypoint[strlen(env_entrypoint) - 1] = '\0'; + LOGP(entrypoint); + if (!file_exists(entrypoint)) { + LOGP("Entrypoint not found (.pyo, fallback on .py), abort"); + return -1; + } + } else { + strcpy(entrypoint, env_entrypoint); + } + } else if (!strcmp(dot, ".py")) { + /* if .py is passed, check the pyo version first */ + strcpy(entrypoint, env_entrypoint); + entrypoint[strlen(env_entrypoint) + 1] = '\0'; + entrypoint[strlen(env_entrypoint)] = 'o'; + if (!file_exists(entrypoint)) { + /* fallback on pure python version */ + if (!file_exists(env_entrypoint)) { + LOGP("Entrypoint not found (.py), abort."); + return -1; + } + strcpy(entrypoint, env_entrypoint); + } + } else { + LOGP("Entrypoint have an invalid extension (must be .py or .pyo), abort."); + return -1; + } + // LOGP("Entrypoint is:"); + // LOGP(entrypoint); + fd = fopen(entrypoint, "r"); + if (fd == NULL) { + LOGP("Open the entrypoint failed"); + LOGP(entrypoint); + return -1; + } + + /* run python ! + */ + ret = PyRun_SimpleFile(fd, entrypoint); + + if (PyErr_Occurred() != NULL) { + ret = 1; + PyErr_Print(); /* This exits with the right code if SystemExit. */ + PyObject *f = PySys_GetObject("stdout"); + if (PyFile_WriteString( + "\n", f)) /* python2 used Py_FlushLine, but this no longer exists */ + PyErr_Clear(); + } + + /* close everything + */ + Py_Finalize(); + fclose(fd); + + LOGP("Python for android ended."); + return ret; +} + +JNIEXPORT void JNICALL Java_org_kivy_android_PythonService_nativeStart( + JNIEnv *env, jobject thiz, jstring j_android_private, + jstring j_android_argument, jstring j_service_entrypoint, + jstring j_python_name, jstring j_python_home, jstring j_python_path, + jstring j_arg) { + jboolean iscopy; + const char *android_private = + (*env)->GetStringUTFChars(env, j_android_private, &iscopy); + const char *android_argument = + (*env)->GetStringUTFChars(env, j_android_argument, &iscopy); + const char *service_entrypoint = + (*env)->GetStringUTFChars(env, j_service_entrypoint, &iscopy); + const char *python_name = + (*env)->GetStringUTFChars(env, j_python_name, &iscopy); + const char *python_home = + (*env)->GetStringUTFChars(env, j_python_home, &iscopy); + const char *python_path = + (*env)->GetStringUTFChars(env, j_python_path, &iscopy); + const char *arg = (*env)->GetStringUTFChars(env, j_arg, &iscopy); + + setenv("ANDROID_PRIVATE", android_private, 1); + setenv("ANDROID_ARGUMENT", android_argument, 1); + setenv("ANDROID_ENTRYPOINT", service_entrypoint, 1); + setenv("PYTHONOPTIMIZE", "2", 1); + setenv("PYTHON_NAME", python_name, 1); + setenv("PYTHONHOME", python_home, 1); + setenv("PYTHONPATH", python_path, 1); + setenv("PYTHON_SERVICE_ARGUMENT", arg, 1); + + char *argv[] = {"."}; + /* ANDROID_ARGUMENT points to service subdir, + * so main() will run main.py from this dir + */ + main(1, argv); +} + +#endif diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/Octal.java b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/Octal.java new file mode 100755 index 0000000000..dd10624eab --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/Octal.java @@ -0,0 +1,141 @@ +/** + * Copyright 2012 Kamran Zafar + * + * 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 org.kamranzafar.jtar; + +/** + * @author Kamran Zafar + * + */ +public class Octal { + + /** + * Parse an octal string from a header buffer. This is used for the file + * permission mode value. + * + * @param header + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * + * @return The long value of the octal string. + */ + public static long parseOctal(byte[] header, int offset, int length) { + long result = 0; + boolean stillPadding = true; + + int end = offset + length; + for (int i = offset; i < end; ++i) { + if (header[i] == 0) + break; + + if (header[i] == (byte) ' ' || header[i] == '0') { + if (stillPadding) + continue; + + if (header[i] == (byte) ' ') + break; + } + + stillPadding = false; + + result = ( result << 3 ) + ( header[i] - '0' ); + } + + return result; + } + + /** + * Parse an octal integer from a header buffer. + * + * @param value + * @param buf + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * + * @return The integer value of the octal bytes. + */ + public static int getOctalBytes(long value, byte[] buf, int offset, int length) { + int idx = length - 1; + + buf[offset + idx] = 0; + --idx; + buf[offset + idx] = (byte) ' '; + --idx; + + if (value == 0) { + buf[offset + idx] = (byte) '0'; + --idx; + } else { + for (long val = value; idx >= 0 && val > 0; --idx) { + buf[offset + idx] = (byte) ( (byte) '0' + (byte) ( val & 7 ) ); + val = val >> 3; + } + } + + for (; idx >= 0; --idx) { + buf[offset + idx] = (byte) ' '; + } + + return offset + length; + } + + /** + * Parse the checksum octal integer from a header buffer. + * + * @param value + * @param buf + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * @return The integer value of the entry's checksum. + */ + public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) { + getOctalBytes( value, buf, offset, length ); + buf[offset + length - 1] = (byte) ' '; + buf[offset + length - 2] = 0; + return offset + length; + } + + /** + * Parse an octal long integer from a header buffer. + * + * @param value + * @param buf + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * + * @return The long value of the octal bytes. + */ + public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) { + byte[] temp = new byte[length + 1]; + getOctalBytes( value, temp, 0, length + 1 ); + System.arraycopy( temp, 0, buf, offset, length ); + return offset + length; + } + +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarConstants.java b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarConstants.java new file mode 100755 index 0000000000..4611e20eaa --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarConstants.java @@ -0,0 +1,28 @@ +/** + * Copyright 2012 Kamran Zafar + * + * 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 org.kamranzafar.jtar; + +/** + * @author Kamran Zafar + * + */ +public class TarConstants { + public static final int EOF_BLOCK = 1024; + public static final int DATA_BLOCK = 512; + public static final int HEADER_BLOCK = 512; +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarEntry.java b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarEntry.java new file mode 100755 index 0000000000..fe01db463a --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarEntry.java @@ -0,0 +1,284 @@ +/** + * Copyright 2012 Kamran Zafar + * + * 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 org.kamranzafar.jtar; + +import java.io.File; +import java.util.Date; + +/** + * @author Kamran Zafar + * + */ +public class TarEntry { + protected File file; + protected TarHeader header; + + private TarEntry() { + this.file = null; + header = new TarHeader(); + } + + public TarEntry(File file, String entryName) { + this(); + this.file = file; + this.extractTarHeader(entryName); + } + + public TarEntry(byte[] headerBuf) { + this(); + this.parseTarHeader(headerBuf); + } + + /** + * Constructor to create an entry from an existing TarHeader object. + * + * This method is useful to add new entries programmatically (e.g. for + * adding files or directories that do not exist in the file system). + * + * @param header + * + */ + public TarEntry(TarHeader header) { + this.file = null; + this.header = header; + } + + public boolean equals(TarEntry it) { + return header.name.toString().equals(it.header.name.toString()); + } + + public boolean isDescendent(TarEntry desc) { + return desc.header.name.toString().startsWith(header.name.toString()); + } + + public TarHeader getHeader() { + return header; + } + + public String getName() { + String name = header.name.toString(); + if (header.namePrefix != null && !header.namePrefix.toString().equals("")) { + name = header.namePrefix.toString() + "/" + name; + } + + return name; + } + + public void setName(String name) { + header.name = new StringBuffer(name); + } + + public int getUserId() { + return header.userId; + } + + public void setUserId(int userId) { + header.userId = userId; + } + + public int getGroupId() { + return header.groupId; + } + + public void setGroupId(int groupId) { + header.groupId = groupId; + } + + public String getUserName() { + return header.userName.toString(); + } + + public void setUserName(String userName) { + header.userName = new StringBuffer(userName); + } + + public String getGroupName() { + return header.groupName.toString(); + } + + public void setGroupName(String groupName) { + header.groupName = new StringBuffer(groupName); + } + + public void setIds(int userId, int groupId) { + this.setUserId(userId); + this.setGroupId(groupId); + } + + public void setModTime(long time) { + header.modTime = time / 1000; + } + + public void setModTime(Date time) { + header.modTime = time.getTime() / 1000; + } + + public Date getModTime() { + return new Date(header.modTime * 1000); + } + + public File getFile() { + return this.file; + } + + public long getSize() { + return header.size; + } + + public void setSize(long size) { + header.size = size; + } + + /** + * Checks if the org.kamrazafar.jtar entry is a directory + * + * @return + */ + public boolean isDirectory() { + if (this.file != null) + return this.file.isDirectory(); + + if (header != null) { + if (header.linkFlag == TarHeader.LF_DIR) + return true; + + if (header.name.toString().endsWith("/")) + return true; + } + + return false; + } + + /** + * Extract header from File + * + * @param entryName + */ + public void extractTarHeader(String entryName) { + header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory()); + } + + /** + * Calculate checksum + * + * @param buf + * @return + */ + public long computeCheckSum(byte[] buf) { + long sum = 0; + + for (int i = 0; i < buf.length; ++i) { + sum += 255 & buf[i]; + } + + return sum; + } + + /** + * Writes the header to the byte buffer + * + * @param outbuf + */ + public void writeEntryHeader(byte[] outbuf) { + int offset = 0; + + offset = TarHeader.getNameBytes(header.name, outbuf, offset, TarHeader.NAMELEN); + offset = Octal.getOctalBytes(header.mode, outbuf, offset, TarHeader.MODELEN); + offset = Octal.getOctalBytes(header.userId, outbuf, offset, TarHeader.UIDLEN); + offset = Octal.getOctalBytes(header.groupId, outbuf, offset, TarHeader.GIDLEN); + + long size = header.size; + + offset = Octal.getLongOctalBytes(size, outbuf, offset, TarHeader.SIZELEN); + offset = Octal.getLongOctalBytes(header.modTime, outbuf, offset, TarHeader.MODTIMELEN); + + int csOffset = offset; + for (int c = 0; c < TarHeader.CHKSUMLEN; ++c) + outbuf[offset++] = (byte) ' '; + + outbuf[offset++] = header.linkFlag; + + offset = TarHeader.getNameBytes(header.linkName, outbuf, offset, TarHeader.NAMELEN); + offset = TarHeader.getNameBytes(header.magic, outbuf, offset, TarHeader.USTAR_MAGICLEN); + offset = TarHeader.getNameBytes(header.userName, outbuf, offset, TarHeader.USTAR_USER_NAMELEN); + offset = TarHeader.getNameBytes(header.groupName, outbuf, offset, TarHeader.USTAR_GROUP_NAMELEN); + offset = Octal.getOctalBytes(header.devMajor, outbuf, offset, TarHeader.USTAR_DEVLEN); + offset = Octal.getOctalBytes(header.devMinor, outbuf, offset, TarHeader.USTAR_DEVLEN); + offset = TarHeader.getNameBytes(header.namePrefix, outbuf, offset, TarHeader.USTAR_FILENAME_PREFIX); + + for (; offset < outbuf.length;) + outbuf[offset++] = 0; + + long checkSum = this.computeCheckSum(outbuf); + + Octal.getCheckSumOctalBytes(checkSum, outbuf, csOffset, TarHeader.CHKSUMLEN); + } + + /** + * Parses the tar header to the byte buffer + * + * @param header + * @param bh + */ + public void parseTarHeader(byte[] bh) { + int offset = 0; + + header.name = TarHeader.parseName(bh, offset, TarHeader.NAMELEN); + offset += TarHeader.NAMELEN; + + header.mode = (int) Octal.parseOctal(bh, offset, TarHeader.MODELEN); + offset += TarHeader.MODELEN; + + header.userId = (int) Octal.parseOctal(bh, offset, TarHeader.UIDLEN); + offset += TarHeader.UIDLEN; + + header.groupId = (int) Octal.parseOctal(bh, offset, TarHeader.GIDLEN); + offset += TarHeader.GIDLEN; + + header.size = Octal.parseOctal(bh, offset, TarHeader.SIZELEN); + offset += TarHeader.SIZELEN; + + header.modTime = Octal.parseOctal(bh, offset, TarHeader.MODTIMELEN); + offset += TarHeader.MODTIMELEN; + + header.checkSum = (int) Octal.parseOctal(bh, offset, TarHeader.CHKSUMLEN); + offset += TarHeader.CHKSUMLEN; + + header.linkFlag = bh[offset++]; + + header.linkName = TarHeader.parseName(bh, offset, TarHeader.NAMELEN); + offset += TarHeader.NAMELEN; + + header.magic = TarHeader.parseName(bh, offset, TarHeader.USTAR_MAGICLEN); + offset += TarHeader.USTAR_MAGICLEN; + + header.userName = TarHeader.parseName(bh, offset, TarHeader.USTAR_USER_NAMELEN); + offset += TarHeader.USTAR_USER_NAMELEN; + + header.groupName = TarHeader.parseName(bh, offset, TarHeader.USTAR_GROUP_NAMELEN); + offset += TarHeader.USTAR_GROUP_NAMELEN; + + header.devMajor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN); + offset += TarHeader.USTAR_DEVLEN; + + header.devMinor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN); + offset += TarHeader.USTAR_DEVLEN; + + header.namePrefix = TarHeader.parseName(bh, offset, TarHeader.USTAR_FILENAME_PREFIX); + } +} \ No newline at end of file diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarHeader.java b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarHeader.java new file mode 100755 index 0000000000..b9d3a86bef --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarHeader.java @@ -0,0 +1,243 @@ +/** + * Copyright 2012 Kamran Zafar + * + * 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 org.kamranzafar.jtar; + +import java.io.File; + +/** + * Header + * + *
+ * Offset  Size     Field
+ * 0       100      File name
+ * 100     8        File mode
+ * 108     8        Owner's numeric user ID
+ * 116     8        Group's numeric user ID
+ * 124     12       File size in bytes
+ * 136     12       Last modification time in numeric Unix time format
+ * 148     8        Checksum for header block
+ * 156     1        Link indicator (file type)
+ * 157     100      Name of linked file
+ * 
+ * + * + * File Types + * + *
+ * Value        Meaning
+ * '0'          Normal file
+ * (ASCII NUL)  Normal file (now obsolete)
+ * '1'          Hard link
+ * '2'          Symbolic link
+ * '3'          Character special
+ * '4'          Block special
+ * '5'          Directory
+ * '6'          FIFO
+ * '7'          Contigous
+ * 
+ * + * + * + * Ustar header + * + *
+ * Offset  Size    Field
+ * 257     6       UStar indicator "ustar"
+ * 263     2       UStar version "00"
+ * 265     32      Owner user name
+ * 297     32      Owner group name
+ * 329     8       Device major number
+ * 337     8       Device minor number
+ * 345     155     Filename prefix
+ * 
+ */ + +public class TarHeader { + + /* + * Header + */ + public static final int NAMELEN = 100; + public static final int MODELEN = 8; + public static final int UIDLEN = 8; + public static final int GIDLEN = 8; + public static final int SIZELEN = 12; + public static final int MODTIMELEN = 12; + public static final int CHKSUMLEN = 8; + public static final byte LF_OLDNORM = 0; + + /* + * File Types + */ + public static final byte LF_NORMAL = (byte) '0'; + public static final byte LF_LINK = (byte) '1'; + public static final byte LF_SYMLINK = (byte) '2'; + public static final byte LF_CHR = (byte) '3'; + public static final byte LF_BLK = (byte) '4'; + public static final byte LF_DIR = (byte) '5'; + public static final byte LF_FIFO = (byte) '6'; + public static final byte LF_CONTIG = (byte) '7'; + + /* + * Ustar header + */ + + public static final String USTAR_MAGIC = "ustar"; // POSIX + + public static final int USTAR_MAGICLEN = 8; + public static final int USTAR_USER_NAMELEN = 32; + public static final int USTAR_GROUP_NAMELEN = 32; + public static final int USTAR_DEVLEN = 8; + public static final int USTAR_FILENAME_PREFIX = 155; + + // Header values + public StringBuffer name; + public int mode; + public int userId; + public int groupId; + public long size; + public long modTime; + public int checkSum; + public byte linkFlag; + public StringBuffer linkName; + public StringBuffer magic; // ustar indicator and version + public StringBuffer userName; + public StringBuffer groupName; + public int devMajor; + public int devMinor; + public StringBuffer namePrefix; + + public TarHeader() { + this.magic = new StringBuffer(TarHeader.USTAR_MAGIC); + + this.name = new StringBuffer(); + this.linkName = new StringBuffer(); + + String user = System.getProperty("user.name", ""); + + if (user.length() > 31) + user = user.substring(0, 31); + + this.userId = 0; + this.groupId = 0; + this.userName = new StringBuffer(user); + this.groupName = new StringBuffer(""); + this.namePrefix = new StringBuffer(); + } + + /** + * Parse an entry name from a header buffer. + * + * @param name + * @param header + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * @return The header's entry name. + */ + public static StringBuffer parseName(byte[] header, int offset, int length) { + StringBuffer result = new StringBuffer(length); + + int end = offset + length; + for (int i = offset; i < end; ++i) { + if (header[i] == 0) + break; + result.append((char) header[i]); + } + + return result; + } + + /** + * Determine the number of bytes in an entry name. + * + * @param name + * @param header + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * @return The number of bytes in a header's entry name. + */ + public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) { + int i; + + for (i = 0; i < length && i < name.length(); ++i) { + buf[offset + i] = (byte) name.charAt(i); + } + + for (; i < length; ++i) { + buf[offset + i] = 0; + } + + return offset + length; + } + + /** + * Creates a new header for a file/directory entry. + * + * + * @param name + * File name + * @param size + * File size in bytes + * @param modTime + * Last modification time in numeric Unix time format + * @param dir + * Is directory + * + * @return + */ + public static TarHeader createHeader(String entryName, long size, long modTime, boolean dir) { + String name = entryName; + name = TarUtils.trim(name.replace(File.separatorChar, '/'), '/'); + + TarHeader header = new TarHeader(); + header.linkName = new StringBuffer(""); + + if (name.length() > 100) { + header.namePrefix = new StringBuffer(name.substring(0, name.lastIndexOf('/'))); + header.name = new StringBuffer(name.substring(name.lastIndexOf('/') + 1)); + } else { + header.name = new StringBuffer(name); + } + + if (dir) { + header.mode = 040755; + header.linkFlag = TarHeader.LF_DIR; + if (header.name.charAt(header.name.length() - 1) != '/') { + header.name.append("/"); + } + header.size = 0; + } else { + header.mode = 0100644; + header.linkFlag = TarHeader.LF_NORMAL; + header.size = size; + } + + header.modTime = modTime; + header.checkSum = 0; + header.devMajor = 0; + header.devMinor = 0; + + return header; + } +} \ No newline at end of file diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarInputStream.java b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarInputStream.java new file mode 100755 index 0000000000..ec50a1b688 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarInputStream.java @@ -0,0 +1,249 @@ +/** + * Copyright 2012 Kamran Zafar + * + * 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 org.kamranzafar.jtar; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Kamran Zafar + * + */ +public class TarInputStream extends FilterInputStream { + + private static final int SKIP_BUFFER_SIZE = 2048; + private TarEntry currentEntry; + private long currentFileSize; + private long bytesRead; + private boolean defaultSkip = false; + + public TarInputStream(InputStream in) { + super(in); + currentFileSize = 0; + bytesRead = 0; + } + + @Override + public boolean markSupported() { + return false; + } + + /** + * Not supported + * + */ + @Override + public synchronized void mark(int readlimit) { + } + + /** + * Not supported + * + */ + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + /** + * Read a byte + * + * @see java.io.FilterInputStream#read() + */ + @Override + public int read() throws IOException { + byte[] buf = new byte[1]; + + int res = this.read(buf, 0, 1); + + if (res != -1) { + return 0xFF & buf[0]; + } + + return res; + } + + /** + * Checks if the bytes being read exceed the entry size and adjusts the byte + * array length. Updates the byte counters + * + * + * @see java.io.FilterInputStream#read(byte[], int, int) + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (currentEntry != null) { + if (currentFileSize == currentEntry.getSize()) { + return -1; + } else if ((currentEntry.getSize() - currentFileSize) < len) { + len = (int) (currentEntry.getSize() - currentFileSize); + } + } + + int br = super.read(b, off, len); + + if (br != -1) { + if (currentEntry != null) { + currentFileSize += br; + } + + bytesRead += br; + } + + return br; + } + + /** + * Returns the next entry in the tar file + * + * @return TarEntry + * @throws IOException + */ + public TarEntry getNextEntry() throws IOException { + closeCurrentEntry(); + + byte[] header = new byte[TarConstants.HEADER_BLOCK]; + byte[] theader = new byte[TarConstants.HEADER_BLOCK]; + int tr = 0; + + // Read full header + while (tr < TarConstants.HEADER_BLOCK) { + int res = read(theader, 0, TarConstants.HEADER_BLOCK - tr); + + if (res < 0) { + break; + } + + System.arraycopy(theader, 0, header, tr, res); + tr += res; + } + + // Check if record is null + boolean eof = true; + for (byte b : header) { + if (b != 0) { + eof = false; + break; + } + } + + if (!eof) { + currentEntry = new TarEntry(header); + } + + return currentEntry; + } + + /** + * Returns the current offset (in bytes) from the beginning of the stream. + * This can be used to find out at which point in a tar file an entry's content begins, for instance. + */ + public long getCurrentOffset() { + return bytesRead; + } + + /** + * Closes the current tar entry + * + * @throws IOException + */ + protected void closeCurrentEntry() throws IOException { + if (currentEntry != null) { + if (currentEntry.getSize() > currentFileSize) { + // Not fully read, skip rest of the bytes + long bs = 0; + while (bs < currentEntry.getSize() - currentFileSize) { + long res = skip(currentEntry.getSize() - currentFileSize - bs); + + if (res == 0 && currentEntry.getSize() - currentFileSize > 0) { + // I suspect file corruption + throw new IOException("Possible tar file corruption"); + } + + bs += res; + } + } + + currentEntry = null; + currentFileSize = 0L; + skipPad(); + } + } + + /** + * Skips the pad at the end of each tar entry file content + * + * @throws IOException + */ + protected void skipPad() throws IOException { + if (bytesRead > 0) { + int extra = (int) (bytesRead % TarConstants.DATA_BLOCK); + + if (extra > 0) { + long bs = 0; + while (bs < TarConstants.DATA_BLOCK - extra) { + long res = skip(TarConstants.DATA_BLOCK - extra - bs); + bs += res; + } + } + } + } + + /** + * Skips 'n' bytes on the InputStream
+ * Overrides default implementation of skip + * + */ + @Override + public long skip(long n) throws IOException { + if (defaultSkip) { + // use skip method of parent stream + // may not work if skip not implemented by parent + long bs = super.skip(n); + bytesRead += bs; + + return bs; + } + + if (n <= 0) { + return 0; + } + + long left = n; + byte[] sBuff = new byte[SKIP_BUFFER_SIZE]; + + while (left > 0) { + int res = read(sBuff, 0, (int) (left < SKIP_BUFFER_SIZE ? left : SKIP_BUFFER_SIZE)); + if (res < 0) { + break; + } + left -= res; + } + + return n - left; + } + + public boolean isDefaultSkip() { + return defaultSkip; + } + + public void setDefaultSkip(boolean defaultSkip) { + this.defaultSkip = defaultSkip; + } +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarOutputStream.java b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarOutputStream.java new file mode 100755 index 0000000000..ffdfe87564 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarOutputStream.java @@ -0,0 +1,163 @@ +/** + * Copyright 2012 Kamran Zafar + * + * 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 org.kamranzafar.jtar; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * @author Kamran Zafar + * + */ +public class TarOutputStream extends OutputStream { + private final OutputStream out; + private long bytesWritten; + private long currentFileSize; + private TarEntry currentEntry; + + public TarOutputStream(OutputStream out) { + this.out = out; + bytesWritten = 0; + currentFileSize = 0; + } + + public TarOutputStream(final File fout) throws FileNotFoundException { + this.out = new BufferedOutputStream(new FileOutputStream(fout)); + bytesWritten = 0; + currentFileSize = 0; + } + + /** + * Opens a file for writing. + */ + public TarOutputStream(final File fout, final boolean append) throws IOException { + @SuppressWarnings("resource") + RandomAccessFile raf = new RandomAccessFile(fout, "rw"); + final long fileSize = fout.length(); + if (append && fileSize > TarConstants.EOF_BLOCK) { + raf.seek(fileSize - TarConstants.EOF_BLOCK); + } + out = new BufferedOutputStream(new FileOutputStream(raf.getFD())); + } + + /** + * Appends the EOF record and closes the stream + * + * @see java.io.FilterOutputStream#close() + */ + @Override + public void close() throws IOException { + closeCurrentEntry(); + write( new byte[TarConstants.EOF_BLOCK] ); + out.close(); + } + /** + * Writes a byte to the stream and updates byte counters + * + * @see java.io.FilterOutputStream#write(int) + */ + @Override + public void write(int b) throws IOException { + out.write( b ); + bytesWritten += 1; + + if (currentEntry != null) { + currentFileSize += 1; + } + } + + /** + * Checks if the bytes being written exceed the current entry size. + * + * @see java.io.FilterOutputStream#write(byte[], int, int) + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (currentEntry != null && !currentEntry.isDirectory()) { + if (currentEntry.getSize() < currentFileSize + len) { + throw new IOException( "The current entry[" + currentEntry.getName() + "] size[" + + currentEntry.getSize() + "] is smaller than the bytes[" + ( currentFileSize + len ) + + "] being written." ); + } + } + + out.write( b, off, len ); + + bytesWritten += len; + + if (currentEntry != null) { + currentFileSize += len; + } + } + + /** + * Writes the next tar entry header on the stream + * + * @param entry + * @throws IOException + */ + public void putNextEntry(TarEntry entry) throws IOException { + closeCurrentEntry(); + + byte[] header = new byte[TarConstants.HEADER_BLOCK]; + entry.writeEntryHeader( header ); + + write( header ); + + currentEntry = entry; + } + + /** + * Closes the current tar entry + * + * @throws IOException + */ + protected void closeCurrentEntry() throws IOException { + if (currentEntry != null) { + if (currentEntry.getSize() > currentFileSize) { + throw new IOException( "The current entry[" + currentEntry.getName() + "] of size[" + + currentEntry.getSize() + "] has not been fully written." ); + } + + currentEntry = null; + currentFileSize = 0; + + pad(); + } + } + + /** + * Pads the last content block + * + * @throws IOException + */ + protected void pad() throws IOException { + if (bytesWritten > 0) { + int extra = (int) ( bytesWritten % TarConstants.DATA_BLOCK ); + + if (extra > 0) { + write( new byte[TarConstants.DATA_BLOCK - extra] ); + } + } + } +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarUtils.java b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarUtils.java new file mode 100755 index 0000000000..50165765c0 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kamranzafar/jtar/TarUtils.java @@ -0,0 +1,96 @@ +/** + * Copyright 2012 Kamran Zafar + * + * 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 org.kamranzafar.jtar; + +import java.io.File; + +/** + * @author Kamran + * + */ +public class TarUtils { + /** + * Determines the tar file size of the given folder/file path + * + * @param path + * @return + */ + public static long calculateTarSize(File path) { + return tarSize(path) + TarConstants.EOF_BLOCK; + } + + private static long tarSize(File dir) { + long size = 0; + + if (dir.isFile()) { + return entrySize(dir.length()); + } else { + File[] subFiles = dir.listFiles(); + + if (subFiles != null && subFiles.length > 0) { + for (File file : subFiles) { + if (file.isFile()) { + size += entrySize(file.length()); + } else { + size += tarSize(file); + } + } + } else { + // Empty folder header + return TarConstants.HEADER_BLOCK; + } + } + + return size; + } + + private static long entrySize(long fileSize) { + long size = 0; + size += TarConstants.HEADER_BLOCK; // Header + size += fileSize; // File size + + long extra = size % TarConstants.DATA_BLOCK; + + if (extra > 0) { + size += (TarConstants.DATA_BLOCK - extra); // pad + } + + return size; + } + + public static String trim(String s, char c) { + StringBuffer tmp = new StringBuffer(s); + for (int i = 0; i < tmp.length(); i++) { + if (tmp.charAt(i) != c) { + break; + } else { + tmp.deleteCharAt(i); + } + } + + for (int i = tmp.length() - 1; i >= 0; i--) { + if (tmp.charAt(i) != c) { + break; + } else { + tmp.deleteCharAt(i); + } + } + + return tmp.toString(); + } +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/AssetExtract.java b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/AssetExtract.java new file mode 100644 index 0000000000..40d501c7d0 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/AssetExtract.java @@ -0,0 +1,186 @@ +package org.kivy.android; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.util.Log; + +import org.kamranzafar.jtar.TarEntry; +import org.kamranzafar.jtar.TarInputStream; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; + +public class AssetExtract { + private static String TAG = AssetExtract.class.getSimpleName(); + + /** + * @param parent File or directory to delete recursively + */ + public static void recursiveDelete(File parent) { + if (parent.isDirectory()) { + for (File child : parent.listFiles()) { + recursiveDelete(child); + } + } + parent.delete(); + } + + public static void extractAsset(Context ctx, String assetName, File target) { + Log.v(TAG, "Extract asset " + assetName + " to " + target.getAbsolutePath()); + + // The version of data in memory and on disk. + String packaged_version; + String disk_version; + + try { + PackageManager manager = ctx.getPackageManager(); + PackageInfo info = manager.getPackageInfo(ctx.getPackageName(), 0); + packaged_version = info.versionName; + + Log.v(TAG, "Data version is " + packaged_version); + } catch (PackageManager.NameNotFoundException e) { + packaged_version = null; + } + // If no packaged data version, no unpacking is necessary. + if (packaged_version == null) { + Log.w(TAG, "Data version not found"); + return; + } + + // Check the current disk version, if any. + String filesDir = target.getAbsolutePath(); + String disk_version_fn = filesDir + "/" + assetName + ".version"; + + try { + byte buf[] = new byte[64]; + FileInputStream is = new FileInputStream(disk_version_fn); + int len = is.read(buf); + disk_version = new String(buf, 0, len); + is.close(); + } catch (Exception e) { + disk_version = ""; + } + + if (packaged_version.equals(disk_version)) { + Log.v(TAG, "Disk data version equals packaged data version."); + return; + } + + recursiveDelete(target); + target.mkdirs(); + + if (!extractTar(ctx.getAssets(), assetName, target.getAbsolutePath())) { + Log.e(TAG, "Could not extract " + assetName + " data."); + } + + try { + // Write .nomedia. + new File(target, ".nomedia").createNewFile(); + + // Write version file. + FileOutputStream os = new FileOutputStream(disk_version_fn); + os.write(packaged_version.getBytes()); + os.close(); + } catch (Exception ex) { + Log.w(TAG, ex); + } + } + + public static boolean extractTar(AssetManager assets, String assetName, String target) { + byte buf[] = new byte[1024 * 1024]; + + InputStream assetStream = null; + TarInputStream tis = null; + + try { + assetStream = assets.open(assetName, AssetManager.ACCESS_STREAMING); + tis = new TarInputStream(new BufferedInputStream( + new GZIPInputStream(new BufferedInputStream(assetStream, + 8192)), 8192)); + } catch (IOException e) { + Log.e(TAG, "opening up extract tar", e); + return false; + } + + while (true) { + TarEntry entry = null; + + try { + entry = tis.getNextEntry(); + } catch (java.io.IOException e) { + Log.e(TAG, "extracting tar", e); + return false; + } + + if (entry == null) { + break; + } + + Log.v(TAG, "extracting " + entry.getName()); + + if (entry.isDirectory()) { + + try { + new File(target + "/" + entry.getName()).mkdirs(); + } catch (SecurityException e) { + Log.e(TAG, "extracting tar", e); + } + + continue; + } + + OutputStream out = null; + String path = target + "/" + entry.getName(); + + try { + out = new BufferedOutputStream(new FileOutputStream(path), 8192); + } catch (FileNotFoundException e) { + Log.e(TAG, "extracting tar", e); + } catch (SecurityException e) { + Log.e(TAG, "extracting tar", e); + } + + if (out == null) { + Log.e(TAG, "could not open " + path); + return false; + } + + try { + while (true) { + int len = tis.read(buf); + + if (len == -1) { + break; + } + + out.write(buf, 0, len); + } + + out.flush(); + out.close(); + } catch (java.io.IOException e) { + Log.e(TAG, "extracting zip", e); + return false; + } + } + + try { + tis.close(); + assetStream.close(); + } catch (IOException e) { + // pass + } + + return true; + } +} \ No newline at end of file diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java new file mode 100644 index 0000000000..a9bc8c9a90 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java @@ -0,0 +1,158 @@ +package org.kivy.android; + +import android.app.Activity; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Process; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +public class PythonService extends Service implements Runnable { + private static String TAG = PythonService.class.getSimpleName(); + + public static PythonService mService = null; + /** + * Intent that started the service + */ + private Intent startIntent = null; + + private Thread pythonThread = null; + + // Python environment variables + private String androidPrivate; + private String androidArgument; + private String pythonName; + private String pythonHome; + private String pythonPath; + private String serviceEntrypoint; + private String pythonServiceArgument; + + private boolean autoRestartService = false; + + public void setAutoRestartService(boolean restart) { + autoRestartService = restart; + } + + public boolean canDisplayNotification() { + return true; + } + + public int startType() { + return START_NOT_STICKY; + } + + /** + * {@inheritDoc} + */ + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void onCreate() { + Log.v(TAG, "Device: " + android.os.Build.DEVICE); + Log.v(TAG, "Model: " + android.os.Build.MODEL); + AssetExtract.extractAsset(getApplicationContext(), "private.mp3", getFilesDir()); + super.onCreate(); + } + + /** + * {@inheritDoc} + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (pythonThread != null) { + Log.v(TAG, "Service exists, do not start again"); + return START_NOT_STICKY; + } + startIntent = intent; + + Bundle extras = intent.getExtras(); + androidPrivate = extras.getString("androidPrivate"); + androidArgument = extras.getString("androidArgument"); + serviceEntrypoint = extras.getString("serviceEntrypoint"); + pythonName = extras.getString("pythonName"); + pythonHome = extras.getString("pythonHome"); + pythonPath = extras.getString("pythonPath"); + pythonServiceArgument = extras.getString("pythonServiceArgument"); + + Log.v(TAG, "Starting Python thread"); + pythonThread = new Thread(this); + pythonThread.start(); + + if (canDisplayNotification()) { + doStartForeground(extras); + } + + return startType(); + } + + protected void doStartForeground(Bundle extras) { + String serviceTitle = extras.getString("serviceTitle"); + String serviceDescription = extras.getString("serviceDescription"); + + Context context = getApplicationContext(); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(this) + .setSmallIcon(context.getApplicationInfo().icon) + .setContentTitle(serviceTitle) + .setContentText(serviceDescription); + + int NOTIFICATION_ID = 1; + + Intent targetIntent = new Intent(this, Activity.class); + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, targetIntent, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentIntent(contentIntent); + + startForeground(NOTIFICATION_ID, builder.build()); + } + + /** + * {@inheritDoc} + */ + @Override + public void onDestroy() { + super.onDestroy(); + pythonThread = null; + if (autoRestartService && startIntent != null) { + Log.v(TAG, "Service restart requested"); + startService(startIntent); + } + Process.killProcess(Process.myPid()); + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + PythonUtil.loadLibraries(getFilesDir()); + mService = this; + nativeStart(androidPrivate, androidArgument, serviceEntrypoint, + pythonName, pythonHome, pythonPath, pythonServiceArgument); + stopSelf(); + } + + /** + * @param androidPrivate Directory for private files + * @param androidArgument Android path + * @param serviceEntrypoint Python file to execute first + * @param pythonName Python name + * @param pythonHome Python home + * @param pythonPath Python path + * @param pythonServiceArgument Argument to pass to Python code + */ + public static native void nativeStart(String androidPrivate, + String androidArgument, String serviceEntrypoint, + String pythonName, String pythonHome, String pythonPath, + String pythonServiceArgument); +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonUtil.java new file mode 100644 index 0000000000..692ee13b2c --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonUtil.java @@ -0,0 +1,34 @@ +package org.kivy.android; + +import android.util.Log; + +import java.io.File; + +public class PythonUtil { + private static String TAG = PythonUtil.class.getSimpleName(); + + protected static String[] getLibraries() { + return new String[]{ + "python2.7", + "main", + "/lib/python2.7/lib-dynload/_io.so", + "/lib/python2.7/lib-dynload/unicodedata.so", + "/lib/python2.7/lib-dynload/_ctypes.so", + }; + } + + public static void loadLibraries(File filesDir) { + String filesDirPath = filesDir.getAbsolutePath(); + Log.v(TAG, "Loading libraries from " + filesDirPath); + + for (String lib : getLibraries()) { + if (lib.startsWith("/")) { + System.load(filesDirPath + lib); + } else { + System.loadLibrary(lib); + } + } + + Log.v(TAG, "Loaded everything!"); + } +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/concurrency/PythonEvent.java b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/concurrency/PythonEvent.java new file mode 100644 index 0000000000..9911356ba0 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/concurrency/PythonEvent.java @@ -0,0 +1,45 @@ +package org.kivy.android.concurrency; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Created by ryan on 3/28/14. + */ +public class PythonEvent { + private final Lock lock = new ReentrantLock(); + private final Condition cond = lock.newCondition(); + private boolean flag = false; + + public void set() { + lock.lock(); + try { + flag = true; + cond.signalAll(); + } finally { + lock.unlock(); + } + } + + public void wait_() throws InterruptedException { + lock.lock(); + try { + while (!flag) { + cond.await(); + } + } finally { + lock.unlock(); + } + } + + public void clear() { + lock.lock(); + try { + flag = false; + cond.signalAll(); + } finally { + lock.unlock(); + } + } +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/concurrency/PythonLock.java b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/concurrency/PythonLock.java new file mode 100644 index 0000000000..22f9d903e1 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/concurrency/PythonLock.java @@ -0,0 +1,19 @@ +package org.kivy.android.concurrency; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Created by ryan on 3/28/14. + */ +public class PythonLock { + private final Lock lock = new ReentrantLock(); + + public void acquire() { + lock.lock(); + } + + public void release() { + lock.unlock(); + } +} diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/renpy/android/Hardware.java b/pythonforandroid/bootstraps/service_only/build/src/org/renpy/android/Hardware.java new file mode 100644 index 0000000000..c56d7b1f77 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/src/org/renpy/android/Hardware.java @@ -0,0 +1,257 @@ +package org.renpy.android; + +import java.util.List; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.os.Vibrator; +import android.view.View; + +/** + * Methods that are expected to be called via JNI, to access the device's + * non-screen hardware. (For example, the vibration and accelerometer.) + */ +public class Hardware { + + // The context. + static Context context; + static View view; + + /** + * Vibrate for s seconds. + */ + public static void vibrate(double s) { + Vibrator v = (Vibrator) context + .getSystemService(Context.VIBRATOR_SERVICE); + if (v != null) { + v.vibrate((int) (1000 * s)); + } + } + + /** + * Get an Overview of all Hardware Sensors of an Android Device + */ + public static String getHardwareSensors() { + SensorManager sm = (SensorManager) context + .getSystemService(Context.SENSOR_SERVICE); + List allSensors = sm.getSensorList(Sensor.TYPE_ALL); + + if (allSensors != null) { + String resultString = ""; + for (Sensor s : allSensors) { + resultString += String.format("Name=" + s.getName()); + resultString += String.format(",Vendor=" + s.getVendor()); + resultString += String.format(",Version=" + s.getVersion()); + resultString += String.format(",MaximumRange=" + + s.getMaximumRange()); + // XXX MinDelay is not in the 2.2 + // resultString += String.format(",MinDelay=" + + // s.getMinDelay()); + resultString += String.format(",Power=" + s.getPower()); + resultString += String.format(",Type=" + s.getType() + "\n"); + } + return resultString; + } + return ""; + } + + /** + * Get Access to 3 Axis Hardware Sensors Accelerometer, Orientation and + * Magnetic Field Sensors + */ + public static class generic3AxisSensor implements SensorEventListener { + private final SensorManager sSensorManager; + private final Sensor sSensor; + private final int sSensorType; + SensorEvent sSensorEvent; + + public generic3AxisSensor(int sensorType) { + sSensorType = sensorType; + sSensorManager = (SensorManager) context + .getSystemService(Context.SENSOR_SERVICE); + sSensor = sSensorManager.getDefaultSensor(sSensorType); + } + + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + public void onSensorChanged(SensorEvent event) { + sSensorEvent = event; + } + + /** + * Enable or disable the Sensor by registering/unregistering + */ + public void changeStatus(boolean enable) { + if (enable) { + sSensorManager.registerListener(this, sSensor, + SensorManager.SENSOR_DELAY_NORMAL); + } else { + sSensorManager.unregisterListener(this, sSensor); + } + } + + /** + * Read the Sensor + */ + public float[] readSensor() { + if (sSensorEvent != null) { + return sSensorEvent.values; + } else { + float rv[] = { 0f, 0f, 0f }; + return rv; + } + } + } + + public static generic3AxisSensor accelerometerSensor = null; + public static generic3AxisSensor orientationSensor = null; + public static generic3AxisSensor magneticFieldSensor = null; + + /** + * functions for backward compatibility reasons + */ + + public static void accelerometerEnable(boolean enable) { + if (accelerometerSensor == null) + accelerometerSensor = new generic3AxisSensor( + Sensor.TYPE_ACCELEROMETER); + accelerometerSensor.changeStatus(enable); + } + + public static float[] accelerometerReading() { + float rv[] = { 0f, 0f, 0f }; + if (accelerometerSensor == null) + return rv; + return (float[]) accelerometerSensor.readSensor(); + } + + public static void orientationSensorEnable(boolean enable) { + if (orientationSensor == null) + orientationSensor = new generic3AxisSensor(Sensor.TYPE_ORIENTATION); + orientationSensor.changeStatus(enable); + } + + public static float[] orientationSensorReading() { + float rv[] = { 0f, 0f, 0f }; + if (orientationSensor == null) + return rv; + return (float[]) orientationSensor.readSensor(); + } + + public static void magneticFieldSensorEnable(boolean enable) { + if (magneticFieldSensor == null) + magneticFieldSensor = new generic3AxisSensor( + Sensor.TYPE_MAGNETIC_FIELD); + magneticFieldSensor.changeStatus(enable); + } + + public static float[] magneticFieldSensorReading() { + float rv[] = { 0f, 0f, 0f }; + if (magneticFieldSensor == null) + return rv; + return (float[]) magneticFieldSensor.readSensor(); + } + + /** + * Scan WiFi networks + */ + static List latestResult; + + public static void enableWifiScanner() { + IntentFilter i = new IntentFilter(); + i.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + + context.registerReceiver(new BroadcastReceiver() { + + @Override + public void onReceive(Context c, Intent i) { + // Code to execute when SCAN_RESULTS_AVAILABLE_ACTION event + // occurs + WifiManager w = (WifiManager) c + .getSystemService(Context.WIFI_SERVICE); + latestResult = w.getScanResults(); // Returns a of + // scanResults + } + + }, i); + + } + + public static String scanWifi() { + + // Now you can call this and it should execute the broadcastReceiver's + // onReceive() + WifiManager wm = (WifiManager) context + .getSystemService(Context.WIFI_SERVICE); + boolean a = wm.startScan(); + + if (latestResult != null) { + + String latestResultString = ""; + for (ScanResult result : latestResult) { + latestResultString += String.format("%s\t%s\t%d\n", + result.SSID, result.BSSID, result.level); + } + + return latestResultString; + } + + return ""; + } + + /** + * network state + */ + + public static boolean network_state = false; + + /** + * Check network state directly + * + * (only one connection can be active at a given moment, detects all network + * type) + * + */ + public static boolean checkNetwork() { + boolean state = false; + final ConnectivityManager conMgr = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + + final NetworkInfo activeNetwork = conMgr.getActiveNetworkInfo(); + if (activeNetwork != null && activeNetwork.isConnected()) { + state = true; + } else { + state = false; + } + + return state; + } + + /** + * To recieve network state changes + */ + public static void registerNetworkCheck() { + IntentFilter i = new IntentFilter(); + i.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + context.registerReceiver(new BroadcastReceiver() { + + @Override + public void onReceive(Context c, Intent i) { + network_state = checkNetwork(); + } + + }, i); + } + +} diff --git a/pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml new file mode 100644 index 0000000000..0ac9581be1 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml @@ -0,0 +1,8 @@ + {% if service %} + + {% endif %} + {% for name in service_names %} + + {% endfor %} \ No newline at end of file diff --git a/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java b/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java new file mode 100644 index 0000000000..3523ecc114 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java @@ -0,0 +1,49 @@ +package {{ args.package }}; + +import android.content.Intent; +import android.content.Context; +import org.kivy.android.PythonService; + + +public class Service{{ name|capitalize }} extends PythonService { + {% if sticky %} + /** + * {@inheritDoc} + */ + @Override + public int startType() { + return START_STICKY; + } + {% endif %} + + {% if not foreground %} + /** + * {@inheritDoc} + */ + @Override + public boolean canDisplayNotification() { + return false; + } + {% endif %} + + public static void start(Context ctx, String pythonServiceArgument) { + String argument = ctx.getFilesDir().getAbsolutePath(); + Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); + intent.putExtra("androidPrivate", argument); + intent.putExtra("androidArgument", argument); + intent.putExtra("serviceEntrypoint", "{{ entrypoint }}"); + intent.putExtra("serviceTitle", "{{ name|capitalize }}"); + intent.putExtra("serviceDescription", ""); + intent.putExtra("pythonName", "{{ name }}"); + intent.putExtra("pythonHome", argument); + intent.putExtra("pythonPath", argument + ":" + argument + "/lib"); + intent.putExtra("pythonServiceArgument", pythonServiceArgument); + ctx.startService(intent); + } + + public static void stop(Context ctx) { + Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); + ctx.stopService(intent); + } + +} diff --git a/pythonforandroid/bootstraps/service_only/build/templates/app.build.tmpl.gradle b/pythonforandroid/bootstraps/service_only/build/templates/app.build.tmpl.gradle new file mode 100644 index 0000000000..514ba0f816 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/templates/app.build.tmpl.gradle @@ -0,0 +1,59 @@ +apply plugin: 'com.android.model.application' + +model { + android { + compileSdkVersion {{ args.sdk_version }} + buildToolsVersion "23.0.3" + + defaultConfig { + applicationId "{{ args.package }}" + minSdkVersion.apiLevel {{ args.min_sdk_version }} + targetSdkVersion.apiLevel {{ args.sdk_version }} + versionCode {{ args.numeric_version }} + versionName "{{ args.version }}" + + buildConfigFields { + create() { + type "int" + name "VALUE" + value "1" + } + } + } + ndk { + abiFilters.add("armeabi-v7a") + moduleName = "main" + toolchain = "gcc" + toolchainVersion = "4.9" + platformVersion = 16 + stl = "gnustl_shared" + renderscriptNdkMode = false + CFlags.add("-I" + file("src/main/jni/include/python2.7")) + ldFlags.add("-L" + file("src/main/jni/lib")) + ldLibs.addAll(["log", "python2.7"]) + } + // Configures source set directory. + sources { + main { + jniLibs { + dependencies { + library "gnustl_shared" + // add pre-built libraries here and locate them below: + } + } + } + } + } + repositories { + libs(PrebuiltLibraries) { + gnustl_shared { + binaries.withType(SharedLibraryBinary) { + sharedLibraryFile = file("src/main/jniLibs/${targetPlatform.getName()}/libgnustl_shared.so") + } + } + // more here + } + } +} + +// normal project dependencies here \ No newline at end of file diff --git a/pythonforandroid/bootstraps/service_only/build/whitelist.txt b/pythonforandroid/bootstraps/service_only/build/whitelist.txt new file mode 100644 index 0000000000..41b06ee258 --- /dev/null +++ b/pythonforandroid/bootstraps/service_only/build/whitelist.txt @@ -0,0 +1 @@ +# put files here that you need to un-blacklist diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py old mode 100755 new mode 100644 index 039389db44..23820a3654 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -180,6 +180,7 @@ def __init__(self): clean_all Delete all build components clean_builds Delete all build caches clean_dists Delete all compiled distributions +clean_bootstraps Delete all compiled bootstraps clean_download_cache Delete any downloaded recipe packages clean_recipe_build Delete the build files of a recipe distributions List all distributions @@ -423,6 +424,14 @@ def clean_dists(self, args): if exists(ctx.dist_dir): shutil.rmtree(ctx.dist_dir) + def clean_bootstraps(self, args): + '''Delete all the bootstrap builds.''' + for bs in Bootstrap.list_bootstraps(): + bs = Bootstrap.get_bootstrap(bs, self.ctx) + if bs.build_dir and exists(bs.build_dir): + info('Cleaning build for {} bootstrap.'.format(bs.name)) + shutil.rmtree(bs.build_dir) + def clean_builds(self, args): '''Delete all build caches for each recipe, python-install, java code and compiled libs collection.