diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index f1e15b5a..5311348e 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -1,5 +1,16 @@ steps: - - block: "Build :android:?" - if: build.source == "trigger_job" - - label: "Build Android App" + - block: "Build dev APK?" + # Always run if this is a Kolibri release + if: build.env("LE_KOLIBRI_RELEASE") != "true" + + - label: "Build dev APK" command: ".buildkite/build.sh" + env: KOLIBRI_ANDROID_BUILD_MODE=dev + + - block: "Build release APK?" + # Always run if this is a Kolibri release + if: build.env("LE_KOLIBRI_RELEASE") == "true" + + - label: "Build Release APK :tada:" + command: ".buildkite/build.sh" + env: KOLIBRI_ANDROID_BUILD_MODE=release \ No newline at end of file diff --git a/.gitignore b/.gitignore index a778c3a5..b6dfa32e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ src/kolibri +src/preseeded_kolibri_home +src/extra-packages +tmpenv whl/* # File format for signing key diff --git a/Dockerfile b/Dockerfile index d9c7dcaf..50f70cc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:16.04 as build +FROM ubuntu:bionic as build LABEL maintainer="Learning Equality " tag="kolibrikivy" ENV DEBIAN_FRONTEND noninteractive @@ -14,11 +14,13 @@ RUN dpkg --add-architecture i386 && \ cython \ gcc \ git \ + iproute2 \ libffi-dev \ libltdl-dev\ libncurses5:i386 \ libstdc++6:i386 \ libtool \ + locales \ lsb-release \ openjdk-8-jdk \ python-dev \ @@ -27,12 +29,26 @@ RUN dpkg --add-architecture i386 && \ wget \ xclip \ xsel \ + zip \ zlib1g-dev \ zlib1g:i386 \ python-wxgtk3.0 \ libgtk-3-dev \ + python3 \ && apt-get clean +# Use java 1.8 because Ubuntu's gradle version doesn't support 1.11 +ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 +ENV PATH=$PATH:$JAVA_HOME + +RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ + python3 get-pip.py + +# Ensure that python is using python3 +# copying approach from official python images +ENV PATH /usr/local/bin:$PATH +RUN cd /usr/local/bin && \ + ln -s $(which python3) python # Allows us to invalidate cache if those repos update. # Intentionally not pinning for dev velocity. @@ -40,14 +56,16 @@ ADD https://github.com/kollivier/python-for-android/archive/webview_plus.zip p4a ADD https://github.com/kollivier/pyeverywhere/archive/dev.zip pew.zip # install python dependencies -RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ - python get-pip.py && \ - pip install cython virtualenv && \ +RUN pip install cython virtualenv && \ # get kevin's custom packages - pip install -e git+https://github.com/kollivier/pyeverywhere@dev#egg=pyeverywhere && \ - pip install -e git+https://github.com/kollivier/python-for-android@webview_plus#egg=python-for-android && \ + pip install -e git+https://github.com/kollivier/pyeverywhere@p4a_update#egg=pyeverywhere && \ + pip install -e git+https://github.com/kollivier/python-for-android@pew_webview#egg=python-for-android && \ useradd -lm kivy +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ + locale-gen +ENV LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 + USER kivy:kivy WORKDIR /home/kivy diff --git a/Makefile b/Makefile index 574861a7..003ab3c1 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,28 @@ +# run with `make ... ARCH=64bit` to build for v8a ARCH= +ifeq ($(ARCH), 64bit) + ARM_VER=v8a +else + ARM_VER=v7a +endif + # Clear out apks clean: - rm -rf dist/android/*.apk project_info.json ./src/kolibri deepclean: clean - rm -r ~/.local/share/python-for-android - rm -r build - yes y | docker system prune -a + python-for-android clean_dists + rm -r build || true + rm -r dist || true + yes y | docker system prune -a || true rm build_docker 2> /dev/null # Extract the whl file src/kolibri: clean + rm -r src/kolibri 2> /dev/null || true unzip -qo "whl/kolibri*.whl" "kolibri/*" -x "kolibri/dist/cext*" -d src/ + ./delete_kolibri_blacklist.sh # Generate the project info file project_info.json: project_info.template src/kolibri scripts/create_project_info.py @@ -27,8 +37,8 @@ pew_release_flag = --release endif .PHONY: kolibri.apk -# Buld the debug version of the apk -kolibri.apk: p4a_android_distro +# Build the debug version of the apk +kolibri.apk: p4a_android_distro preseeded_kolibri_home pew build android $(pew_release_flag) $(ARCH) # DOCKER BUILD @@ -39,20 +49,38 @@ kolibri.apk: p4a_android_distro build_docker: Dockerfile docker build -t android_kolibri . -create_premigrated_db: - pip install whl/*.whl - KOLIBRI_HOME=tmphome kolibri manage migrate - cp tmphome/db.sqlite3 src/db.sqlite3.empty +preseeded_kolibri_home: export KOLIBRI_HOME := src/preseeded_kolibri_home +preseeded_kolibri_home: export PYTHONPATH := tmpenv +preseeded_kolibri_home: + rm -r tmpenv 2> /dev/null || true + rm -r src/preseeded_kolibri_home 2> /dev/null || true + pip uninstall kolibri 2> /dev/null || true + pip install --target tmpenv whl/*.whl + tmpenv/bin/kolibri start --port=16294 + sleep 1 + tmpenv/bin/kolibri stop + sleep 1 + yes yes | tmpenv/bin/kolibri manage deprovision + rm -r src/preseeded_kolibri_home/logs 2> /dev/null || true + rm -r src/preseeded_kolibri_home/sessions 2> /dev/null || true + rm -r src/preseeded_kolibri_home/process_cache 2> /dev/null || true + touch src/preseeded_kolibri_home/was_preseeded # Run the docker image. # TODO Would be better to just specify the file here? run_docker: build_docker ./scripts/rundocker.sh -launch: +softbuild: project_info.json pew build android $(pew_release_flag) $(ARCH) + +install: adb uninstall org.learningequality.Kolibri || true 2> /dev/null - rm dist/android/Kolibri-0-debug.apk || true 2> /dev/null - adb install dist/android/*-debug.apk + adb install dist/android/*$(ARM_VER)-debug-*.apk + +run: install adb shell am start -n org.learningequality.Kolibri/org.kivy.android.PythonActivity - adb logcat | grep -E "python|Python| server " \ No newline at end of file + sleep 1 + adb logcat | grep -i -E "python|kolibr| `adb shell ps | grep ' org.learningequality.Kolibri$$' | tr -s [:space:] ' ' | cut -d' ' -f2` " | grep -E -v "WifiTrafficPoller|localhost:5000|NetworkManagementSocketTagger|No jobs to start" + +launch: softbuild run \ No newline at end of file diff --git a/assets/_load.html b/assets/_load.html index 9aadf7a6..f2823a66 100644 --- a/assets/_load.html +++ b/assets/_load.html @@ -1,22 +1,302 @@ + + + + - + Kolibri is starting... -
+ + + +
-
-
+ + Kolibri Loader + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - -
- -

Please wait, Kolibri is loading...

- -
- - - - +

Starting Kolibri

+

Please wait; this may take some time.

+ + + \ No newline at end of file diff --git a/assets/loading-spinner.gif b/assets/loading-spinner.gif deleted file mode 100644 index 638e6609..00000000 Binary files a/assets/loading-spinner.gif and /dev/null differ diff --git a/delete_kolibri_blacklist.sh b/delete_kolibri_blacklist.sh new file mode 100755 index 00000000..bf8ee636 --- /dev/null +++ b/delete_kolibri_blacklist.sh @@ -0,0 +1,60 @@ +# remove the OIDC dependencies, as we won't use them here +rm -r src/kolibri/plugins/oidc_provider_plugin +rm -r src/kolibri/dist/oidc_provider +rm -r src/kolibri/dist/jwkest +rm -r src/kolibri/dist/Cryptodome + +# remove some assorted additional plugins +rm -r src/kolibri/plugins/demo_server +rm -r src/kolibri/plugins/style_guide + +# remove python2-only stuff +rm -r src/kolibri/dist/py2only + +# remove pycountry and replace with stub +# (only used by getlang_by_alpha2 in le-utils, which Kolibri doesn't call) +rm -r src/kolibri/dist/pycountry/* +touch src/kolibri/dist/pycountry/__init__.py + +# remove source maps +find src/kolibri -name "*.js.map" -type f -delete + +# remove node_modules (contains only core-js) +rm -r src/kolibri/core/node_modules + +# remove unused translation files from django and other apps +rm -r src/kolibri/dist/rest_framework/locale +rm -r src/kolibri/dist/django_filters/locale +rm -r src/kolibri/dist/mptt/locale + +rm -r src/kolibri/dist/django/contrib/admindocs/locale +rm -r src/kolibri/dist/django/contrib/auth/locale +rm -r src/kolibri/dist/django/contrib/sites/locale +rm -r src/kolibri/dist/django/contrib/contenttypes/locale +rm -r src/kolibri/dist/django/contrib/flatpages/locale +rm -r src/kolibri/dist/django/contrib/sessions/locale +rm -r src/kolibri/dist/django/contrib/humanize/locale +rm -r src/kolibri/dist/django/contrib/admin/locale + +# remove some django components entirely +rm -r src/kolibri/dist/django/contrib/gis +rm -r src/kolibri/dist/django/contrib/redirects +rm -r src/kolibri/dist/django/conf/app_template +rm -r src/kolibri/dist/django/conf/project_template +rm -r src/kolibri/dist/django/db/backends/postgresql_psycopg2 +rm -r src/kolibri/dist/django/db/backends/postgresql +rm -r src/kolibri/dist/django/db/backends/mysql +rm -r src/kolibri/dist/django/db/backends/oracle +rm -r src/kolibri/dist/django/contrib/postgres + +# remove bigger chunks of django admin (may not want to do this) +rm -r src/kolibri/dist/django/contrib/admin/static +rm -r src/kolibri/dist/django/contrib/admin/templates + +# other assorted testing stuff +find src/kolibri -wholename "*/test/*" -not -wholename "*/django/test/*" -delete +rm -r src/kolibri/dist/tzlocal/test_data + +# remove some unnecessary apps +rm -r src/kolibri/dist/redis_cache +rm -r src/kolibri/dist/redis \ No newline at end of file diff --git a/large_icon.png b/large_icon.png new file mode 100644 index 00000000..4d1bfda1 Binary files /dev/null and b/large_icon.png differ diff --git a/logcat.sh b/logcat.sh new file mode 100755 index 00000000..bedb3d57 --- /dev/null +++ b/logcat.sh @@ -0,0 +1,10 @@ +main_pid=$(adb shell ps | grep ' org.learningequality.Kolibri$' | tr -s [:space:] ' ' | cut -d' ' -f2) +server_pid=$(adb shell ps | grep ' org.learningequality.Kolibri:service_kolibri$' | tr -s [:space:] ' ' | cut -d' ' -f2) +exclusion="NetworkManagementSocketTagger|Could not ping|No jobs|port:5000" +if [ -z "$server_pid" ]; then + echo "Searching for: $main_pid" + adb logcat | grep -E " $main_pid " +else + echo "Searching for: $main_pid | $server_pid " | egrep -v "$exclusion" + adb logcat | grep -E " $main_pid | $server_pid " | egrep -v "$exclusion" +fi \ No newline at end of file diff --git a/project_info.template b/project_info.template index ad2d83e8..3b83052d 100644 --- a/project_info.template +++ b/project_info.template @@ -3,10 +3,18 @@ "version": "${apk_version}", "build_number": "${build_number}", "identifier": "org.learningequality.Kolibri", - "requirements": {"android": ["python2", "pyjnius", "genericndkbuild", "sqlite3", "cryptography", "pyopenssl", "openssl", "six"]}, + "requirements": {"android": ["python3", "android", "pyjnius", "genericndkbuild", "sqlite3", "cryptography", "pyopenssl", "openssl", "six", "flask"]}, "whitelist_file": {"android": "whitelist.txt"}, "icons": {"android": "icon.png"}, "launch_images": {"android": "assets/launch-image.png"}, "asset_dirs": ["assets"], - "extra_build_options": {"android": {"services": ["server:server.py"], "extra_permissions": [], "sdk": 17}} + "extra_build_options": { + "android": { + "services": ["kolibri:android_service.py"], + "extra_permissions": ["FOREGROUND_SERVICE"], + "sdk": 29, + "minsdk": 21, + "fileprovider_paths_filename": "fileprovider_paths.xml" + } + } } diff --git a/scripts/rundocker.sh b/scripts/rundocker.sh index 8b015a7a..1caaa196 100755 --- a/scripts/rundocker.sh +++ b/scripts/rundocker.sh @@ -43,7 +43,7 @@ echo "Starting ${CONTAINER_ID}" docker start -i ${CONTAINER_ID} # copy the apk to our host. Handles permissions. -echo -e "Coping APK \n\t From ${CONTAINER_ID}:${CONTAINER_HOME}/dist/ to ${PWD}" +echo -e "Copying APK \n\t From ${CONTAINER_ID}:${CONTAINER_HOME}/dist/ to ${PWD}" docker cp ${CONTAINER_ID}:${CONTAINER_HOME}/dist/ . # manually remove the container afterward diff --git a/src/android_service.py b/src/android_service.py new file mode 100644 index 00000000..d7ba0919 --- /dev/null +++ b/src/android_service.py @@ -0,0 +1,65 @@ +import initialization # keep this first, to ensure we're set up for other imports + +import flask +import logging +import os +import pew.ui +import shutil +import time + +from config import FLASK_PORT + +from android_utils import share_by_intent +from kolibri_utils import get_content_file_path + +# initialize logging before loading any third-party modules, as they may cause logging to get configured. +logging.basicConfig(level=logging.DEBUG) + + +logging.info("Entering android_service.py...") + +from android_utils import get_service_args, make_service_foreground +from kolibri_utils import start_kolibri_server + +# load the arguments passed into the service into environment variables +args = get_service_args() +for arg, val in args.items(): + print("setting envvar '{}' to '{}'".format(arg, val)) + os.environ[arg] = str(val) + +# move in a templated Kolibri data directory, including pre-migrated DB, to speed up startup +HOME_TEMPLATE_PATH = "preseeded_kolibri_home" +HOME_PATH = os.environ["KOLIBRI_HOME"] +if not os.path.exists(HOME_PATH) and os.path.exists(HOME_TEMPLATE_PATH): + shutil.move(HOME_TEMPLATE_PATH, HOME_PATH) + +# ensure the service stays running by "foregrounding" it with a persistent notification +make_service_foreground("Kolibri is running...", "Click here to resume.") + +# start the kolibri server as a thread +thread = pew.ui.PEWThread(target=start_kolibri_server) +thread.daemon = True +thread.start() + +# start a parallel Flask server as a backchannel for triggering events +flaskapp = flask.Flask(__name__) + +@flaskapp.route('/share_by_intent') +def do_share_by_intent(): + + args = flask.request.args + allowed_args = ["filename", "path", "msg", "app", "mimetype"] + kwargs = {key: args[key] for key in args if key in allowed_args} + + if "filename" in kwargs: + kwargs["path"] = get_content_file_path(kwargs.pop("filename")) + + logging.error("Sharing: {}".format(kwargs)) + + share_by_intent(**kwargs) + + return "OK, boomer" + + +if __name__ == "__main__": + flaskapp.run(host="localhost", port=FLASK_PORT) diff --git a/src/android_utils.py b/src/android_utils.py new file mode 100644 index 00000000..bae0db9a --- /dev/null +++ b/src/android_utils.py @@ -0,0 +1,135 @@ +import json +import logging +import os +from jnius import autoclass, cast, jnius + +logging.basicConfig(level=logging.DEBUG) + +AndroidString = autoclass("java.lang.String") +Context = autoclass("android.content.Context") +Environment = autoclass("android.os.Environment") +File = autoclass("java.io.File") +FileProvider = autoclass('android.support.v4.content.FileProvider') +Intent = autoclass("android.content.Intent") +NotificationBuilder = autoclass("android.app.Notification$Builder") +NotificationManager = autoclass("android.app.NotificationManager") +PackageManager = autoclass("android.content.pm.PackageManager") +PendingIntent = autoclass("android.app.PendingIntent") +PythonActivity = autoclass("org.kivy.android.PythonActivity") +Timezone = autoclass("java.util.TimeZone") +Uri = autoclass("android.net.Uri") + +ANDROID_VERSION = autoclass("android.os.Build$VERSION") +SDK_INT = ANDROID_VERSION.SDK_INT + + +def is_service_context(): + return "PYTHON_SERVICE_ARGUMENT" in os.environ + + +def get_service(): + assert is_service_context(), "Cannot get service, as we are not in a service context." + PythonService = autoclass("org.kivy.android.PythonService") + return PythonService.mService + + +def get_timezone_name(): + return Timezone.getDefault().getDisplayName() + + +def start_service(service_name, service_args): + service = autoclass("org.learningequality.Kolibri.Service{}".format(service_name.title())) + service.start(PythonActivity.mActivity, json.dumps(dict(service_args))) + + +def get_service_args(): + assert is_service_context(), "Cannot get service args, as we are not in a service context." + return json.loads(os.environ.get("PYTHON_SERVICE_ARGUMENT") or "{}") + + +def get_version_name(): + return get_activity().getPackageManager().getPackageInfo(PythonActivity.getPackageName(), 0).versionName + + +def get_activity(): + if is_service_context(): + return cast("android.app.Service", get_service()) + else: + return PythonActivity.mActivity + + +def is_app_installed(app_id): + + manager = get_activity().getPackageManager() + + try: + manager.getPackageInfo(app_id, PackageManager.GET_ACTIVITIES) + except jnius.JavaException as e: + return False + + return True + + +# TODO: check for storage availability, allow user to chose sd card or internal +def get_home_folder(): + kolibri_home_file = get_activity().getExternalFilesDir(None) + return os.path.join(kolibri_home_file.toString(), "KOLIBRI_DATA") + + +def send_whatsapp_message(msg): + share_by_intent(msg=msg, app="com.whatsapp") + + +def share_by_intent(path=None, filename=None, msg=None, app=None, mimetype=None): + + assert path or msg or filename, "Must provide either a path, a filename, or a msg to share" + + sendIntent = Intent() + sendIntent.setAction(Intent.ACTION_SEND) + if path: + uri = FileProvider.getUriForFile( + Context.getApplicationContext(), + "org.learningequality.Kolibri.fileprovider", + File(path) + ) + parcelable = cast("android.os.Parcelable", uri) + sendIntent.putExtra(Intent.EXTRA_STREAM, parcelable) + sendIntent.setType(AndroidString(mimetype or "*/*")) + sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + if msg: + if not path: + sendIntent.setType(AndroidString(mimetype or "text/plain")) + sendIntent.putExtra(Intent.EXTRA_TEXT, AndroidString(msg)) + if app: + sendIntent.setPackage(AndroidString(app)) + sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + get_activity().startActivity(sendIntent) + + +def make_service_foreground(title, message): + service = get_service() + Drawable = autoclass("{}.R$drawable".format(service.getPackageName())) + app_context = service.getApplication().getApplicationContext() + + if SDK_INT >= 26: + NotificationChannel = autoclass("android.app.NotificationChannel") + notification_service = cast(NotificationManager, get_activity().getSystemService(Context.NOTIFICATION_SERVICE)) + channel_id = get_activity().getPackageName() + app_channel = NotificationChannel(channel_id, "Kolibri Background Server", NotificationManager.IMPORTANCE_DEFAULT) + notification_service.createNotificationChannel(app_channel) + notification_builder = NotificationBuilder(app_context, channel_id) + else: + notification_builder = NotificationBuilder(app_context) + + notification_builder.setContentTitle(AndroidString(title)) + notification_builder.setContentText(AndroidString(message)) + notification_intent = Intent(app_context, PythonActivity) + notification_intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) + notification_intent.setAction(Intent.ACTION_MAIN) + notification_intent.addCategory(Intent.CATEGORY_LAUNCHER) + intent = PendingIntent.getActivity(service, 0, notification_intent, 0) + notification_builder.setContentIntent(intent) + notification_builder.setSmallIcon(Drawable.icon) + notification_builder.setAutoCancel(True) + new_notification = notification_builder.getNotification() + service.startForeground(1, new_notification) diff --git a/src/config.py b/src/config.py index d3e4ac46..a9c1af87 100644 --- a/src/config.py +++ b/src/config.py @@ -1 +1,2 @@ -PORT = 8080 \ No newline at end of file +KOLIBRI_PORT = 8080 +FLASK_PORT = 5226 \ No newline at end of file diff --git a/src/db.sqlite3.empty b/src/db.sqlite3.empty deleted file mode 100644 index 5dfcc731..00000000 Binary files a/src/db.sqlite3.empty and /dev/null differ diff --git a/src/fileprovider_paths.xml b/src/fileprovider_paths.xml new file mode 100644 index 00000000..3514378c --- /dev/null +++ b/src/fileprovider_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/initialization.py b/src/initialization.py new file mode 100644 index 00000000..7ab266dc --- /dev/null +++ b/src/initialization.py @@ -0,0 +1,21 @@ +import os +import pew.ui +import sys + + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(script_dir) +sys.path.append(os.path.join(script_dir, "kolibri", "dist")) +sys.path.append(os.path.join(script_dir, "extra-packages")) + +os.environ["DJANGO_SETTINGS_MODULE"] = "kolibri_app_settings" + +# TODO: before shipping the app, make this contingent on debug vs production mode +os.environ["KOLIBRI_RUN_MODE"] = "pew-dev" + + +if pew.ui.platform == "android": + # initialize some system environment variables needed to run smoothly on Android + from android_utils import get_timezone_name + os.environ["TZ"] = get_timezone_name() + os.environ["LC_ALL"] = "en_US.UTF-8" \ No newline at end of file diff --git a/src/kolibri_utils.py b/src/kolibri_utils.py new file mode 100644 index 00000000..0d6283b4 --- /dev/null +++ b/src/kolibri_utils.py @@ -0,0 +1,17 @@ +import os +import pew.ui +import sys + + +def start_kolibri_server(): + from kolibri.utils.cli import main + print("Starting Kolibri server...") + print("Port: {}".format(os.environ.get("KOLIBRI_HTTP_PORT", "(default)"))) + print("Home folder: {}".format(os.environ.get("KOLIBRI_HOME", "(default)"))) + print("Timezone: {}".format(os.environ.get("TZ", "(default)"))) + main(["start", "--foreground"]) + + +def get_content_file_path(filename): + from kolibri.core.content.utils.paths import get_content_storage_file_path + return get_content_storage_file_path(filename) \ No newline at end of file diff --git a/src/main.py b/src/main.py index ef341a1c..bbf91932 100644 --- a/src/main.py +++ b/src/main.py @@ -1,8 +1,10 @@ -import json +import initialization # keep this first, to ensure we're set up for other imports + import logging import os import time -import urllib2 +import urllib.error +import urllib.request # initialize logging before loading any third-party modules, as they may cause logging to get configured. logging.basicConfig(level=logging.DEBUG) @@ -10,41 +12,38 @@ import pew import pew.ui -from config import PORT +from config import KOLIBRI_PORT pew.set_app_name("Kolibri") logging.info("Entering main.py...") -def start_kolibri_server(port): +if pew.ui.platform == "android": - if pew.ui.platform == "android": + from android_utils import get_home_folder, get_version_name - from jnius import autoclass + os.environ["KOLIBRI_HOME"] = get_home_folder() + os.environ["KOLIBRI_APK_VERSION_NAME"] = get_version_name() - PythonActivity = autoclass("org.kivy.android.PythonActivity") - service = autoclass('org.learningequality.Kolibri.ServiceServer') - # TODO: check for storage availability, allow user to chose sd card or internal - def get_home_folder(): - kolibri_home_file = PythonActivity.mActivity.getExternalFilesDir(None) - return kolibri_home_file.toString() +def start_kolibri(port): + + os.environ["KOLIBRI_HTTP_PORT"] = str(port) + + if pew.ui.platform == "android": logging.info("Starting kolibri server via Android service...") - service.start(PythonActivity.mActivity, json.dumps({ - "HOME": get_home_folder(), - "PORT": port, - "VERSION": PythonActivity.getPackageManager().getPackageInfo(PythonActivity.getPackageName(), 0).versionName, - })) + from android_utils import start_service + start_service("kolibri", dict(os.environ)) else: - from server import start_django - logging.info("Starting kolibri server directly as thread...") - thread = pew.ui.PEWThread(target=start_django, args=(port,)) + from kolibri_utils import start_kolibri_server + + thread = pew.ui.PEWThread(target=start_kolibri_server) thread.daemon = True thread.start() @@ -62,8 +61,8 @@ def setUp(self): self.kolibri_loaded = False self.view = pew.ui.WebUIView("Kolibri", self.loader_url, delegate=self) - # start thread - start_kolibri_server(PORT) + # start kolibri server + start_kolibri(KOLIBRI_PORT) self.load_thread = pew.ui.PEWThread(target=self.wait_for_server) self.load_thread.daemon = True @@ -96,14 +95,15 @@ def page_loaded(self, url): def wait_for_server(self): - home_url = "http://localhost:{port}".format(port=PORT) + home_url = "http://localhost:{port}".format(port=KOLIBRI_PORT) # test url to see if server has started def running(): try: - urllib2.urlopen(home_url) + with urllib.request.urlopen(home_url) as response: + response.read() return True - except urllib2.URLError: + except urllib.error.URLError: return False # Tie up this thread until the server is running diff --git a/src/server.py b/src/server.py deleted file mode 100644 index e32814f8..00000000 --- a/src/server.py +++ /dev/null @@ -1,88 +0,0 @@ -import json -import logging -import os -import shutil -import sys - -# initialize logging before loading any third-party modules, as they may cause logging to get configured. -logging.basicConfig(level=logging.DEBUG) - -import pew -import pew.ui - -logging.info("Entering server.py...") - -script_dir = os.path.dirname(os.path.abspath(__file__)) -sys.path.append(script_dir) -sys.path.append(os.path.join(script_dir, "kolibri", "dist")) - -os.environ["DJANGO_SETTINGS_MODULE"] = "kolibri_app_settings" - -# TODO: before shipping the app, make this contingent on debug vs production mode -os.environ["KOLIBRI_RUN_MODE"] = "pew-dev" - -def start_django(port): - - from kolibri.utils.cli import main - - logging.info("Starting server...") - - main(["start", "--foreground", "--port={port}".format(port=port)]) - - -if pew.ui.platform == "android": - - from jnius import autoclass - - service_args = json.loads(os.environ.get("PYTHON_SERVICE_ARGUMENT") or "{}") - - service = autoclass('org.kivy.android.PythonService').mService - File = autoclass("java.io.File") - Timezone = autoclass("java.util.TimeZone") - AndroidString = autoclass('java.lang.String') - Drawable = autoclass("{}.R$drawable".format(service.getPackageName())) - Context = autoclass('android.content.Context') - Intent = autoclass('android.content.Intent') - PendingIntent = autoclass('android.app.PendingIntent') - NotificationBuilder = autoclass('android.app.Notification$Builder') - Notification = autoclass('android.app.Notification') - - def make_service_foreground(title, message): - PythonActivity = autoclass('org.kivy.android.PythonActivity') - app_context = service.getApplication().getApplicationContext() - notification_builder = NotificationBuilder(app_context) - notification_builder.setContentTitle(AndroidString(title)) - notification_builder.setContentText(AndroidString(message)) - notification_intent = Intent(app_context, PythonActivity) - notification_intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) - notification_intent.setAction(Intent.ACTION_MAIN) - notification_intent.addCategory(Intent.CATEGORY_LAUNCHER) - intent = PendingIntent.getActivity(service, 0, notification_intent, 0) - notification_builder.setContentIntent(intent) - notification_builder.setSmallIcon(Drawable.icon) - notification_builder.setAutoCancel(True) - new_notification = notification_builder.getNotification() - service.startForeground(1, new_notification) - - os.environ["KOLIBRI_HOME"] = service_args["HOME"] - - # copy an empty pre-migrated database into the Kolibri data directory to speed up startup - DB_TEMPLATE_PATH = "db.sqlite3.empty" - DB_PATH = os.path.join(os.environ["KOLIBRI_HOME"], "db.sqlite3") - if not os.path.exists(DB_PATH) and os.path.exists(DB_TEMPLATE_PATH): - if not os.path.exists(os.environ["KOLIBRI_HOME"]): - os.makedirs(os.environ["KOLIBRI_HOME"]) - shutil.copyfile(DB_TEMPLATE_PATH, DB_PATH) - - # store the version name into an envvar to be picked up by Kolibri - os.environ["KOLIBRI_APK_VERSION_NAME"] = service_args["VERSION"] - - os.environ["TZ"] = Timezone.getDefault().getDisplayName() - os.environ["LC_ALL"] = "en_US.UTF-8" - - logging.info("Home folder: {}".format(os.environ["KOLIBRI_HOME"])) - logging.info("Timezone: {}".format(os.environ["TZ"])) - - make_service_foreground("Kolibri is running...", "Click here to resume.") - - start_django(service_args["PORT"])