diff --git a/doc/source/buildoptions.rst b/doc/source/buildoptions.rst
index e55fc66241..92816d5197 100644
--- a/doc/source/buildoptions.rst
+++ b/doc/source/buildoptions.rst
@@ -93,6 +93,26 @@ yet have one.
features of the Pygame one. It is under active development to fix
these omissions.
+webview
+~~~~~~~
+
+You can use this with ``--bootstrap=webview``, or simply include the
+``webviewjni`` recipe in your ``--requirements``.
+
+The webview bootstrap gui is, per the name, a WebView displaying a
+webpage, but this page is hosted on the device via a Python
+webserver. For instance, your Python code can start a Flask
+application, and your app will display and allow the user to navigate
+this website.
+
+This bootstrap will automatically try to load a website on port 5000
+(the default for Flask), or you can specify a different option with
+the `--port` command line option. If the webserver is not immediately
+present (e.g. during the short Python loading time when first
+started), it will instead display a loading screen until the server is
+ready.
+
+
pygame
~~~~~~
diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py
index 5f831e8583..4cbd1f9266 100644
--- a/pythonforandroid/bootstrap.py
+++ b/pythonforandroid/bootstrap.py
@@ -111,7 +111,7 @@ def run_distribute(self):
json.dump({'dist_name': self.ctx.dist_name,
'bootstrap': self.ctx.bootstrap.name,
'archs': [arch.arch for arch in self.ctx.archs],
- 'recipes': self.ctx.recipe_build_order},
+ 'recipes': self.ctx.recipe_build_order + self.ctx.python_modules},
fileh)
@classmethod
diff --git a/pythonforandroid/bootstraps/webview/__init__.py b/pythonforandroid/bootstraps/webview/__init__.py
new file mode 100644
index 0000000000..315092d7dd
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/__init__.py
@@ -0,0 +1,119 @@
+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 WebViewBootstrap(Bootstrap):
+ name = 'webview'
+
+ recipe_depends = ['webviewjni', ('python2', 'python3crystax')]
+
+ def run_distribute(self):
+ info_main('# Creating Android project from build and {} bootstrap'.format(
+ self.name))
+
+ 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))
+
+ 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(WebViewBootstrap, self).run_distribute()
+
+bootstrap = WebViewBootstrap()
diff --git a/pythonforandroid/bootstraps/webview/build/AndroidManifest.xml b/pythonforandroid/bootstraps/webview/build/AndroidManifest.xml
new file mode 100644
index 0000000000..a3dfc7b224
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/ant.properties b/pythonforandroid/bootstraps/webview/build/ant.properties
new file mode 100644
index 0000000000..f74e644b8a
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/ant.properties
@@ -0,0 +1,18 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked into Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
+source.absolute.dir = tmp-src
diff --git a/pythonforandroid/bootstraps/webview/build/blacklist.txt b/pythonforandroid/bootstraps/webview/build/blacklist.txt
new file mode 100644
index 0000000000..d220d2a2ae
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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')
+ ap.add_argument('--port', help='The port on localhost that the WebView will access',
+ default='5000')
+
+ 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/webview/build/build.xml b/pythonforandroid/bootstraps/webview/build/build.xml
new file mode 100644
index 0000000000..9f19a077b1
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/build.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/jni/Android.mk b/pythonforandroid/bootstraps/webview/build/jni/Android.mk
new file mode 100644
index 0000000000..5053e7d643
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/jni/Android.mk
@@ -0,0 +1 @@
+include $(call all-subdir-makefiles)
diff --git a/pythonforandroid/bootstraps/webview/build/jni/Application.mk b/pythonforandroid/bootstraps/webview/build/jni/Application.mk
new file mode 100644
index 0000000000..e79e378f94
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/jni/src/Android.mk b/pythonforandroid/bootstraps/webview/build/jni/src/Android.mk
new file mode 100644
index 0000000000..b431059f12
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/jni/src/Android.mk
@@ -0,0 +1,24 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := main
+
+# LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include
+
+# 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/webview/build/jni/src/Android_static.mk b/pythonforandroid/bootstraps/webview/build/jni/src/Android_static.mk
new file mode 100644
index 0000000000..faed669c0e
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/jni/src/Android_static.mk
@@ -0,0 +1,12 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := main
+
+LOCAL_SRC_FILES := YourSourceHere.c
+
+LOCAL_STATIC_LIBRARIES := SDL2_static
+
+include $(BUILD_SHARED_LIBRARY)
+$(call import-module,SDL)LOCAL_PATH := $(call my-dir)
diff --git a/pythonforandroid/bootstraps/webview/build/jni/src/pyjniusjni.c b/pythonforandroid/bootstraps/webview/build/jni/src/pyjniusjni.c
new file mode 100644
index 0000000000..d67972a4db
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/jni/src/start.c b/pythonforandroid/bootstraps/webview/build/jni/src/start.c
new file mode 100644
index 0000000000..34372d2ee2
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/jni/src/start.c
@@ -0,0 +1,355 @@
+
+#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);
+}
+
+void Java_org_kivy_android_PythonActivity_nativeSetEnv(
+ JNIEnv* env, jclass jcls,
+ jstring j_name, jstring j_value)
+/* JNIEXPORT void JNICALL Java_org_libsdl_app_SDLActivity_nativeSetEnv( */
+/* JNIEnv* env, jclass jcls, */
+/* jstring j_name, jstring j_value) */
+{
+ jboolean iscopy;
+ const char *name = (*env)->GetStringUTFChars(env, j_name, &iscopy);
+ const char *value = (*env)->GetStringUTFChars(env, j_value, &iscopy);
+ setenv(name, value, 1);
+ (*env)->ReleaseStringUTFChars(env, j_name, name);
+ (*env)->ReleaseStringUTFChars(env, j_value, value);
+}
+
+
+void Java_org_kivy_android_PythonActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj)
+{
+ /* This nativeInit follows SDL2 */
+
+ /* This interface could expand with ABI negotiation, calbacks, etc. */
+ /* SDL_Android_Init(env, cls); */
+
+ /* SDL_SetMainReady(); */
+
+ /* Run the application code! */
+ int status;
+ char *argv[2];
+ argv[0] = "Python_app";
+ argv[1] = NULL;
+ /* status = SDL_main(1, argv); */
+
+ main(1, argv);
+
+ /* Do not issue an exit or the whole application will terminate instead of just the SDL thread */
+ /* exit(status); */
+}
+
+#endif
diff --git a/pythonforandroid/bootstraps/webview/build/proguard-project.txt b/pythonforandroid/bootstraps/webview/build/proguard-project.txt
new file mode 100644
index 0000000000..f2fe1559a2
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/pythonforandroid/bootstraps/webview/build/res/drawable/.gitkeep b/pythonforandroid/bootstraps/webview/build/res/drawable/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/pythonforandroid/bootstraps/webview/build/res/drawable/icon.png b/pythonforandroid/bootstraps/webview/build/res/drawable/icon.png
new file mode 100644
index 0000000000..59a00ba6ff
Binary files /dev/null and b/pythonforandroid/bootstraps/webview/build/res/drawable/icon.png differ
diff --git a/pythonforandroid/bootstraps/webview/build/res/layout/main.xml b/pythonforandroid/bootstraps/webview/build/res/layout/main.xml
new file mode 100644
index 0000000000..123c4b6eac
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/res/layout/main.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/res/values/strings.xml b/pythonforandroid/bootstraps/webview/build/res/values/strings.xml
new file mode 100644
index 0000000000..daebceb9d5
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ SDL App
+ 0.1
+
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/kamranzafar/jtar/Octal.java b/pythonforandroid/bootstraps/webview/build/src/org/kamranzafar/jtar/Octal.java
new file mode 100755
index 0000000000..dd10624eab
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/kamranzafar/jtar/TarConstants.java b/pythonforandroid/bootstraps/webview/build/src/org/kamranzafar/jtar/TarConstants.java
new file mode 100755
index 0000000000..4611e20eaa
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/kamranzafar/jtar/TarEntry.java b/pythonforandroid/bootstraps/webview/build/src/org/kamranzafar/jtar/TarEntry.java
new file mode 100755
index 0000000000..fe01db463a
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/kamranzafar/jtar/TarHeader.java b/pythonforandroid/bootstraps/webview/build/src/org/kamranzafar/jtar/TarHeader.java
new file mode 100755
index 0000000000..b9d3a86bef
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/kamranzafar/jtar/TarInputStream.java b/pythonforandroid/bootstraps/webview/build/src/org/kamranzafar/jtar/TarInputStream.java
new file mode 100755
index 0000000000..ec50a1b688
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/kamranzafar/jtar/TarOutputStream.java b/pythonforandroid/bootstraps/webview/build/src/org/kamranzafar/jtar/TarOutputStream.java
new file mode 100755
index 0000000000..ffdfe87564
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/kamranzafar/jtar/TarUtils.java b/pythonforandroid/bootstraps/webview/build/src/org/kamranzafar/jtar/TarUtils.java
new file mode 100755
index 0000000000..50165765c0
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/kivy/android/GenericBroadcastReceiver.java b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/GenericBroadcastReceiver.java
new file mode 100644
index 0000000000..58a1c5edf8
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/GenericBroadcastReceiver.java
@@ -0,0 +1,19 @@
+package org.kivy.android;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.Context;
+
+public class GenericBroadcastReceiver extends BroadcastReceiver {
+
+ GenericBroadcastReceiverCallback listener;
+
+ public GenericBroadcastReceiver(GenericBroadcastReceiverCallback listener) {
+ super();
+ this.listener = listener;
+ }
+
+ public void onReceive(Context context, Intent intent) {
+ this.listener.onReceive(context, intent);
+ }
+}
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/GenericBroadcastReceiverCallback.java b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/GenericBroadcastReceiverCallback.java
new file mode 100644
index 0000000000..1a87c98b2d
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/GenericBroadcastReceiverCallback.java
@@ -0,0 +1,8 @@
+package org.kivy.android;
+
+import android.content.Intent;
+import android.content.Context;
+
+public interface GenericBroadcastReceiverCallback {
+ void onReceive(Context context, Intent intent);
+};
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonActivity.java
new file mode 100644
index 0000000000..ba00ab36f2
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonActivity.java
@@ -0,0 +1,393 @@
+
+package org.kivy.android;
+
+import java.net.Socket;
+import java.net.InetSocketAddress;
+
+import android.os.SystemClock;
+
+import java.io.InputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ArrayList;
+
+import android.app.*;
+import android.content.*;
+import android.view.*;
+import android.view.ViewGroup;
+import android.view.SurfaceView;
+import android.app.Activity;
+import android.content.Intent;
+import android.util.Log;
+import android.widget.Toast;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.graphics.PixelFormat;
+import android.view.SurfaceHolder;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ApplicationInfo;
+import android.content.Intent;
+import android.widget.ImageView;
+import java.io.InputStream;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import android.widget.AbsoluteLayout;
+import android.view.ViewGroup.LayoutParams;
+
+import android.webkit.WebViewClient;
+import android.webkit.WebView;
+
+import org.kivy.android.PythonUtil;
+
+import org.kivy.android.WebViewLoader;
+
+import org.renpy.android.ResourceManager;
+import org.renpy.android.AssetExtract;
+
+public class PythonActivity extends Activity {
+ // This activity is modified from a mixture of the SDLActivity and
+ // PythonActivity in the SDL2 bootstrap, but removing all the SDL2
+ // specifics.
+
+ private static final String TAG = "PythonActivity";
+
+ public static PythonActivity mActivity = null;
+
+ /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
+ public static boolean mBrokenLibraries;
+
+ protected static ViewGroup mLayout;
+ protected static WebView mWebView;
+
+ protected static Thread mPythonThread;
+
+ private ResourceManager resourceManager = null;
+ private Bundle mMetaData = null;
+ private PowerManager.WakeLock mWakeLock = null;
+
+ public static void initialize() {
+ // The static nature of the singleton and Android quirkyness force us to initialize everything here
+ // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
+ mWebView = null;
+ mLayout = null;
+ mBrokenLibraries = false;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.v(TAG, "My oncreate running");
+ resourceManager = new ResourceManager(this);
+
+ Log.v(TAG, "Ready to unpack");
+ unpackData("private", getFilesDir());
+
+ this.mActivity = this;
+
+ Log.v("Python", "Device: " + android.os.Build.DEVICE);
+ Log.v("Python", "Model: " + android.os.Build.MODEL);
+ super.onCreate(savedInstanceState);
+
+ PythonActivity.initialize();
+
+ // Load shared libraries
+ String errorMsgBrokenLib = "";
+ try {
+ loadLibraries();
+ } catch(UnsatisfiedLinkError e) {
+ System.err.println(e.getMessage());
+ mBrokenLibraries = true;
+ errorMsgBrokenLib = e.getMessage();
+ } catch(Exception e) {
+ System.err.println(e.getMessage());
+ mBrokenLibraries = true;
+ errorMsgBrokenLib = e.getMessage();
+ }
+
+ if (mBrokenLibraries)
+ {
+ AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
+ dlgAlert.setMessage("An error occurred while trying to load the application libraries. Please try again and/or reinstall."
+ + System.getProperty("line.separator")
+ + System.getProperty("line.separator")
+ + "Error: " + errorMsgBrokenLib);
+ dlgAlert.setTitle("Python Error");
+ dlgAlert.setPositiveButton("Exit",
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog,int id) {
+ // if this button is clicked, close current activity
+ PythonActivity.mActivity.finish();
+ }
+ });
+ dlgAlert.setCancelable(false);
+ dlgAlert.create().show();
+
+ return;
+ }
+
+ // Set up the webview
+ mWebView = new WebView(this);
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.getSettings().setDomStorageEnabled(true);
+ mWebView.loadUrl("file:///" + mActivity.getFilesDir().getAbsolutePath() + "/_load.html");
+
+ mWebView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+ mWebView.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ view.loadUrl(url);
+ return false;
+ }
+ });
+
+ mLayout = new AbsoluteLayout(this);
+ mLayout.addView(mWebView);
+
+ setContentView(mLayout);
+
+ String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath();
+ Log.v(TAG, "Setting env vars for start.c and Python to use");
+ PythonActivity.nativeSetEnv("ANDROID_PRIVATE", mFilesDirectory);
+ PythonActivity.nativeSetEnv("ANDROID_ARGUMENT", mFilesDirectory);
+ PythonActivity.nativeSetEnv("ANDROID_APP_PATH", mFilesDirectory);
+ PythonActivity.nativeSetEnv("ANDROID_ENTRYPOINT", "main.pyo");
+ PythonActivity.nativeSetEnv("PYTHONHOME", mFilesDirectory);
+ PythonActivity.nativeSetEnv("PYTHONPATH", mFilesDirectory + ":" + mFilesDirectory + "/lib");
+
+ try {
+ Log.v(TAG, "Access to our meta-data...");
+ this.mMetaData = this.mActivity.getPackageManager().getApplicationInfo(
+ this.mActivity.getPackageName(), PackageManager.GET_META_DATA).metaData;
+
+ PowerManager pm = (PowerManager) this.mActivity.getSystemService(Context.POWER_SERVICE);
+ if ( this.mMetaData.getInt("wakelock") == 1 ) {
+ this.mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Screen On");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+
+ final Thread pythonThread = new Thread(new PythonMain(), "PythonThread");
+ PythonActivity.mPythonThread = pythonThread;
+ pythonThread.start();
+
+ final Thread wvThread = new Thread(new WebViewLoaderMain(), "WvThread");
+ wvThread.start();
+
+ }
+
+ public void loadLibraries() {
+ PythonUtil.loadLibraries(getFilesDir());
+ }
+
+ public void recursiveDelete(File f) {
+ if (f.isDirectory()) {
+ for (File r : f.listFiles()) {
+ recursiveDelete(r);
+ }
+ }
+ f.delete();
+ }
+
+ /**
+ * Show an error using a toast. (Only makes sense from non-UI
+ * threads.)
+ */
+ public void toastError(final String msg) {
+
+ final Activity thisActivity = this;
+
+ runOnUiThread(new Runnable () {
+ public void run() {
+ Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show();
+ }
+ });
+
+ // Wait to show the error.
+ synchronized (this) {
+ try {
+ this.wait(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ public void unpackData(final String resource, File target) {
+
+ Log.v(TAG, "UNPACKING!!! " + resource + " " + target.getName());
+
+ // The version of data in memory and on disk.
+ String data_version = resourceManager.getString(resource + "_version");
+ String disk_version = null;
+
+ Log.v(TAG, "Data version is " + data_version);
+
+ // If no version, no unpacking is necessary.
+ if (data_version == null) {
+ return;
+ }
+
+ // Check the current disk version, if any.
+ String filesDir = target.getAbsolutePath();
+ String disk_version_fn = filesDir + "/" + resource + ".version";
+
+ try {
+ byte buf[] = new byte[64];
+ InputStream 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 the disk data is out of date, extract it and write the
+ // version file.
+ // if (! data_version.equals(disk_version)) {
+ if (! data_version.equals(disk_version)) {
+ Log.v(TAG, "Extracting " + resource + " assets.");
+
+ recursiveDelete(target);
+ target.mkdirs();
+
+ AssetExtract ae = new AssetExtract(this);
+ if (!ae.extractTar(resource + ".mp3", target.getAbsolutePath())) {
+ toastError("Could not extract " + resource + " data.");
+ }
+
+ try {
+ // Write .nomedia.
+ new File(target, ".nomedia").createNewFile();
+
+ // Write version file.
+ FileOutputStream os = new FileOutputStream(disk_version_fn);
+ os.write(data_version.getBytes());
+ os.close();
+ } catch (Exception e) {
+ Log.w("python", e);
+ }
+ }
+ }
+
+ public static ViewGroup getLayout() {
+ return mLayout;
+ }
+
+
+ //----------------------------------------------------------------------------
+ // Listener interface for onNewIntent
+ //
+
+ public interface NewIntentListener {
+ void onNewIntent(Intent intent);
+ }
+
+ private List newIntentListeners = null;
+
+ public void registerNewIntentListener(NewIntentListener listener) {
+ if ( this.newIntentListeners == null )
+ this.newIntentListeners = Collections.synchronizedList(new ArrayList());
+ this.newIntentListeners.add(listener);
+ }
+
+ public void unregisterNewIntentListener(NewIntentListener listener) {
+ if ( this.newIntentListeners == null )
+ return;
+ this.newIntentListeners.remove(listener);
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if ( this.newIntentListeners == null )
+ return;
+ this.onResume();
+ synchronized ( this.newIntentListeners ) {
+ Iterator iterator = this.newIntentListeners.iterator();
+ while ( iterator.hasNext() ) {
+ (iterator.next()).onNewIntent(intent);
+ }
+ }
+ }
+
+ //----------------------------------------------------------------------------
+ // Listener interface for onActivityResult
+ //
+
+ public interface ActivityResultListener {
+ void onActivityResult(int requestCode, int resultCode, Intent data);
+ }
+
+ private List activityResultListeners = null;
+
+ public void registerActivityResultListener(ActivityResultListener listener) {
+ if ( this.activityResultListeners == null )
+ this.activityResultListeners = Collections.synchronizedList(new ArrayList());
+ this.activityResultListeners.add(listener);
+ }
+
+ public void unregisterActivityResultListener(ActivityResultListener listener) {
+ if ( this.activityResultListeners == null )
+ return;
+ this.activityResultListeners.remove(listener);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if ( this.activityResultListeners == null )
+ return;
+ this.onResume();
+ synchronized ( this.activityResultListeners ) {
+ Iterator iterator = this.activityResultListeners.iterator();
+ while ( iterator.hasNext() )
+ (iterator.next()).onActivityResult(requestCode, resultCode, intent);
+ }
+ }
+
+ public static void start_service(String serviceTitle, String serviceDescription,
+ String pythonServiceArgument) {
+ Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
+ String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath();
+ String filesDirectory = argument;
+ serviceIntent.putExtra("androidPrivate", argument);
+ serviceIntent.putExtra("androidArgument", argument);
+ serviceIntent.putExtra("serviceEntrypoint", "service/main.pyo");
+ serviceIntent.putExtra("pythonHome", argument);
+ serviceIntent.putExtra("pythonPath", argument + ":" + filesDirectory + "/lib");
+ serviceIntent.putExtra("serviceTitle", serviceTitle);
+ serviceIntent.putExtra("serviceDescription", serviceDescription);
+ serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument);
+ PythonActivity.mActivity.startService(serviceIntent);
+ }
+
+ public static void stop_service() {
+ Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
+ PythonActivity.mActivity.stopService(serviceIntent);
+ }
+
+
+ public static native void nativeSetEnv(String j_name, String j_value);
+ public static native int nativeInit(Object arguments);
+
+}
+
+
+class PythonMain implements Runnable {
+ @Override
+ public void run() {
+ PythonActivity.nativeInit(new String[0]);
+ }
+}
+
+class WebViewLoaderMain implements Runnable {
+ @Override
+ public void run() {
+ WebViewLoader.testConnection();
+ }
+}
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonService.java
new file mode 100644
index 0000000000..f8dde3e0d2
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonService.java
@@ -0,0 +1,129 @@
+package org.kivy.android;
+
+import android.app.Service;
+import android.os.IBinder;
+import android.os.Bundle;
+import android.content.Intent;
+import android.content.Context;
+import android.util.Log;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.os.Process;
+
+import org.kivy.android.PythonUtil;
+
+import org.renpy.android.Hardware;
+
+
+public class PythonService extends Service implements Runnable {
+
+ // Thread for Python code
+ 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;
+ // Argument to pass to Python code,
+ private String pythonServiceArgument;
+ public static PythonService mService = null;
+ private Intent startIntent = null;
+
+ private boolean autoRestartService = false;
+
+ public void setAutoRestartService(boolean restart) {
+ autoRestartService = restart;
+ }
+
+ public boolean canDisplayNotification() {
+ return true;
+ }
+
+ public int startType() {
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent arg0) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (pythonThread != null) {
+ Log.v("python service", "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");
+
+ 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();
+ Notification notification = new Notification(context.getApplicationInfo().icon,
+ serviceTitle, System.currentTimeMillis());
+ Intent contextIntent = new Intent(context, PythonActivity.class);
+ PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ notification.setLatestEventInfo(context, serviceTitle, serviceDescription, pIntent);
+ startForeground(1, notification);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ pythonThread = null;
+ if (autoRestartService && startIntent != null) {
+ Log.v("python service", "service restart requested");
+ startService(startIntent);
+ }
+ Process.killProcess(Process.myPid());
+ }
+
+ @Override
+ public void run(){
+ PythonUtil.loadLibraries(getFilesDir());
+ this.mService = this;
+ nativeStart(
+ androidPrivate, androidArgument,
+ serviceEntrypoint, pythonName,
+ pythonHome, pythonPath,
+ pythonServiceArgument);
+ stopSelf();
+ }
+
+ // Native part
+ public static native void nativeStart(
+ String androidPrivate, String androidArgument,
+ String serviceEntrypoint, String pythonName,
+ String pythonHome, String pythonPath,
+ String pythonServiceArgument);
+}
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonUtil.java
new file mode 100644
index 0000000000..9d532b613f
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonUtil.java
@@ -0,0 +1,56 @@
+package org.kivy.android;
+
+import java.io.File;
+
+import android.util.Log;
+
+
+public class PythonUtil {
+ private static final String TAG = "PythonUtil";
+
+ protected static String[] getLibraries() {
+ return new String[] {
+ // "SDL2",
+ // "SDL2_image",
+ // "SDL2_mixer",
+ // "SDL2_ttf",
+ "python2.7",
+ "python3.5m",
+ "main"
+ };
+ }
+
+ public static void loadLibraries(File filesDir) {
+
+ String filesDirPath = filesDir.getAbsolutePath();
+ boolean skippedPython = false;
+
+ for (String lib : getLibraries()) {
+ try {
+ System.loadLibrary(lib);
+ } catch(UnsatisfiedLinkError e) {
+ if (lib.startsWith("python") && !skippedPython) {
+ skippedPython = true;
+ continue;
+ }
+ throw e;
+ }
+ }
+
+ try {
+ System.load(filesDirPath + "/lib/python2.7/lib-dynload/_io.so");
+ System.load(filesDirPath + "/lib/python2.7/lib-dynload/unicodedata.so");
+ } catch(UnsatisfiedLinkError e) {
+ Log.v(TAG, "Failed to load _io.so or unicodedata.so...but that's okay.");
+ }
+
+ try {
+ // System.loadLibrary("ctypes");
+ System.load(filesDirPath + "/lib/python2.7/lib-dynload/_ctypes.so");
+ } catch(UnsatisfiedLinkError e) {
+ Log.v(TAG, "Unsatisfied linker when loading ctypes");
+ }
+
+ Log.v(TAG, "Loaded everything!");
+ }
+}
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/concurrency/PythonEvent.java b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/concurrency/PythonEvent.java
new file mode 100644
index 0000000000..9911356ba0
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/kivy/android/concurrency/PythonLock.java b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/concurrency/PythonLock.java
new file mode 100644
index 0000000000..22f9d903e1
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/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/webview/build/src/org/renpy/android/AssetExtract.java b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/AssetExtract.java
new file mode 100644
index 0000000000..52d6424e09
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/AssetExtract.java
@@ -0,0 +1,115 @@
+// This string is autogenerated by ChangeAppSettings.sh, do not change
+// spaces amount
+package org.renpy.android;
+
+import java.io.*;
+
+import android.app.Activity;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.File;
+
+import java.util.zip.GZIPInputStream;
+
+import android.content.res.AssetManager;
+
+import org.kamranzafar.jtar.*;
+
+public class AssetExtract {
+
+ private AssetManager mAssetManager = null;
+ private Activity mActivity = null;
+
+ public AssetExtract(Activity act) {
+ mActivity = act;
+ mAssetManager = act.getAssets();
+ }
+
+ public boolean extractTar(String asset, String target) {
+
+ byte buf[] = new byte[1024 * 1024];
+
+ InputStream assetStream = null;
+ TarInputStream tis = null;
+
+ try {
+ assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
+ tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192));
+ } catch (IOException e) {
+ Log.e("python", "opening up extract tar", e);
+ return false;
+ }
+
+ while (true) {
+ TarEntry entry = null;
+
+ try {
+ entry = tis.getNextEntry();
+ } catch ( java.io.IOException e ) {
+ Log.e("python", "extracting tar", e);
+ return false;
+ }
+
+ if ( entry == null ) {
+ break;
+ }
+
+ Log.v("python", "extracting " + entry.getName());
+
+ if (entry.isDirectory()) {
+
+ try {
+ new File(target +"/" + entry.getName()).mkdirs();
+ } catch ( SecurityException e ) { };
+
+ continue;
+ }
+
+ OutputStream out = null;
+ String path = target + "/" + entry.getName();
+
+ try {
+ out = new BufferedOutputStream(new FileOutputStream(path), 8192);
+ } catch ( FileNotFoundException e ) {
+ } catch ( SecurityException e ) { };
+
+ if ( out == null ) {
+ Log.e("python", "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("python", "extracting zip", e);
+ return false;
+ }
+ }
+
+ try {
+ tis.close();
+ assetStream.close();
+ } catch (IOException e) {
+ // pass
+ }
+
+ return true;
+ }
+}
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/Hardware.java b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/Hardware.java
new file mode 100644
index 0000000000..c50692d71d
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/Hardware.java
@@ -0,0 +1,287 @@
+package org.renpy.android;
+
+import android.content.Context;
+import android.os.Vibrator;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.util.DisplayMetrics;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.View;
+
+import java.util.List;
+import java.util.ArrayList;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import org.kivy.android.PythonActivity;
+
+/**
+ * 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();
+ }
+
+ static public DisplayMetrics metrics = new DisplayMetrics();
+
+ /**
+ * Get display DPI.
+ */
+ public static int getDPI() {
+ // AND: Shouldn't have to get the metrics like this every time...
+ PythonActivity.mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
+ return metrics.densityDpi;
+ }
+
+ // /**
+ // * Show the soft keyboard.
+ // */
+ // public static void showKeyboard(int input_type) {
+ // //Log.i("python", "hardware.Java show_keyword " input_type);
+
+ // InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // SDLSurfaceView vw = (SDLSurfaceView) view;
+
+ // int inputType = input_type;
+
+ // if (vw.inputType != inputType){
+ // vw.inputType = inputType;
+ // imm.restartInput(view);
+ // }
+
+ // imm.showSoftInput(view, InputMethodManager.SHOW_FORCED);
+ // }
+
+ /**
+ * Hide the soft keyboard.
+ */
+ public static void hideKeyboard() {
+ InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ /**
+ * 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/webview/build/src/org/renpy/android/PythonActivity.java b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/PythonActivity.java
new file mode 100644
index 0000000000..0d34d31c9a
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/PythonActivity.java
@@ -0,0 +1,12 @@
+package org.renpy.android;
+
+import android.util.Log;
+
+class PythonActivity extends org.kivy.android.PythonActivity {
+ static {
+ Log.w("PythonActivity", "Accessing org.renpy.android.PythonActivity "
+ + "is deprecated and will be removed in a "
+ + "future version. Please switch to "
+ + "org.kivy.android.PythonActivity.");
+ }
+}
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/PythonService.java b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/PythonService.java
new file mode 100644
index 0000000000..73febed68a
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/PythonService.java
@@ -0,0 +1,12 @@
+package org.renpy.android;
+
+import android.util.Log;
+
+class PythonService extends org.kivy.android.PythonService {
+ static {
+ Log.w("PythonService", "Accessing org.renpy.android.PythonService "
+ + "is deprecated and will be removed in a "
+ + "future version. Please switch to "
+ + "org.kivy.android.PythonService.");
+ }
+}
diff --git a/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/ResourceManager.java b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/ResourceManager.java
new file mode 100644
index 0000000000..47455abb04
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/src/org/renpy/android/ResourceManager.java
@@ -0,0 +1,54 @@
+/**
+ * This class takes care of managing resources for us. In our code, we
+ * can't use R, since the name of the package containing R will
+ * change. (This same code is used in both org.renpy.android and
+ * org.renpy.pygame.) So this is the next best thing.
+ */
+
+package org.renpy.android;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.view.View;
+
+import android.util.Log;
+
+public class ResourceManager {
+
+ private Activity act;
+ private Resources res;
+
+ public ResourceManager(Activity activity) {
+ act = activity;
+ res = act.getResources();
+ }
+
+ public int getIdentifier(String name, String kind) {
+ Log.v("SDL", "getting identifier");
+ Log.v("SDL", "kind is " + kind + " and name " + name);
+ Log.v("SDL", "result is " + res.getIdentifier(name, kind, act.getPackageName()));
+ return res.getIdentifier(name, kind, act.getPackageName());
+ }
+
+ public String getString(String name) {
+
+ try {
+ Log.v("SDL", "asked to get string " + name);
+ return res.getString(getIdentifier(name, "string"));
+ } catch (Exception e) {
+ Log.v("SDL", "got exception looking for string!");
+ return null;
+ }
+ }
+
+ public View inflateView(String name) {
+ int id = getIdentifier(name, "layout");
+ return act.getLayoutInflater().inflate(id, null);
+ }
+
+ public View getViewById(View v, String name) {
+ int id = getIdentifier(name, "id");
+ return v.findViewById(id);
+ }
+
+}
diff --git a/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml
new file mode 100644
index 0000000000..079638e0e9
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml
@@ -0,0 +1,95 @@
+
+
+
+ = 9 %}
+ android:xlargeScreens="true"
+ {% endif %}
+ />
+
+
+
+
+
+
+ {% for perm in args.permissions %}
+ {% if '.' in perm %}
+
+ {% else %}
+
+ {% endif %}
+ {% endfor %}
+
+ {% if args.wakelock %}
+
+ {% endif %}
+
+ {% if args.billing_pubkey %}
+
+ {% endif %}
+
+
+
+
+ {% for m in args.meta_data %}
+ {% endfor %}
+
+
+
+
+
+
+
+ {%- if args.intent_filters -%}
+ {{- args.intent_filters -}}
+ {%- endif -%}
+
+
+ {% if service %}
+
+ {% endif %}
+ {% for name in service_names %}
+
+ {% endfor %}
+
+ {% if args.billing_pubkey %}
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/templates/Service.tmpl.java b/pythonforandroid/bootstraps/webview/build/templates/Service.tmpl.java
new file mode 100644
index 0000000000..bf87996212
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/templates/Service.tmpl.java
@@ -0,0 +1,56 @@
+package {{ args.package }};
+
+import android.content.Intent;
+import android.content.Context;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.os.Bundle;
+import org.kivy.android.PythonService;
+import org.kivy.android.PythonActivity;
+
+
+public class Service{{ name|capitalize }} extends PythonService {
+ {% if sticky %}
+ @Override
+ public int startType() {
+ return START_STICKY;
+ }
+ {% endif %}
+
+ {% if not foreground %}
+ @Override
+ public boolean canDisplayNotification() {
+ return false;
+ }
+ {% endif %}
+
+ @Override
+ protected void doStartForeground(Bundle extras) {
+ Context context = getApplicationContext();
+ Notification notification = new Notification(context.getApplicationInfo().icon,
+ "{{ args.name }}", System.currentTimeMillis());
+ Intent contextIntent = new Intent(context, PythonActivity.class);
+ PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ notification.setLatestEventInfo(context, "{{ args.name }}", "{{ name| capitalize }}", pIntent);
+ startForeground({{ service_id }}, notification);
+ }
+
+ static public void start(Context ctx, String pythonServiceArgument) {
+ Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
+ String argument = ctx.getFilesDir().getAbsolutePath();
+ intent.putExtra("androidPrivate", argument);
+ intent.putExtra("androidArgument", argument);
+ intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
+ intent.putExtra("pythonName", "{{ name }}");
+ intent.putExtra("pythonHome", argument);
+ intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
+ intent.putExtra("pythonServiceArgument", pythonServiceArgument);
+ ctx.startService(intent);
+ }
+
+ static public void stop(Context ctx) {
+ Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
+ ctx.stopService(intent);
+ }
+}
diff --git a/pythonforandroid/bootstraps/webview/build/templates/WebViewLoader.tmpl.java b/pythonforandroid/bootstraps/webview/build/templates/WebViewLoader.tmpl.java
new file mode 100644
index 0000000000..df6578bdee
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/templates/WebViewLoader.tmpl.java
@@ -0,0 +1,59 @@
+package org.kivy.android;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.InetSocketAddress;
+
+import android.os.SystemClock;
+
+import android.os.Handler;
+
+import org.kivy.android.PythonActivity;
+
+public class WebViewLoader {
+ private static final String TAG = "WebViewLoader";
+
+ public static void testConnection() {
+
+ while (true) {
+ if (WebViewLoader.pingHost("localhost", {{ args.port }}, 100)) {
+ Log.v(TAG, "Successfully pinged localhost:{{ args.port }}");
+ Handler mainHandler = new Handler(PythonActivity.mActivity.getMainLooper());
+
+ Runnable myRunnable = new Runnable() {
+ @Override
+ public void run() {
+ PythonActivity.mActivity.mWebView.loadUrl("http://127.0.0.1:{{ args.port }}/");
+ Log.v(TAG, "Loaded webserver in webview");
+ }
+ };
+ mainHandler.post(myRunnable);
+ break;
+
+ } else {
+ Log.v(TAG, "Could not ping localhost:{{ args.port }}");
+ try {
+ Thread.sleep(100);
+ } catch(InterruptedException e) {
+ Log.v(TAG, "InterruptedException occurred when sleeping");
+ }
+ }
+ }
+ }
+
+ public static boolean pingHost(String host, int port, int timeout) {
+ Socket socket = new Socket();
+ try {
+ socket.connect(new InetSocketAddress(host, port), timeout);
+ socket.close();
+ return true;
+ } catch (IOException e) {
+ try {socket.close();} catch (IOException f) {return false;}
+ return false; // Either timeout or unreachable or failed DNS lookup.
+ }
+ }
+}
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/templates/build.tmpl.xml b/pythonforandroid/bootstraps/webview/build/templates/build.tmpl.xml
new file mode 100644
index 0000000000..9ab301ad94
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/templates/build.tmpl.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/templates/custom_rules.tmpl.xml b/pythonforandroid/bootstraps/webview/build/templates/custom_rules.tmpl.xml
new file mode 100644
index 0000000000..8b2f60c7e1
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/templates/custom_rules.tmpl.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ {% for dir, includes in args.extra_source_dirs %}
+
+ {% endfor %}
+
+
+
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/templates/kivy-icon.png b/pythonforandroid/bootstraps/webview/build/templates/kivy-icon.png
new file mode 100644
index 0000000000..59a00ba6ff
Binary files /dev/null and b/pythonforandroid/bootstraps/webview/build/templates/kivy-icon.png differ
diff --git a/pythonforandroid/bootstraps/webview/build/templates/kivy-presplash.jpg b/pythonforandroid/bootstraps/webview/build/templates/kivy-presplash.jpg
new file mode 100644
index 0000000000..161ebc0928
Binary files /dev/null and b/pythonforandroid/bootstraps/webview/build/templates/kivy-presplash.jpg differ
diff --git a/pythonforandroid/bootstraps/webview/build/templates/strings.tmpl.xml b/pythonforandroid/bootstraps/webview/build/templates/strings.tmpl.xml
new file mode 100644
index 0000000000..0bbeb192f7
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/templates/strings.tmpl.xml
@@ -0,0 +1,5 @@
+
+
+ {{ args.name }}
+ 0.1
+
diff --git a/pythonforandroid/bootstraps/webview/build/templates/test/build.tmpl.xml b/pythonforandroid/bootstraps/webview/build/templates/test/build.tmpl.xml
new file mode 100644
index 0000000000..9564aae306
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/templates/test/build.tmpl.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/templates/test/build.xml.tmpl b/pythonforandroid/bootstraps/webview/build/templates/test/build.xml.tmpl
new file mode 100644
index 0000000000..9564aae306
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/templates/test/build.xml.tmpl
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/webview_includes/_load.html b/pythonforandroid/bootstraps/webview/build/webview_includes/_load.html
new file mode 100644
index 0000000000..fbbeda0617
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/webview_includes/_load.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+ Python WebView loader
+
+
+
+
+
+
+
+
Loading...
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pythonforandroid/bootstraps/webview/build/whitelist.txt b/pythonforandroid/bootstraps/webview/build/whitelist.txt
new file mode 100644
index 0000000000..41b06ee258
--- /dev/null
+++ b/pythonforandroid/bootstraps/webview/build/whitelist.txt
@@ -0,0 +1 @@
+# put files here that you need to un-blacklist
diff --git a/pythonforandroid/recipes/flask/__init__.py b/pythonforandroid/recipes/flask/__init__.py
new file mode 100644
index 0000000000..d1cf069017
--- /dev/null
+++ b/pythonforandroid/recipes/flask/__init__.py
@@ -0,0 +1,20 @@
+
+from pythonforandroid.toolchain import PythonRecipe, shprint
+import sh
+
+
+class FlaskRecipe(PythonRecipe):
+ version = '0.10.1' # The webserver of 'master' seems to fail
+ # after a little while on Android, so use
+ # 0.10.1 at least for now
+ url = 'https://github.com/pallets/flask/archive/{version}.zip'
+
+ depends = [('python2', 'python3'), 'setuptools']
+
+ python_depends = ['jinja2', 'werkzeug', 'markupsafe', 'itsdangerous', 'click']
+
+ call_hostpython_via_targetpython = False
+ install_in_hostpython = False
+
+
+recipe = FlaskRecipe()
diff --git a/pythonforandroid/recipes/pyjnius/__init__.py b/pythonforandroid/recipes/pyjnius/__init__.py
index 03a82ef752..ee4ba699bd 100644
--- a/pythonforandroid/recipes/pyjnius/__init__.py
+++ b/pythonforandroid/recipes/pyjnius/__init__.py
@@ -9,10 +9,11 @@ class PyjniusRecipe(CythonRecipe):
version = 'master'
url = 'https://github.com/kivy/pyjnius/archive/{version}.zip'
name = 'pyjnius'
- depends = [('python2', 'python3crystax'), ('sdl2', 'sdl'), 'six']
+ depends = [('python2', 'python3crystax'), ('sdl2', 'sdl', 'webviewjni'), 'six']
site_packages_name = 'jnius'
- patches = [('sdl2_jnienv_getter.patch', will_build('sdl2'))]
+ patches = [('sdl2_jnienv_getter.patch', will_build('sdl2')),
+ ('webviewjni_jnienv_getter.patch', will_build('webviewjni'))]
def postbuild_arch(self, arch):
super(PyjniusRecipe, self).postbuild_arch(arch)
diff --git a/pythonforandroid/recipes/pyjnius/webviewjni_jnienv_getter.patch b/pythonforandroid/recipes/pyjnius/webviewjni_jnienv_getter.patch
new file mode 100644
index 0000000000..50c62cb395
--- /dev/null
+++ b/pythonforandroid/recipes/pyjnius/webviewjni_jnienv_getter.patch
@@ -0,0 +1,25 @@
+diff --git a/jnius/jnius_jvm_android.pxi b/jnius/jnius_jvm_android.pxi
+index ac89fec..71daa43 100644
+--- a/jnius/jnius_jvm_android.pxi
++++ b/jnius/jnius_jvm_android.pxi
+@@ -1,5 +1,5 @@
+ # on android, rely on SDL to get the JNI env
+-cdef extern JNIEnv *SDL_ANDROID_GetJNIEnv()
++cdef extern JNIEnv *WebView_AndroidGetJNIEnv()
+
+ cdef JNIEnv *get_platform_jnienv():
+- return SDL_ANDROID_GetJNIEnv()
++ return WebView_AndroidGetJNIEnv()
+diff --git a/setup.py b/setup.py
+index 740510f..0c8e55f 100644
+--- a/setup.py
++++ b/setup.py
+@@ -53,7 +53,7 @@ except ImportError:
+
+ if platform == 'android':
+ # for android, we use SDL...
+- libraries = ['sdl', 'log']
++ libraries = ['main', 'log']
+ library_dirs = ['libs/' + getenv('ARCH')]
+ elif platform == 'darwin':
+ import subprocess
diff --git a/pythonforandroid/recipes/webviewjni/__init__.py b/pythonforandroid/recipes/webviewjni/__init__.py
new file mode 100644
index 0000000000..af3930f871
--- /dev/null
+++ b/pythonforandroid/recipes/webviewjni/__init__.py
@@ -0,0 +1,31 @@
+from pythonforandroid.toolchain import BootstrapNDKRecipe, shprint, current_directory, info
+from os.path import exists, join
+import sh
+
+
+class WebViewJNIRecipe(BootstrapNDKRecipe):
+ version = None
+ url = None
+
+ depends = [('python2', 'python3crystax')]
+ conflicts = ['sdl2', 'pygame', 'sdl']
+
+ def should_build(self, arch):
+ return True
+
+ def get_recipe_env(self, arch=None):
+ env = super(WebViewJNIRecipe, self).get_recipe_env(arch)
+ py2 = self.get_recipe('python2', arch.ctx)
+ env['PYTHON2_NAME'] = py2.get_dir_name()
+ if 'python2' in self.ctx.recipe_build_order:
+ env['EXTRA_LDLIBS'] = ' -lpython2.7'
+ return env
+
+ def build_arch(self, arch):
+ env = self.get_recipe_env(arch)
+
+ with current_directory(self.get_jni_dir()):
+ shprint(sh.ndk_build, "V=1", _env=env)
+
+
+recipe = WebViewJNIRecipe()
diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py
index 7c31c47f8b..8f8579fca9 100755
--- a/pythonforandroid/toolchain.py
+++ b/pythonforandroid/toolchain.py
@@ -128,6 +128,7 @@ def build_dist_from_args(ctx, dist, args):
build_order, python_modules, bs \
= get_recipe_order_and_bootstrap(ctx, dist.recipes, bs)
ctx.recipe_build_order = build_order
+ ctx.python_modules = python_modules
info('The selected bootstrap is {}'.format(bs.name))
info_main('# Creating dist with {} bootstrap'.format(bs.name))
diff --git a/testapps/testapp_flask/main.py b/testapps/testapp_flask/main.py
new file mode 100644
index 0000000000..cf0c69c83f
--- /dev/null
+++ b/testapps/testapp_flask/main.py
@@ -0,0 +1,87 @@
+print('main.py was successfully called')
+print('this is the new main.py')
+
+import os
+print('imported os')
+
+try:
+ print('contents of ./lib/python2.7/site-packages/ etc.')
+ print(os.listdir('./lib'))
+ print(os.listdir('./lib/python2.7'))
+ print(os.listdir('./lib/python2.7/site-packages'))
+except OSError:
+ print('could not look in dirs')
+ print('this is expected on desktop')
+
+import flask
+print('flask1???')
+
+print('contents of this dir', os.listdir('./'))
+
+import flask
+print('flask???')
+
+import sys
+print('pythonpath is', sys.path)
+
+
+from flask import Flask
+app = Flask(__name__)
+
+from flask import (Flask, url_for, render_template, request, redirect,
+ flash)
+
+print('imported flask etc')
+print('importing pyjnius')
+
+from jnius import autoclass
+Context = autoclass('android.content.Context')
+PythonActivity = autoclass('org.kivy.android.PythonActivity')
+activity = PythonActivity.mActivity
+
+vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE)
+
+ActivityInfo = autoclass('android.content.pm.ActivityInfo')
+
+@app.route('/')
+def page1():
+ return render_template('index.html')
+
+@app.route('/page2')
+def page2():
+ return render_template('page2.html')
+
+@app.route('/vibrate')
+def vibrate():
+ args = request.args
+ if 'time' not in args:
+ print('ERROR: asked to vibrate but without time argument')
+ print('asked to vibrate', args['time'])
+
+ vibrator.vibrate(float(args['time']) * 1000)
+ print('vibrated')
+
+@app.route('/orientation')
+def orientation():
+ args = request.args
+ if 'dir' not in args:
+ print('ERROR: asked to orient but no dir specified')
+ direction = args['dir']
+ if direction not in ('horizontal', 'vertical'):
+ print('ERROR: asked to orient to neither horizontal nor vertical')
+
+ if direction == 'horizontal':
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+ else:
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
+
+
+from os import curdir
+from os.path import realpath
+print('curdir', realpath(curdir))
+if realpath(curdir).startswith('/data'):
+ app.run(debug=False)
+else:
+ app.run(debug=True)
diff --git a/testapps/testapp_flask/static/colours.png b/testapps/testapp_flask/static/colours.png
new file mode 100644
index 0000000000..30b685e32b
Binary files /dev/null and b/testapps/testapp_flask/static/colours.png differ
diff --git a/testapps/testapp_flask/static/coloursinv.png b/testapps/testapp_flask/static/coloursinv.png
new file mode 100644
index 0000000000..fe7e79376d
Binary files /dev/null and b/testapps/testapp_flask/static/coloursinv.png differ
diff --git a/testapps/testapp_flask/static/style.css b/testapps/testapp_flask/static/style.css
new file mode 100644
index 0000000000..68338551d0
--- /dev/null
+++ b/testapps/testapp_flask/static/style.css
@@ -0,0 +1,78 @@
+
+h1 {
+ font-size: 30px;
+ color: blue;
+ font-weight: bold;
+ text-align:center;
+}
+
+h2 {
+ text-align:center;
+}
+
+button {
+ margin-left: auto;
+ margin-right: auto;
+ display: block;
+ margin-top: 50px;
+ font-size: 30px;
+}
+
+
+/* Loader from http://projects.lukehaas.me/css-loaders/#load1 */
+
+.loader,
+.loader:before,
+.loader:after {
+ background: #aaaaff;
+ -webkit-animation: load1 1s infinite ease-in-out;
+ animation: load1 1s infinite ease-in-out;
+ width: 1em;
+ height: 4em;
+}
+.loader:before,
+.loader:after {
+ position: absolute;
+ top: 0;
+ content: '';
+}
+.loader:before {
+ left: -1.5em;
+}
+.loader {
+ text-indent: -9999em;
+ margin: 8em auto;
+ position: relative;
+ font-size: 11px;
+ -webkit-animation-delay: -0.16s;
+ animation-delay: -0.16s;
+}
+.loader:after {
+ left: 1.5em;
+ -webkit-animation-delay: -0.32s;
+ animation-delay: -0.32s;
+}
+@-webkit-keyframes load1 {
+ 0%,
+ 80%,
+ 100% {
+ box-shadow: 0 0 #aaaaff;
+ height: 4em;
+ }
+ 40% {
+ box-shadow: 0 -2em #aaaaff;
+ height: 5em;
+ }
+}
+@keyframes load1 {
+ 0%,
+ 80%,
+ 100% {
+ box-shadow: 0 0 #aaaaff;
+ height: 4em;
+ }
+ 40% {
+ box-shadow: 0 -2em #aaaaff;
+ height: 5em;
+ }
+}
diff --git a/testapps/testapp_flask/templates/base.html b/testapps/testapp_flask/templates/base.html
new file mode 100644
index 0000000000..63b4881302
--- /dev/null
+++ b/testapps/testapp_flask/templates/base.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ {% block title %}
+ Flask on Android
+ {% endblock %}
+
+
+
+
+
+
+