diff --git a/buildozer.spec.sample b/buildozer.spec.sample
index 9c5ecf6f..9101dad0 100644
--- a/buildozer.spec.sample
+++ b/buildozer.spec.sample
@@ -36,7 +36,7 @@ version = 0.1
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
-requirements = openssl, sqlite3, hostpython2, pycrypto==2.6.1, android, pyjnius, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pycrypto==2.6.1, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, txrequests==0.9.5, seccure==0.3.1.3, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, git+https://github.com/lbryio/lbryschema.git#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbry
+requirements = openssl, sqlite3, hostpython2, pycrypto==2.6.1, android, pyjnius, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pycrypto==2.6.1, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, txrequests==0.9.5, seccure==0.3.1.3, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, git+https://github.com/lbryio/lbryschema.git@v0.0.12rc1#egg=lbryschema, git+https://github.com/lbryio/lbryum.git@v3.1.9rc2#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.0.3, funcsigs, mock, pbr, unqlite
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
diff --git a/buildozer.spec.travis b/buildozer.spec.travis
index d4abac05..722821ed 100644
--- a/buildozer.spec.travis
+++ b/buildozer.spec.travis
@@ -36,7 +36,7 @@ version = 0.1
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
-requirements = openssl, sqlite3, hostpython2, pycrypto==2.6.1, android, pyjnius, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pycrypto==2.6.1, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, txrequests==0.9.5, seccure==0.3.1.3, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, git+https://github.com/lbryio/lbryschema.git#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbry
+requirements = openssl, sqlite3, hostpython2, pycrypto==2.6.1, android, pyjnius, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pycrypto==2.6.1, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, txrequests==0.9.5, seccure==0.3.1.3, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, git+https://github.com/lbryio/lbryschema.git@v0.0.12rc1#egg=lbryschema, git+https://github.com/lbryio/lbryum.git@v3.1.9rc2#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.0.3, funcsigs, mock, pbr, unqlite
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/blacklist.txt b/p4a/pythonforandroid/bootstraps/lbry/build/blacklist.txt
index 3d596e44..72ebeafd 100644
--- a/p4a/pythonforandroid/bootstraps/lbry/build/blacklist.txt
+++ b/p4a/pythonforandroid/bootstraps/lbry/build/blacklist.txt
@@ -6,7 +6,7 @@
*.egg-info
# unit test
-unittest/*
+#unittest/*
# python config
config/makesetup
diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/templates/AndroidManifest.tmpl.xml b/p4a/pythonforandroid/bootstraps/lbry/build/templates/AndroidManifest.tmpl.xml
index d0a66605..f5c35e8b 100644
--- a/p4a/pythonforandroid/bootstraps/lbry/build/templates/AndroidManifest.tmpl.xml
+++ b/p4a/pythonforandroid/bootstraps/lbry/build/templates/AndroidManifest.tmpl.xml
@@ -118,6 +118,8 @@
{% endfor %}
+
{% if args.billing_pubkey %}
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/templates/strings.tmpl.xml b/p4a/pythonforandroid/bootstraps/lbry/build/templates/strings.tmpl.xml
index b56a6fbb..04d36941 100644
--- a/p4a/pythonforandroid/bootstraps/lbry/build/templates/strings.tmpl.xml
+++ b/p4a/pythonforandroid/bootstraps/lbry/build/templates/strings.tmpl.xml
@@ -10,4 +10,7 @@
Stopped
START
STOP
+
+ Unit tests
+ RUN
diff --git a/recipes/cffi/__init__.py b/recipes/cffi/__init__.py
new file mode 100644
index 00000000..0fc6018d
--- /dev/null
+++ b/recipes/cffi/__init__.py
@@ -0,0 +1,36 @@
+from pythonforandroid.recipe import CompiledComponentsPythonRecipe, Recipe
+from os.path import join
+
+
+class CffiRecipe(CompiledComponentsPythonRecipe):
+ name = 'cffi'
+ version = '1.11.0'
+ #url = 'https://pypi.python.org/packages/source/c/cffi/cffi-{version}.tar.gz'
+ url = 'https://pypi.python.org/packages/4e/32/4070bdf32812c89eb635c80880a5caa2e0189aa7999994c265577e5154f3/cffi-{version}.tar.gz'
+
+ depends = [('python2', 'python3crystax'), 'setuptools', 'pycparser', 'libffi']
+
+ patches = ['disable-pkg-config.patch']
+
+ # call_hostpython_via_targetpython = False
+ install_in_hostpython = True
+
+ def get_recipe_env(self, arch=None):
+ env = super(CffiRecipe, self).get_recipe_env(arch)
+ libffi = self.get_recipe('libffi', self.ctx)
+ includes = libffi.get_include_dirs(arch)
+ env['CFLAGS'] = ' -I'.join([env.get('CFLAGS', '')] + includes)
+ env['LDFLAGS'] = (env.get('CFLAGS', '') + ' -L' +
+ self.ctx.get_libs_dir(arch.arch))
+ env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions'
+ env['PYTHONPATH'] = ':'.join([self.ctx.get_site_packages_dir(), env['BUILDLIB_PATH']])
+
+ target_python = Recipe.get_recipe('python2', self.ctx).get_build_dir(arch.arch)
+ env['PYTHON_ROOT'] = join(target_python, 'python-install')
+ env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7'
+ env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + ' -lpython2.7'
+
+ return env
+
+
+recipe = CffiRecipe()
diff --git a/recipes/cffi/disable-pkg-config.patch b/recipes/cffi/disable-pkg-config.patch
new file mode 100644
index 00000000..56346bb7
--- /dev/null
+++ b/recipes/cffi/disable-pkg-config.patch
@@ -0,0 +1,29 @@
+diff -Naur cffi-1.4.2/setup.py b/setup.py
+--- cffi-1.4.2/setup.py 2015-12-21 12:09:47.000000000 -0600
++++ b/setup.py 2015-12-23 10:20:40.590622524 -0600
+@@ -5,8 +5,7 @@
+
+ sources = ['c/_cffi_backend.c']
+ libraries = ['ffi']
+-include_dirs = ['/usr/include/ffi',
+- '/usr/include/libffi'] # may be changed by pkg-config
++include_dirs = []
+ define_macros = []
+ library_dirs = []
+ extra_compile_args = []
+@@ -67,14 +66,7 @@
+ sys.stderr.write("The above error message can be safely ignored\n")
+
+ def use_pkg_config():
+- if sys.platform == 'darwin' and os.path.exists('/usr/local/bin/brew'):
+- use_homebrew_for_libffi()
+-
+- _ask_pkg_config(include_dirs, '--cflags-only-I', '-I', sysroot=True)
+- _ask_pkg_config(extra_compile_args, '--cflags-only-other')
+- _ask_pkg_config(library_dirs, '--libs-only-L', '-L', sysroot=True)
+- _ask_pkg_config(extra_link_args, '--libs-only-other')
+- _ask_pkg_config(libraries, '--libs-only-l', '-l')
++ pass
+
+ def use_homebrew_for_libffi():
+ # We can build by setting:
diff --git a/recipes/cryptography/__init__.py b/recipes/cryptography/__init__.py
new file mode 100644
index 00000000..e3f77aca
--- /dev/null
+++ b/recipes/cryptography/__init__.py
@@ -0,0 +1,30 @@
+from pythonforandroid.recipe import CompiledComponentsPythonRecipe, Recipe
+from os.path import dirname, join
+
+class CryptographyRecipe(CompiledComponentsPythonRecipe):
+ name = 'cryptography'
+ version = '2.0.3'
+ url = 'https://github.com/pyca/cryptography/archive/{version}.tar.gz'
+ depends = [('python2', 'python3crystax'), 'openssl', 'idna', 'pyasn1', 'six', 'setuptools', 'enum34', 'ipaddress', 'cffi']
+ call_hostpython_via_targetpython = False
+
+ def get_recipe_env(self, arch):
+ env = super(CryptographyRecipe, self).get_recipe_env(arch)
+ r = self.get_recipe('openssl', self.ctx)
+ openssl_dir = r.get_build_dir(arch.arch)
+ target_python = Recipe.get_recipe('python2', self.ctx).get_build_dir(arch.arch)
+ env['PYTHON_ROOT'] = self.ctx.get_python_install_dir()
+ env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' + \
+ ' -I' + join(openssl_dir, 'include') + \
+ ' -I' + join(target_python, 'Modules/_ctypes/libffi_arm_wince')
+ # Set linker to use the correct gcc
+ env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions'
+ env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + \
+ ' -L' + openssl_dir + \
+ ' -lpython2.7' + \
+ ' -lssl' + r.version + \
+ ' -lcrypto' + r.version
+
+ return env
+
+recipe = CryptographyRecipe()
diff --git a/recipes/ipaddress/__init__.py b/recipes/ipaddress/__init__.py
new file mode 100644
index 00000000..12a042a1
--- /dev/null
+++ b/recipes/ipaddress/__init__.py
@@ -0,0 +1,14 @@
+from pythonforandroid.recipe import PythonRecipe
+
+
+class IpaddressRecipe(PythonRecipe):
+ name = 'ipaddress'
+ version = '1.0.16'
+ url = 'https://pypi.python.org/packages/source/i/ipaddress/ipaddress-{version}.tar.gz'
+
+ depends = ['python2']
+
+ call_hostpython_via_targetpython = False
+
+
+recipe = IpaddressRecipe()
diff --git a/recipes/pyasn1/__init__.py b/recipes/pyasn1/__init__.py
new file mode 100644
index 00000000..3edfc052
--- /dev/null
+++ b/recipes/pyasn1/__init__.py
@@ -0,0 +1,11 @@
+
+from pythonforandroid.toolchain import PythonRecipe
+
+
+class PyASN1Recipe(PythonRecipe):
+ version = '0.1.8'
+ url = 'https://pypi.python.org/packages/source/p/pyasn1/pyasn1-{version}.tar.gz'
+ depends = ['python2']
+ call_hostpython_via_targetpython = False
+
+recipe = PyASN1Recipe()
diff --git a/recipes/unqlite/__init__.py b/recipes/unqlite/__init__.py
new file mode 100644
index 00000000..a47be77a
--- /dev/null
+++ b/recipes/unqlite/__init__.py
@@ -0,0 +1,32 @@
+import glob
+from pythonforandroid.toolchain import (
+ CythonRecipe,
+ Recipe,
+ current_directory,
+ info,
+ shprint,
+)
+from os.path import join
+import sh
+
+
+class UnqliteRecipe(CythonRecipe):
+ version = '0.6.0'
+ url = 'https://pypi.python.org/packages/cb/4e/e1f64a3d0f6462167805940b4c72f47bafc1129e363fc4c0f79a1cdc5dd1/unqlite-{version}.tar.gz'
+ depends = ['python2', 'setuptools']
+ call_hostpython_via_targetpython = False
+
+ patches = ['setup.patch']
+
+ def get_recipe_env(self, arch):
+ env = super(UnqliteRecipe, self).get_recipe_env(arch)
+
+ target_python = Recipe.get_recipe('python2', self.ctx).get_build_dir(arch.arch)
+ env['PYTHON_ROOT'] = join(target_python, 'python-install')
+ env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7'
+ env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + ' -lpython2.7'
+
+ return env
+
+
+recipe = UnqliteRecipe()
diff --git a/recipes/unqlite/setup.patch b/recipes/unqlite/setup.patch
new file mode 100644
index 00000000..69c865d0
--- /dev/null
+++ b/recipes/unqlite/setup.patch
@@ -0,0 +1,26 @@
+--- a/setup.py 2016-08-22 19:45:52.000000000 +0100
++++ b/setup.py 2017-09-16 01:35:36.675202502 +0100
+@@ -1,11 +1,7 @@
+ import glob
+-from distutils.core import setup, Extension
+-
+-try:
+- from Cython.Build import cythonize
+-except ImportError:
+- raise RuntimeError('Cython must be installed to build unqlite-python.')
+-
++from setuptools import setup
++from setuptools.extension import Extension
++from setuptools.command.build_ext import build_ext
+
+ python_source = 'unqlite.pyx'
+ library_source = glob.glob('src/*.c')
+@@ -45,5 +41,7 @@
+ 'Topic :: Database :: Database Engines/Servers',
+ 'Topic :: Software Development :: Embedded Systems',
+ 'Topic :: Software Development :: Libraries :: Python Modules'],
+- ext_modules=cythonize(unqlite_extension)
++ cmdclass = {'build_ext': build_ext},
++ ext_modules=[unqlite_extension]
+ )
++
diff --git a/src/main/java/io/lbry/lbrynet/LbrynetService.java b/src/main/java/io/lbry/lbrynet/LbrynetService.java
index c3efb17a..af11a2d0 100644
--- a/src/main/java/io/lbry/lbrynet/LbrynetService.java
+++ b/src/main/java/io/lbry/lbrynet/LbrynetService.java
@@ -1,19 +1,20 @@
package io.lbry.lbrynet;
import android.app.Activity;
-import android.content.Intent;
-import android.content.Context;
import android.app.Notification;
import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.Context;
+import android.os.Bundle;
import android.util.Log;
-import java.io.InputStream;
+
+import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
-import java.io.File;
-import android.os.Bundle;
+import java.io.InputStream;
+
import org.kivy.android.PythonService;
-import org.kivy.android.PythonActivity;
import org.renpy.android.AssetExtract;
import org.renpy.android.ResourceManager;
@@ -51,7 +52,8 @@ public class LbrynetService extends PythonService {
unpackData("private", app_root_file);
if (intent == null) {
- intent = buildIntent(getApplicationContext(), "");
+ intent = ServiceHelper.buildIntent(
+ getApplicationContext(), "", LbrynetService.class, "lbrynetservice");
}
return super.onStartCommand(intent, flags, startId);
@@ -134,28 +136,4 @@ public class LbrynetService extends PythonService {
}
}
}
-
- public static Intent buildIntent(Context ctx, String pythonServiceArgument) {
- Intent intent = new Intent(ctx, LbrynetService.class);
- String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
- intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
- intent.putExtra("androidArgument", argument);
- intent.putExtra("serviceEntrypoint", "./lbrynetservice.py");
- intent.putExtra("pythonName", "lbrynetservice");
- intent.putExtra("pythonHome", argument);
- intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
- intent.putExtra("pythonServiceArgument", pythonServiceArgument);
-
- return intent;
- }
-
- public static void start(Context ctx, String pythonServiceArgument) {
- Intent intent = buildIntent(ctx, pythonServiceArgument);
- ctx.startService(intent);
- }
-
- public static void stop(Context ctx) {
- Intent intent = new Intent(ctx, LbrynetService.class);
- ctx.stopService(intent);
- }
}
diff --git a/src/main/java/io/lbry/lbrynet/LbrynetTestRunnerService.java b/src/main/java/io/lbry/lbrynet/LbrynetTestRunnerService.java
new file mode 100644
index 00000000..df5b015b
--- /dev/null
+++ b/src/main/java/io/lbry/lbrynet/LbrynetTestRunnerService.java
@@ -0,0 +1,126 @@
+package io.lbry.lbrynet;
+
+import android.content.Intent;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.InputStream;
+
+import org.kivy.android.PythonService;
+import org.renpy.android.AssetExtract;
+import org.renpy.android.ResourceManager;
+
+public class LbrynetTestRunnerService extends PythonService {
+
+ public static String TAG = "LbrynetTestRunnerService";
+
+ public static LbrynetTestRunnerService serviceInstance;
+
+ @Override
+ public boolean canDisplayNotification() {
+ return false;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // Extract files
+ File app_root_file = new File(getAppRoot());
+ unpackData("private", app_root_file);
+
+ if (intent == null) {
+ intent = ServiceHelper.buildIntent(
+ getApplicationContext(), "", LbrynetTestRunnerService.class, "testrunnerservice");
+ }
+
+ serviceInstance = this;
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ public void onDestroy() {
+ serviceInstance = null;
+ super.onDestroy();
+ }
+
+ public void broadcastTestRunnerOutput(String output) {
+ Intent intent = new Intent();
+ intent.setAction(ServiceControlActivity.TEST_RUNNER_OUTPUT);
+ intent.putExtra("output", output);
+ sendBroadcast(intent);
+ }
+
+ public void unpackData(final String resource, File target) {
+ Log.v(TAG, "UNPACKING!!! " + resource + " " + target.getName());
+
+ // The version of data in memory and on disk.
+ ResourceManager resourceManager = new ResourceManager(getApplicationContext());
+ 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(getApplicationContext());
+ if (!ae.extractTar(resource + ".mp3", target.getAbsolutePath())) {
+ //toastError("Could not extract " + resource + " data.");
+ Log.e(TAG, "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 void recursiveDelete(File f) {
+ if (f.isDirectory()) {
+ for (File r : f.listFiles()) {
+ recursiveDelete(r);
+ }
+ }
+ f.delete();
+ }
+
+ public String getAppRoot() {
+ String app_root = getApplicationContext().getFilesDir().getAbsolutePath() + "/app";
+ return app_root;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/lbry/lbrynet/ServiceControlActivity.java b/src/main/java/io/lbry/lbrynet/ServiceControlActivity.java
index 3e06f3a6..cf2c2a17 100644
--- a/src/main/java/io/lbry/lbrynet/ServiceControlActivity.java
+++ b/src/main/java/io/lbry/lbrynet/ServiceControlActivity.java
@@ -1,18 +1,34 @@
package io.lbry.lbrynet;
-import org.kivy.android.PythonActivity;
import android.app.Activity;
import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
import android.content.Context;
-import android.os.Handler;
-//import android.support.v7.app.AppCompatActivity;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
import android.os.Bundle;
+import android.os.Handler;
+import android.text.Html;
+import android.text.Spanned;
+import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
public class ServiceControlActivity extends Activity {
+ public static ServiceControlActivity activityInstance;
+
+ private IntentFilter intentFilter;
+
+ public static String TEST_RUNNER_OUTPUT = "io.lbry.lbrynet.TEST_RUNNER_OUTPUT";
+
/**
* Flag which indicates whether or not the service is running. Will be updated in the
* onResume method.
@@ -24,6 +40,10 @@ public class ServiceControlActivity extends Activity {
*/
private Button startStopButton;
+ private Button runTestsButton;
+
+ private TextView testRunnerOutput;
+
/**
* Service status text.
*/
@@ -33,32 +53,69 @@ public class ServiceControlActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_service_control);
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(TEST_RUNNER_OUTPUT);
startStopButton = (Button) findViewById(R.id.btn_start_stop);
+ runTestsButton = (Button) findViewById(R.id.btn_run_tests);
serviceStatusText = (TextView) findViewById(R.id.text_service_status);
+ testRunnerOutput = (TextView) findViewById(R.id.test_runner_output);
startStopButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (serviceRunning) {
- LbrynetService.stop(ServiceControlActivity.this);
+ ServiceHelper.stop(ServiceControlActivity.this, LbrynetService.class);
} else {
- LbrynetService.start(ServiceControlActivity.this, "");
+ ServiceHelper.start(ServiceControlActivity.this, "", LbrynetService.class, "lbrynetservice");
}
serviceRunning = isServiceRunning(LbrynetService.class);
updateServiceStatus();
}
});
+
+ runTestsButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ boolean testsRunning = isServiceRunning(LbrynetTestRunnerService.class);
+ if (!testsRunning) {
+ ServiceHelper.start(
+ ServiceControlActivity.this, "", LbrynetTestRunnerService.class, "testrunnerservice");
+ }
+ }
+ });
}
@Override
public void onResume() {
super.onResume();
+ registerReceiver(testRunnerOutputReceiver, intentFilter);
+
+ activityInstance = this;
serviceRunning = isServiceRunning(LbrynetService.class);
updateServiceStatus();
}
+ private BroadcastReceiver testRunnerOutputReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (TEST_RUNNER_OUTPUT.equals(intent.getAction())) {
+ String output = intent.getStringExtra("output");
+ updateTestRunnerOutput(output);
+ }
+ }
+ };
+
+ @Override
+ public void onPause() {
+ unregisterReceiver(testRunnerOutputReceiver);
+ // set the activity instance to null on pause in order to prevent NullPointerException
+ // if the activity shuts down prematurely, for example
+ activityInstance = null;
+ super.onPause();
+ }
+
public void updateServiceStatus() {
new Handler().post(new Runnable() {
@Override
@@ -76,6 +133,10 @@ public class ServiceControlActivity extends Activity {
});
}
+ public void updateTestRunnerOutput(String output) {
+ testRunnerOutput.setText(formatTestRunnerOutput(output));
+ }
+
private boolean isServiceRunning(Class> serviceClass) {
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo serviceInfo : manager.getRunningServices(Integer.MAX_VALUE)) {
@@ -86,4 +147,12 @@ public class ServiceControlActivity extends Activity {
return false;
}
+
+ public static Spanned formatTestRunnerOutput(String output) {
+ output = output.replace("[OK]", "[OK]");
+ output = output.replace("[ERROR]", "[ERROR]");
+ output = output.replace("[FAILURE]", "[FAILURE]");
+
+ return Html.fromHtml(output);
+ }
}
diff --git a/src/main/java/io/lbry/lbrynet/ServiceHelper.java b/src/main/java/io/lbry/lbrynet/ServiceHelper.java
new file mode 100644
index 00000000..4c30cae9
--- /dev/null
+++ b/src/main/java/io/lbry/lbrynet/ServiceHelper.java
@@ -0,0 +1,30 @@
+package io.lbry.lbrynet;
+
+import android.content.Intent;
+import android.content.Context;
+
+public class ServiceHelper {
+ public static Intent buildIntent(Context ctx, String pythonServiceArgument, Class serviceClass, String pythonName) {
+ Intent intent = new Intent(ctx, serviceClass);
+ String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
+ intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
+ intent.putExtra("androidArgument", argument);
+ intent.putExtra("serviceEntrypoint", "./" + pythonName + ".py");
+ intent.putExtra("pythonName", pythonName);
+ intent.putExtra("pythonHome", argument);
+ intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
+ intent.putExtra("pythonServiceArgument", pythonServiceArgument);
+
+ return intent;
+ }
+
+ public static void start(Context ctx, String pythonServiceArgument, Class serviceClass, String pythonName) {
+ Intent intent = buildIntent(ctx, pythonServiceArgument, serviceClass, pythonName);
+ ctx.startService(intent);
+ }
+
+ public static void stop(Context ctx, Class serviceClass) {
+ Intent intent = new Intent(ctx, serviceClass);
+ ctx.stopService(intent);
+ }
+}
\ No newline at end of file
diff --git a/src/main/python/lbrynetservice.py b/src/main/python/lbrynetservice.py
index 1e51c37e..fa0673c4 100644
--- a/src/main/python/lbrynetservice.py
+++ b/src/main/python/lbrynetservice.py
@@ -54,7 +54,8 @@ lbrynet.daemon.auth.util.save_api_keys = save_api_keys
lbrynet.daemon.auth.util.initialize_api_key_file = initialize_api_key_file
import logging.handlers
-
+if __name__ == '__main__':
+ ServiceApp().run()
from lbrynet.core import log_support
from twisted.internet import defer, reactor
from jsonrpc.proxy import JSONRPCProxy
diff --git a/src/main/python/testrunnerservice.py b/src/main/python/testrunnerservice.py
new file mode 100644
index 00000000..80403749
--- /dev/null
+++ b/src/main/python/testrunnerservice.py
@@ -0,0 +1,76 @@
+import sys
+import StringIO
+
+from twisted.trial.runner import (
+ TestLoader,
+ TrialRunner
+)
+from twisted.trial.reporter import TreeReporter
+from twisted.plugin import getPlugins, IPlugin
+from jnius import autoclass
+import lbrynet.tests
+from os import listdir
+
+str_stream = StringIO.StringIO()
+
+serviceClass = autoclass('io.lbry.lbrynet.LbrynetTestRunnerService')
+
+def update_output_in_activity(str):
+ service = serviceClass.serviceInstance
+ if service is not None:
+ service.broadcastTestRunnerOutput(str)
+
+class AndroidTestReporter(TreeReporter):
+ def addSuccess(self, test):
+ super(TreeReporter, self).addSuccess(test)
+ self.endLine('[OK]', self.SUCCESS)
+ update_output_in_activity(str_to_basic_html(self._stream.getvalue()))
+
+ def addError(self, *args):
+ super(TreeReporter, self).addError(*args)
+ self.endLine('[ERROR]', self.ERROR)
+ update_output_in_activity(str_to_basic_html(self._stream.getvalue()))
+
+ def addFailure(self, *args):
+ super(TreeReporter, self).addFailure(*args)
+ self.endLine('[FAIL]', self.FAILURE)
+ update_output_in_activity(str_to_basic_html(self._stream.getvalue()))
+
+ def addSkip(self, *args):
+ super(TreeReporter, self).addSkip(*args)
+ self.endLine('[SKIPPED]', self.SKIP)
+ update_output_in_activity(str_to_basic_html(self._stream.getvalue()))
+
+ def addExpectedFailure(self, *args):
+ super(TreeReporter, self).addExpectedFailure(*args)
+ self.endLine('[TODO]', self.TODO)
+ update_output_in_activity(str_to_basic_html(self._stream.getvalue()))
+
+ def addUnexpectedSuccess(self, *args):
+ super(TreeReporter, self).addUnexpectedSuccess(*args)
+ self.endLine('[SUCCESS!?!]', self.TODONE)
+ update_output_in_activity(str_to_basic_html(self._stream.getvalue()))
+
+ def startTest(self, test):
+ super(AndroidTestReporter, self).startTest(test)
+ update_output_in_activity(str_to_basic_html(self._stream.getvalue()))
+
+ def endLine(self, message, color):
+ super(AndroidTestReporter, self).endLine(message, color)
+ update_output_in_activity(str_to_basic_html(self._stream.getvalue()))
+
+def str_to_basic_html(value):
+ return value.replace("\n", "
").replace(" ", ' ')
+
+def run():
+ loader = TestLoader();
+ suite = loader.loadPackage(lbrynet.tests, True)
+ runner = TrialRunner(AndroidTestReporter)
+ runner.stream = str_stream
+ passFail = not runner.run(suite).wasSuccessful()
+
+ print str_stream.getvalue()
+ sys.exit(passFail)
+
+if __name__ == '__main__':
+ run()