Test 112 deps #17

Open
jessopb wants to merge 11 commits from test-112-deps into master
981 changed files with 58286 additions and 3900 deletions

View file

@ -2,11 +2,11 @@ name: Publish Assets
on:
push:
branches: [master]
branches: [master, test-112-deps]
jobs:
build_arm64_aar:
runs-on: ubuntu-latest
runs-on: self-hosted
container: lbry/android-base:python39
steps:
- name: checkout

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
.gradle
app/node_modules/
bin
venv
buildozer.spec
build.log
recipes/**/*.pyc

View file

@ -36,13 +36,6 @@ Alternatively, the JDK available from http://www.oracle.com/technetwork/java/jav
sudo -H pip install --upgrade cython==0.28.1 setuptools
```
#### Install buildozer
A forked version of `buildozer` needs to be installed in order to copy the React Native UI source files into the corresponding directories.
```
git clone https://github.com/lbryio/buildozer.git
cd buildozer && python2.7 setup.py install && cd ..
```
#### Create buildozer.spec
Assuming `lbry-android` as the current working folder:
* Copy `buildozer.spec.sample` to `buildozer.spec` in the `lbry-android` folder. Running `buildozer init` instead will create a new `buildozer.spec` file.

View file

@ -39,7 +39,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.5.30, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.17, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry"
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.10.08, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, cffi==1.13.2, libtorrent==2.0.6, filetype==1.0.9, pyyaml==5.3.1, protobuf==3.17.2, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.18, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve==15.0.0, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir==3.1.2, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry"
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes

View file

@ -39,7 +39,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.5.30, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.17, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry"
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.10.08, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, libtorrent==2.0.6, filetype==1.0.9, pyyaml==5.3.1, protobuf==3.17.2, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.18, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve==15.0.0, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir==3.1.2, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry"
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes

View file

@ -39,7 +39,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.5.30, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.17, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry"
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.10.08, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, cffi==1.13.2, libtorrent==2.0.6, filetype==1.0.9, pyyaml==5.3.1, protobuf==3.17.2, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.18, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve==15.0.0, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir==3.1.2, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry"
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes

View file

@ -39,7 +39,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.5.30, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, defusedxml, netifaces, git+https://github.com/lbryio/aioupnp.git@ab7ef0048bbce6404e463d20e8a15046ea6941f0#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, prometheus_client==0.7.1, "git+https://github.com/lbryio/lbry-sdk@v0.102.0#egg=lbry"
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.10.08, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, cffi==1.13.2, libtorrent==2.0.6, filetype==1.0.9, pyyaml==5.3.1, protobuf==3.17.2, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.18, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve==15.0.0, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir==3.1.2, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry"
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
@ -275,4 +275,3 @@ warn_on_root = 1
# Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2020.12.5, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==21.0.0, defusedxml, aioupnp==0.0.17, asn1crypto, mock, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry"

1857
p4a/CHANGELOG.md Normal file

File diff suppressed because it is too large Load diff

115
p4a/Dockerfile Normal file
View file

@ -0,0 +1,115 @@
# Dockerfile with:
# - Android build environment
# - python-for-android dependencies
#
# Build with:
# docker build --tag=p4a --file Dockerfile .
#
# Run with:
# docker run -it --rm p4a /bin/sh -c '. venv/bin/activate && p4a apk --help'
#
# Or for interactive shell:
# docker run -it --rm p4a
#
# Note:
# Use 'docker run' without '--rm' flag for keeping the container and use
# 'docker commit <container hash> <new image>' to extend the original image
# If platform is not specified, by default the target platform of the build request is used.
# This is not what we want, as Google doesn't provide a linux/arm64 compatible NDK.
# See: https://docs.docker.com/engine/reference/builder/#from
FROM --platform=linux/amd64 ubuntu:20.04
# configure locale
RUN apt -y update -qq > /dev/null \
&& DEBIAN_FRONTEND=noninteractive apt install -qq --yes --no-install-recommends \
locales && \
locale-gen en_US.UTF-8
ENV LANG="en_US.UTF-8" \
LANGUAGE="en_US.UTF-8" \
LC_ALL="en_US.UTF-8"
RUN apt -y update -qq > /dev/null \
&& DEBIAN_FRONTEND=noninteractive apt install -qq --yes --no-install-recommends \
ca-certificates \
curl \
&& apt -y autoremove \
&& apt -y clean \
&& rm -rf /var/lib/apt/lists/*
# retry helper script, refs:
# https://github.com/kivy/python-for-android/issues/1306
ENV RETRY="retry -t 3 --"
RUN curl https://raw.githubusercontent.com/kadwanev/retry/1.0.1/retry \
--output /usr/local/bin/retry && chmod +x /usr/local/bin/retry
ENV USER="user"
ENV HOME_DIR="/home/${USER}"
ENV WORK_DIR="${HOME_DIR}/app" \
PATH="${HOME_DIR}/.local/bin:${PATH}" \
ANDROID_HOME="${HOME_DIR}/.android" \
JAVA_HOME="/usr/lib/jvm/java-13-openjdk-amd64"
# install system dependencies
RUN dpkg --add-architecture i386 \
&& ${RETRY} apt -y update -qq > /dev/null \
&& ${RETRY} DEBIAN_FRONTEND=noninteractive apt install -qq --yes --no-install-recommends \
autoconf \
automake \
autopoint \
build-essential \
ccache \
cmake \
gettext \
git \
lbzip2 \
libffi-dev \
libgtk2.0-0:i386 \
libidn11:i386 \
libltdl-dev \
libncurses5:i386 \
libssl-dev \
libstdc++6:i386 \
libtool \
openjdk-13-jdk \
patch \
pkg-config \
python3 \
python3-dev \
python3-pip \
python3-venv \
sudo \
unzip \
wget \
zip \
zlib1g-dev \
zlib1g:i386 \
&& apt -y autoremove \
&& apt -y clean \
&& rm -rf /var/lib/apt/lists/*
# prepare non root env
RUN useradd --create-home --shell /bin/bash ${USER}
# with sudo access and no password
RUN usermod -append --groups sudo ${USER}
RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
WORKDIR ${WORK_DIR}
RUN mkdir ${ANDROID_HOME} && chown --recursive ${USER} ${HOME_DIR} ${ANDROID_HOME}
USER ${USER}
# Download and install android's NDK/SDK
COPY --chown=user:user ci/makefiles/android.mk /tmp/android.mk
RUN make --file /tmp/android.mk \
&& sudo rm /tmp/android.mk
# install python-for-android from current branch
COPY --chown=user:user Makefile README.md setup.py pythonforandroid/__init__.py ${WORK_DIR}/
RUN mkdir pythonforandroid \
&& mv __init__.py pythonforandroid/ \
&& make virtualenv \
&& rm -rf ~/.cache/
COPY --chown=user:user . ${WORK_DIR}

View file

@ -1,13 +1,13 @@
include LICENSE README.md
include *.toml
recursive-include doc *
prune doc/build
recursive-include pythonforandroid *.py *.tmpl biglink liblink
recursive-include pythonforandroid/recipes *.py *.patch *.c *.pyx Setup *.h
recursive-include pythonforandroid/recipes *.py *.patch *.diff *.c *.pyx Setup *.h
recursive-include pythonforandroid/bootstraps *.properties *.xml *.java *.tmpl *.txt *.png *.aidl *.py *.sh *.c *.h *.html
recursive-include pythonforandroid/bootstraps *.properties *.xml *.java *.tmpl *.txt *.png *.aidl *.py *.sh *.c *.h *.html *.patch
prune .git
prune pythonforandroid/bootstraps/pygame/build/libs

129
p4a/Makefile Normal file
View file

@ -0,0 +1,129 @@
VIRTUAL_ENV ?= venv
PIP=$(VIRTUAL_ENV)/bin/pip
TOX=`which tox`
ACTIVATE=$(VIRTUAL_ENV)/bin/activate
PYTHON=$(VIRTUAL_ENV)/bin/python
FLAKE8=$(VIRTUAL_ENV)/bin/flake8
PYTEST=$(VIRTUAL_ENV)/bin/pytest
SOURCES=src/ tests/
PYTHON_MAJOR_VERSION=3
PYTHON_MINOR_VERSION=6
PYTHON_VERSION=$(PYTHON_MAJOR_VERSION).$(PYTHON_MINOR_VERSION)
PYTHON_MAJOR_MINOR=$(PYTHON_MAJOR_VERSION)$(PYTHON_MINOR_VERSION)
PYTHON_WITH_VERSION=python$(PYTHON_VERSION)
DOCKER_IMAGE=kivy/python-for-android
ANDROID_SDK_HOME ?= $(HOME)/.android/android-sdk
ANDROID_NDK_HOME ?= $(HOME)/.android/android-ndk
ANDROID_NDK_HOME_LEGACY ?= $(HOME)/.android/android-ndk-legacy
REBUILD_UPDATED_RECIPES_EXTRA_ARGS ?= ''
all: virtualenv
$(VIRTUAL_ENV):
python3 -m venv $(VIRTUAL_ENV)
$(PIP) install Cython
$(PIP) install -e .
virtualenv: $(VIRTUAL_ENV)
# ignores test_pythonpackage.py since it runs for too long
test:
$(TOX) -- tests/ --ignore tests/test_pythonpackage.py
rebuild_updated_recipes: virtualenv
. $(ACTIVATE) && \
ANDROID_SDK_HOME=$(ANDROID_SDK_HOME) ANDROID_NDK_HOME=$(ANDROID_NDK_HOME) \
$(PYTHON) ci/rebuild_updated_recipes.py $(REBUILD_UPDATED_RECIPES_EXTRA_ARGS)
testapps-with-numpy: virtualenv
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
--requirements libffi,sdl2,pyjnius,kivy,python3,openssl,requests,urllib3,chardet,idna,sqlite3,setuptools,numpy \
--arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86
testapps-with-scipy: virtualenv
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
export LEGACY_NDK=$(ANDROID_NDK_HOME_LEGACY) && \
python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
--requirements python3,scipy,kivy \
--arch=armeabi-v7a --arch=arm64-v8a
testapps-with-numpy-aab: virtualenv
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
python setup.py aab --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
--requirements libffi,sdl2,pyjnius,kivy,python3,openssl,requests,urllib3,chardet,idna,sqlite3,setuptools,numpy \
--arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86 --release
testapps-service_library-aar: virtualenv
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
python setup.py aar --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
--bootstrap service_library \
--requirements python3 \
--arch=arm64-v8a --arch=x86 --release
testapps-webview: virtualenv
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
--bootstrap webview \
--requirements sqlite3,libffi,openssl,pyjnius,flask,python3,genericndkbuild \
--arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86
testapps-webview-aab: virtualenv
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
python setup.py aab --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
--bootstrap webview \
--requirements sqlite3,libffi,openssl,pyjnius,flask,python3,genericndkbuild \
--arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86 --release
testapps/%: virtualenv
$(eval $@_APP_ARCH := $(shell basename $*))
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
--arch=$($@_APP_ARCH)
clean:
find . -type d -name "__pycache__" -exec rm -r {} +
find . -type d -name "*.egg-info" -exec rm -r {} +
clean/all: clean
rm -rf $(VIRTUAL_ENV) .tox/
docker/pull:
docker pull $(DOCKER_IMAGE):latest || true
docker/build:
docker build --cache-from=$(DOCKER_IMAGE) --tag=$(DOCKER_IMAGE) .
docker/push:
docker push $(DOCKER_IMAGE)
docker/run/test: docker/build
docker run --rm --env-file=.env $(DOCKER_IMAGE) 'make test'
docker/run/command: docker/build
docker run --rm --env-file=.env $(DOCKER_IMAGE) /bin/sh -c "$(COMMAND)"
docker/run/make/with-artifact/apk/%: docker/build
docker run --name p4a-latest --env-file=.env $(DOCKER_IMAGE) make $*
docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/bdist_unit_tests_app-debug-1.1.apk ./apks
docker rm -fv p4a-latest
docker/run/make/with-artifact/aar/%: docker/build
docker run --name p4a-latest --env-file=.env $(DOCKER_IMAGE) make $*
docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/bdist_unit_tests_app-release-1.1.aar ./aars
docker rm -fv p4a-latest
docker/run/make/with-artifact/aab/%: docker/build
docker run --name p4a-latest --env-file=.env $(DOCKER_IMAGE) make $*
docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/bdist_unit_tests_app-release-1.1.aab ./aabs
docker rm -fv p4a-latest
docker/run/make/rebuild_updated_recipes: docker/build
docker run --name p4a-latest -e REBUILD_UPDATED_RECIPES_EXTRA_ARGS --env-file=.env $(DOCKER_IMAGE) make rebuild_updated_recipes
docker/run/make/%: docker/build
docker run --rm --env-file=.env $(DOCKER_IMAGE) make $*
docker/run/shell: docker/build
docker run --rm --env-file=.env -it $(DOCKER_IMAGE)

View file

@ -1,96 +1,144 @@
# python-for-android
python-for-android
==================
python-for-android is a packager for Python apps on Android. You can
[![Unit tests & build apps](https://github.com/kivy/python-for-android/workflows/Unit%20tests%20&%20build%20apps/badge.svg?branch=develop)](https://github.com/kivy/python-for-android/actions?query=workflow%3A%22Unit+tests+%26+build+apps%22)
[![Coverage Status](https://coveralls.io/repos/github/kivy/python-for-android/badge.svg?branch=develop&kill_cache=1)](https://coveralls.io/github/kivy/python-for-android?branch=develop)
[![Backers on Open Collective](https://opencollective.com/kivy/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/kivy/sponsors/badge.svg)](#sponsors)
python-for-android is a packaging tool for Python apps on Android. You can
create your own Python distribution including the modules and
dependencies you want and bundle it in an APK along with your own
code.
dependencies you want, and bundle it in an APK or AAB along with your own code.
Features include:
- Support for building with both Python 2 and Python 3.
- Different app backends including Kivy, PySDL2, and a WebView with
Python webserver.
- Automatic support for most pure Python modules, and built in support
for many others, including popular dependencies such as numpy and
sqlalchemy.
- Multiple architecture targets, for APKs optimized on any given device.
- Different app backends including Kivy, PySDL2, and a WebView with
Python webserver.
- Automatic support for most pure Python modules, and built in support
for many others, including popular dependencies such as numpy and
sqlalchemy.
- Multiple architecture targets, for APKs optimised on any given
device.
- AAB: Android App Bundle support.
For documentation and support, see:
- Website: http://python-for-android.readthedocs.io
- Mailing list: https://groups.google.com/forum/#!forum/kivy-users or
https://groups.google.com/forum/#!forum/python-android.
- Website: http://python-for-android.readthedocs.io
- Mailing list: https://groups.google.com/forum/#!forum/kivy-users or
https://groups.google.com/forum/#!forum/python-android.
In 2015, these tools were rewritten to provide a newer, easier to use and
extended interface. If you are looking for the old toolchain with
distribute.sh and build.py, it is still available at
https://github.com/kivy/python-for-android/tree/old_toolchain, and
issues and PRs relating to this branch are still accepted. However,
the new toolchain contains all the same functionality via the built in
pygame bootstrap.
## Documentation
# Documentation
Follow the [quickstart
instructions](<https://python-for-android.readthedocs.org/en/latest/quickstart/>)
to install and begin creating APKs and AABs.
Follow the
[quickstart instructions](https://python-for-android.readthedocs.org/en/latest/quickstart/)
to install and begin creating APKs.
Quick instructions to start would be::
**Quick instructions**: install python-for-android with:
pip install python-for-android
or to test the master branch::
(for the develop branch: `pip install git+https://github.com/kivy/python-for-android.git`)
pip install git+https://github.com/kivy/python-for-android.git
Test that the install works with:
The executable is called `python-for-android` or `p4a` (both are
equivalent). To test that the installation worked, try
p4a --version
python-for-android recipes
To build any actual apps, **set up the Android SDK and NDK**
as described in the [quickstart](
<https://python-for-android.readthedocs.org/en/latest/quickstart/#installing-android-sdk>).
**Use the SDK/NDK API level & NDK version as in the quickstart,**
other API levels may not work.
This should return a list of recipes available to be built.
With everything installed, build an APK with SDL2 with e.g.:
To build any distributions, you need to set up the Android SDK and NDK
as described in the documentation linked above.
p4a apk --requirements=kivy --private /home/username/devel/planewave_frozen/ --package=net.inclem.planewavessdl2 --name="planewavessdl2" --version=0.5 --bootstrap=sdl2
If you did this, to build an APK with SDL2 you can try e.g.
**If you need to deploy your app on Google Play, Android App Bundle (aab) is required since 1 August 2021:**
p4a apk --requirements=kivy --private /home/asandy/devel/planewave_frozen/ --package=net.inclem.planewavessdl2 --name="planewavessdl2" --version=0.5 --bootstrap=sdl2
**For full instructions and parameter options,** see [the
documentation](https://python-for-android.readthedocs.io/en/latest/quickstart/#usage).
For full instructions and parameter options, see the documentation.
# Support
## Support
If you need assistance, you can ask for help on our mailing list:
* User Group: https://groups.google.com/group/kivy-users
* Email: kivy-users@googlegroups.com
- User Group: https://groups.google.com/group/kivy-users
- Email: kivy-users@googlegroups.com
We also have an IRC channel:
We also have [#support Discord channel](https://chat.kivy.org/).
* Server: irc.freenode.net
* Port: 6667, 6697 (SSL only)
* Channel: #kivy
## Contributing
# Contributing
We love pull requests and discussing novel ideas. Check out our
[contribution guide](http://kivy.org/docs/contribute.html) and
We love pull requests and discussing novel ideas. Check out the Kivy
project [contribution guide](https://kivy.org/doc/stable/contribute.html) and
feel free to improve python-for-android.
See [our
documentation](https://python-for-android.readthedocs.io/en/latest/contribute/)
for more information about the python-for-android development and
release model, but don't worry about the details. You just need to
make a pull request, we'll take care of the rest.
The following mailing list and IRC channel are used exclusively for
discussions about developing the Kivy framework and its sister projects:
* Dev Group: https://groups.google.com/group/kivy-dev
* Email: kivy-dev@googlegroups.com
- Dev Group: https://groups.google.com/group/kivy-dev
- Email: kivy-dev@googlegroups.com
IRC channel:
We also have [#dev Discord channel](https://chat.kivy.org/).
* Server: irc.freenode.net
* Port: 6667, 6697 (SSL only)
* Channel: #kivy or #kivy-dev
## License
# License
python-for-android is released under the terms of the MIT License.
Please refer to the LICENSE file.
python-for-android is released under the terms of the MIT License. Please refer to the
LICENSE file.
## History
In 2015 these tools were rewritten to provide a new, easier-to-use and
easier-to-extend interface. If you'd like to browse the old toolchain, its
status is recorded for posterity at at
https://github.com/kivy/python-for-android/tree/old_toolchain.
In the last quarter of 2018 the python recipes were changed. The
new recipe for python3 (3.7.1) had a new build system which was
applied to the ancient python recipe, allowing us to bump the python2
version number to 2.7.15. This change unified the build process for
both python recipes, and probably solved various issues detected over the
years. These **unified python recipes** require a **minimum target api level of 21**,
*Android 5.0 - Lollipop*. If you need to build targeting an
api level below 21, you should use an older version of python-for-android
(<=0.7.1).
On March of 2020 we dropped support for creating apps that use Python 2. The latest
python-for-android release that supported building Python 2 was version 2019.10.6.
On August of 2021, we added support for Android App Bundle (aab). As a collateral,
now We support multi-arch apk.
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](https://kivy.org/doc/stable/contribute.html)].
<a href="https://github.com/kivy/python-for-android/graphs/contributors"><img src="https://opencollective.com/kivy/contributors.svg?width=890&button=false" /></a>
## Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/kivy#backer)]
<a href="https://opencollective.com/kivy#backers" target="_blank"><img src="https://opencollective.com/kivy/backers.svg?width=890"></a>
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/kivy#sponsor)]
<a href="https://opencollective.com/kivy/sponsor/0/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/1/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/2/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/3/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/4/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/5/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/6/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/7/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/8/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/kivy/sponsor/9/website" target="_blank"><img src="https://opencollective.com/kivy/sponsor/9/avatar.svg"></a>

45
p4a/ci/constants.py Normal file
View file

@ -0,0 +1,45 @@
from enum import Enum
class TargetPython(Enum):
python3 = 2
# recipes that currently break the build
# a recipe could be broken for a target Python and not for the other,
# hence we're maintaining one list per Python target
BROKEN_RECIPES_PYTHON3 = set([
'brokenrecipe',
# enum34 is not compatible with Python 3.6 standard library
# https://stackoverflow.com/a/45716067/185510
'enum34',
# build_dir = glob.glob('build/lib.*')[0]
# IndexError: list index out of range
'secp256k1',
# requires `libpq-dev` system dependency e.g. for `pg_config` binary
'psycopg2',
# most likely some setup in the Docker container, because it works in host
'pyjnius', 'pyopenal',
# SyntaxError: invalid syntax (Python2)
'storm',
# mpmath package with a version >= 0.19 required
'sympy',
'vlc',
# need extra gfortran NDK system add-on
'lapack', 'scipy',
# Outdated and there's a chance that is now useless.
'zope_interface',
# Requires zope_interface, which is broken.
'twisted',
# genericndkbuild is incompatible with sdl2 (which is build by default when targeting sdl2 bootstrap)
'genericndkbuild',
])
BROKEN_RECIPES = {
TargetPython.python3: BROKEN_RECIPES_PYTHON3,
}
# recipes that were already built will be skipped
CORE_RECIPES = set([
'pyjnius', 'kivy', 'openssl', 'requests', 'sqlite3', 'setuptools',
'numpy', 'android', 'hostpython3', 'python3',
])

114
p4a/ci/makefiles/android.mk Normal file
View file

@ -0,0 +1,114 @@
# Downloads and installs the Android SDK depending on supplied platform: darwin or linux
# Those android NDK/SDK variables can be override when running the file
ANDROID_NDK_VERSION ?= 25b
ANDROID_NDK_VERSION_LEGACY ?= 21e
ANDROID_SDK_TOOLS_VERSION ?= 6514223
ANDROID_SDK_BUILD_TOOLS_VERSION ?= 29.0.3
ANDROID_HOME ?= $(HOME)/.android
ANDROID_API_LEVEL ?= 27
# per OS dictionary-like
UNAME_S := $(shell uname -s)
TARGET_OS_Linux = linux
TARGET_OS_ALIAS_Linux = $(TARGET_OS_Linux)
TARGET_OS_Darwin = darwin
TARGET_OS_ALIAS_Darwin = mac
TARGET_OS = $(TARGET_OS_$(UNAME_S))
TARGET_OS_ALIAS = $(TARGET_OS_ALIAS_$(UNAME_S))
ANDROID_SDK_HOME=$(ANDROID_HOME)/android-sdk
ANDROID_SDK_TOOLS_ARCHIVE=commandlinetools-$(TARGET_OS_ALIAS)-$(ANDROID_SDK_TOOLS_VERSION)_latest.zip
ANDROID_SDK_TOOLS_DL_URL=https://dl.google.com/android/repository/$(ANDROID_SDK_TOOLS_ARCHIVE)
ANDROID_NDK_HOME=$(ANDROID_HOME)/android-ndk
ANDROID_NDK_FOLDER=$(ANDROID_HOME)/android-ndk-r$(ANDROID_NDK_VERSION)
ANDROID_NDK_ARCHIVE=android-ndk-r$(ANDROID_NDK_VERSION)-$(TARGET_OS).zip
ANDROID_NDK_HOME_LEGACY=$(ANDROID_HOME)/android-ndk-legacy
ANDROID_NDK_FOLDER_LEGACY=$(ANDROID_HOME)/android-ndk-r$(ANDROID_NDK_VERSION_LEGACY)
ANDROID_NDK_ARCHIVE_LEGACY=android-ndk-r$(ANDROID_NDK_VERSION_LEGACY)-$(TARGET_OS)-x86_64.zip
ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64=gcc-arm64-linux-x86_64.tar.bz2
ANDROID_NDK_GFORTRAN_ARCHIVE_ARM=gcc-arm-linux-x86_64.tar.bz2
ANDROID_NDK_DL_URL=https://dl.google.com/android/repository/$(ANDROID_NDK_ARCHIVE)
ANDROID_NDK_DL_URL_LEGACY=https://dl.google.com/android/repository/$(ANDROID_NDK_ARCHIVE_LEGACY)
$(info Target install OS is : $(target_os))
$(info Android SDK home is : $(ANDROID_SDK_HOME))
$(info Android NDK home is : $(ANDROID_NDK_HOME))
$(info Android NDK Legacy home is : $(ANDROID_NDK_HOME_LEGACY))
$(info Android SDK download url is : $(ANDROID_SDK_TOOLS_DL_URL))
$(info Android NDK download url is : $(ANDROID_NDK_DL_URL))
$(info Android API level is : $(ANDROID_API_LEVEL))
$(info Android NDK version is : $(ANDROID_NDK_VERSION))
$(info Android NDK Legacy version is : $(ANDROID_NDK_VERSION_LEGACY))
$(info JAVA_HOME is : $(JAVA_HOME))
all: install_sdk install_ndk
install_sdk: download_android_sdk extract_android_sdk update_android_sdk
install_ndk: download_android_ndk download_android_ndk_legacy download_android_ndk_gfortran extract_android_ndk extract_android_ndk_legacy extract_android_ndk_gfortran
download_android_sdk:
curl --location --progress-bar --continue-at - \
$(ANDROID_SDK_TOOLS_DL_URL) --output $(ANDROID_SDK_TOOLS_ARCHIVE)
download_android_ndk:
curl --location --progress-bar --continue-at - \
$(ANDROID_NDK_DL_URL) --output $(ANDROID_NDK_ARCHIVE)
download_android_ndk_legacy:
curl --location --progress-bar --continue-at - \
$(ANDROID_NDK_DL_URL_LEGACY) --output $(ANDROID_NDK_ARCHIVE_LEGACY)
download_android_ndk_gfortran:
curl --location --progress-bar --continue-at - \
https://github.com/mzakharo/android-gfortran/releases/download/r$(ANDROID_NDK_VERSION_LEGACY)/$(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64) --output $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64)
curl --location --progress-bar --continue-at - \
https://github.com/mzakharo/android-gfortran/releases/download/r$(ANDROID_NDK_VERSION_LEGACY)/$(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM) --output $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM)
# Extract android SDK and remove the compressed file
extract_android_sdk:
mkdir -p $(ANDROID_SDK_HOME) \
&& unzip -q $(ANDROID_SDK_TOOLS_ARCHIVE) -d $(ANDROID_SDK_HOME) \
&& rm -f $(ANDROID_SDK_TOOLS_ARCHIVE)
# Extract android NDK and remove the compressed file
extract_android_ndk:
mkdir -p $(ANDROID_NDK_FOLDER) \
&& unzip -q $(ANDROID_NDK_ARCHIVE) -d $(ANDROID_HOME) \
&& mv $(ANDROID_NDK_FOLDER) $(ANDROID_NDK_HOME) \
&& rm -f $(ANDROID_NDK_ARCHIVE)
extract_android_ndk_legacy:
mkdir -p $(ANDROID_NDK_FOLDER_LEGACY) \
&& unzip -q $(ANDROID_NDK_ARCHIVE_LEGACY) -d $(ANDROID_HOME) \
&& mv $(ANDROID_NDK_FOLDER_LEGACY) $(ANDROID_NDK_HOME_LEGACY) \
&& rm -f $(ANDROID_NDK_ARCHIVE_LEGACY)
extract_android_ndk_gfortran:
rm -rf $(ANDROID_NDK_HOME_LEGACY)/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/ \
&& mkdir $(ANDROID_NDK_HOME_LEGACY)/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/ \
&& tar -xf $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64) -C $(ANDROID_NDK_HOME_LEGACY)/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/ --strip-components 1 \
&& rm -f $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64) \
&& rm -rf $(ANDROID_NDK_HOME_LEGACY)/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/ \
&& mkdir $(ANDROID_NDK_HOME_LEGACY)/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/ \
&& tar -xf $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM) -C $(ANDROID_NDK_HOME_LEGACY)/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/ --strip-components 1 \
&& rm -f $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM)
# updates Android SDK, install Android API, Build Tools and accept licenses
update_android_sdk:
touch $(ANDROID_HOME)/repositories.cfg
yes | $(ANDROID_SDK_HOME)/tools/bin/sdkmanager --sdk_root=$(ANDROID_SDK_HOME) --licenses > /dev/null
$(ANDROID_SDK_HOME)/tools/bin/sdkmanager --sdk_root=$(ANDROID_SDK_HOME) "build-tools;$(ANDROID_SDK_BUILD_TOOLS_VERSION)" > /dev/null
$(ANDROID_SDK_HOME)/tools/bin/sdkmanager --sdk_root=$(ANDROID_SDK_HOME) "platforms;android-$(ANDROID_API_LEVEL)" > /dev/null
# Set avdmanager permissions (executable)
chmod +x $(ANDROID_SDK_HOME)/tools/bin/avdmanager

13
p4a/ci/makefiles/osx.mk Normal file
View file

@ -0,0 +1,13 @@
# installs Android's SDK/NDK, cython
# The following variable/s can be override when running the file
ANDROID_HOME ?= $(HOME)/.android
all: upgrade_cython install_android_ndk_sdk
upgrade_cython:
pip3 install --upgrade Cython
install_android_ndk_sdk:
mkdir -p $(ANDROID_HOME)
make -f ci/makefiles/android.mk

13
p4a/ci/osx_ci.sh Normal file
View file

@ -0,0 +1,13 @@
#!/bin/bash
set -e -x
arm64_set_path_and_python_version(){
python_version="$1"
if [[ $(/usr/bin/arch) = arm64 ]]; then
export PATH=/opt/homebrew/bin:$PATH
eval "$(pyenv init --path)"
pyenv install $python_version -s
pyenv global $python_version
export PATH=$(pyenv prefix)/bin:$PATH
fi
}

111
p4a/ci/rebuild_updated_recipes.py Executable file
View file

@ -0,0 +1,111 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Continuous Integration helper script.
Automatically detects recipes modified in a changeset (compares with master)
and recompiles them.
To run locally, set the environment variables before running:
```
ANDROID_SDK_HOME=~/.buildozer/android/platform/android-sdk-20
ANDROID_NDK_HOME=~/.buildozer/android/platform/android-ndk-r9c
./ci/rebuild_update_recipes.py
```
Current limitations:
- will fail on conflicting requirements
e.g. https://travis-ci.org/AndreMiras/python-for-android/builds/438840800
the list of recipes was huge and result was:
[ERROR]: Didn't find any valid dependency graphs.
[ERROR]: This means that some of your requirements pull in conflicting dependencies.
- only rebuilds on sdl2 bootstrap
"""
import sh
import os
import sys
import argparse
from pythonforandroid.build import Context
from pythonforandroid import logger
from pythonforandroid.toolchain import current_directory
from pythonforandroid.recipe import Recipe
from ci.constants import TargetPython, CORE_RECIPES, BROKEN_RECIPES
def modified_recipes(branch='origin/develop'):
"""
Returns a set of modified recipes between the current branch and the one
in param.
"""
# using the contrib version on purpose rather than sh.git, since it comes
# with a bunch of fixes, e.g. disabled TTY, see:
# https://stackoverflow.com/a/20128598/185510
git_diff = sh.contrib.git.diff('--name-only', branch)
recipes = set()
for file_path in git_diff:
if 'pythonforandroid/recipes/' in file_path:
recipe = file_path.split('/')[2]
recipes.add(recipe)
return recipes
def build(target_python, requirements, archs):
"""
Builds an APK given a target Python and a set of requirements.
"""
if not requirements:
return
android_sdk_home = os.environ['ANDROID_SDK_HOME']
android_ndk_home = os.environ['ANDROID_NDK_HOME']
requirements.add(target_python.name)
requirements = ','.join(requirements)
logger.info('requirements: {}'.format(requirements))
with current_directory('testapps/on_device_unit_tests/'):
# iterates to stream the output
for line in sh.python(
'setup.py', 'apk', '--sdk-dir', android_sdk_home,
'--ndk-dir', android_ndk_home, '--requirements',
requirements, *[f"--arch={arch}" for arch in archs],
_err_to_out=True, _iter=True):
print(line)
def main():
parser = argparse.ArgumentParser("rebuild_updated_recipes")
parser.add_argument(
"--arch",
help="The archs to build for during tests",
action="append",
default=[],
)
args, unknown = parser.parse_known_args(sys.argv[1:])
logger.info(f"Building updated recipes for the following archs: {args.arch}")
target_python = TargetPython.python3
recipes = modified_recipes()
logger.info('recipes modified: {}'.format(recipes))
recipes -= CORE_RECIPES
logger.info('recipes to build: {}'.format(recipes))
context = Context()
# removing the deleted recipes for the given target (if any)
for recipe_name in recipes.copy():
try:
Recipe.get_recipe(recipe_name, context)
except ValueError:
# recipe doesn't exist, so probably we remove it
recipes.remove(recipe_name)
logger.warning(
'removed {} from recipes because deleted'.format(recipe_name)
)
# removing the known broken recipe for the given target
broken_recipes = BROKEN_RECIPES[target_python]
recipes -= broken_recipes
logger.info('recipes to build (no broken): {}'.format(recipes))
build(target_python, recipes, args.arch)
if __name__ == '__main__':
main()

View file

@ -5,178 +5,97 @@ Working on Android
This page gives details on accessing Android APIs and managing other
interactions on Android.
Storage paths
-------------
Accessing Android APIs
----------------------
If you want to store and retrieve data, you shouldn't just save to
the current directory, and not hardcode `/sdcard/` or some other
path either - it might differ per device.
When writing an Android application you may want to access the normal
Android Java APIs, in order to control your application's appearance
(fullscreen, orientation etc.), interact with other apps or use
hardware like vibration and sensors.
Instead, the `android` module which you can add to your `--requirements`
allows you to query the most commonly required paths::
You can access these with `Pyjnius
<http://pyjnius.readthedocs.org/en/latest/>`_, a Python library for
automatically wrapping Java and making it callable from Python
code. Pyjnius is fairly simple to use, but not very Pythonic and it
inherits Java's verbosity. For this reason the Kivy organisation also
created `Plyer <https://plyer.readthedocs.org/en/latest/>`_, which
further wraps specific APIs in a Pythonic and cross-platform way; you
can call the same code in Python but have it do the right thing also
on platforms other than Android.
from android.storage import app_storage_path
settings_path = app_storage_path()
Pyjnius and Plyer are independent projects whose documentation is
linked above. See below for some simple introductory examples, and
explanation of how to include these modules in your APKs.
from android.storage import primary_external_storage_path
primary_ext_storage = primary_external_storage_path()
This page also documents the ``android`` module which you can include
with p4a, but this is mostly replaced by Pyjnius and is not
recommended for use in new applications.
from android.storage import secondary_external_storage_path
secondary_ext_storage = secondary_external_storage_path()
`app_storage_path()` gives you Android's so-called "internal storage"
which is specific to your app and cannot seen by others or the user.
It compares best to the AppData directory on Windows.
`primary_external_storage_path()` returns Android's so-called
"primary external storage", often found at `/sdcard/` and potentially
accessible to any other app.
It compares best to the Documents directory on Windows.
Requires `Permission.WRITE_EXTERNAL_STORAGE` to read and write to.
`secondary_external_storage_path()` returns Android's so-called
"secondary external storage", often found at `/storage/External_SD/`.
It compares best to an external disk plugged to a Desktop PC, and can
after a device restart become inaccessible if removed.
Requires `Permission.WRITE_EXTERNAL_STORAGE` to read and write to.
.. warning::
Even if `secondary_external_storage_path` returns a path
the external sd card may still not be present.
Only non-empty contents or a successful write indicate that it is.
Read more on all the different storage types and what to use them for
in the Android documentation:
https://developer.android.com/training/data-storage/files
A note on permissions
~~~~~~~~~~~~~~~~~~~~~
Only the internal storage is always accessible with no additional
permissions. For both primary and secondary external storage, you need
to obtain `Permission.WRITE_EXTERNAL_STORAGE` **and the user may deny it.**
Also, if you get it, both forms of external storage may only allow
your app to write to the common pre-existing folders like "Music",
"Documents", and so on. (see the Android Docs linked above for details)
Runtime permissions
-------------------
With API level >= 21, you will need to request runtime permissions
to access the SD card, the camera, and other things.
This can be done through the `android` module which is *available per default*
unless you blacklist it. Use it in your app like this::
from android.permissions import request_permissions, Permission
request_permissions([Permission.WRITE_EXTERNAL_STORAGE])
The available permissions are listed here:
https://developer.android.com/reference/android/Manifest.permission
Using Pyjnius
~~~~~~~~~~~~~
Pyjnius lets you call the Android API directly from Python Pyjnius is
works by dynamically wrapping Java classes, so you don't have to wait
for any particular feature to be pre-supported.
You can include Pyjnius in your APKs by adding `pyjnius` to your build
requirements, e.g. :code:`--requirements=flask,pyjnius`. It is
automatically included in any APK containing Kivy, in which case you
don't need to specify it manually.
The basic mechanism of Pyjnius is the `autoclass` command, which wraps
a Java class. For instance, here is the code to vibrate your device::
from jnius import autoclass
# We need a reference to the Java activity running the current
# application, this reference is stored automatically by
# Kivy's PythonActivity bootstrap
# This one works with Pygame
# PythonActivity = autoclass('org.renpy.android.PythonActivity')
# This one works with SDL2
PythonActivity = autoclass('org.kivy.android.PythonActivity')
activity = PythonActivity.mActivity
Context = autoclass('android.content.Context')
vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE)
vibrator.vibrate(10000) # the argument is in milliseconds
Things to note here are:
- The class that must be wrapped depends on the bootstrap. This is
because Pyjnius is using the bootstrap's java source code to get a
reference to the current activity, which both the Pygame and SDL2
bootstraps store in the ``mActivity`` static variable. This
difference isn't always important, but it's important to know about.
- The code closely follows the Java API - this is exactly the same set
of function calls that you'd use to achieve the same thing from Java
code.
- This is quite verbose - it's a lot of lines to achieve a simple
vibration!
These emphasise both the advantages and disadvantage of Pyjnius; you
*can* achieve just about any API call with it (though the syntax is
sometimes a little more involved, particularly if making Java classes
from Python code), but it's not Pythonic and it's not short. These are
problems that Plyer, explained below, attempts to address.
You can check the `Pyjnius documentation <Pyjnius_>`_ for further details.
Using Plyer
~~~~~~~~~~~
Plyer provides a much less verbose, Pythonic wrapper to
platform-specific APIs. It supports Android as well as iOS and desktop
operating systems, though plyer is a work in progress and not all
platforms support all Plyer calls yet.
Plyer does not support all APIs yet, but you can always Pyjnius to
call anything that is currently missing.
You can include Plyer in your APKs by adding the `Plyer` recipe to
your build requirements, e.g. :code:`--requirements=plyer`.
You should check the `Plyer documentation <Plyer_>`_ for details of all supported
facades (platform APIs), but as an example the following is how you
would achieve vibration as described in the Pyjnius section above::
from plyer.vibrator import vibrate
vibrate(10) # in Plyer, the argument is in seconds
This is obviously *much* less verbose than with Pyjnius!
Using ``android``
~~~~~~~~~~~~~~~~~
This Cython module was used for Android API interaction with Kivy's old
interface, but is now mostly replaced by Pyjnius.
The ``android`` Python module can be included by adding it to your
requirements, e.g. :code:`--requirements=kivy,android`. It is not
automatically included by Kivy unless you use the old (Pygame)
bootstrap.
This module is not separately documented. You can read the source `on
Github
<https://github.com/kivy/python-for-android/tree/master/pythonforandroid/recipes/android/src/android>`__.
One useful facility of this module is to make
:code:`webbrowser.open()` work on Android. You can replicate this
effect without using the android module via the following
code::
from jnius import autoclass
def open_url(url):
Intent = autoclass('android.content.Intent')
Uri = autoclass('android.net.Uri')
browserIntent = Intent()
browserIntent.setAction(Intent.ACTION_VIEW)
browserIntent.setData(Uri.parse(url))
currentActivity = cast('android.app.Activity', mActivity)
currentActivity.startActivity(browserIntent)
class AndroidBrowser(object):
def open(self, url, new=0, autoraise=True):
open_url(url)
def open_new(self, url):
open_url(url)
def open_new_tab(self, url):
open_url(url)
import webbrowser
webbrowser.register('android', AndroidBrowser, None, -1)
Working with the App lifecycle
------------------------------
Other common tasks
------------------
Dismissing the splash screen
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
With the SDL2 bootstrap, the app's splash screen may not be dismissed
immediately when your app has finished loading, due to a limitation
with the way we check if the app has properly started. In this case,
the splash screen overlaps the app gui for a short time.
With the SDL2 bootstrap, the app's splash screen may be visible
longer than necessary (with your app already being loaded) due to a
limitation with the way we check if the app has properly started.
In this case, the splash screen overlaps the app gui for a short time.
You can dismiss the splash screen as follows. Run this code from your
app build method (or use ``kivy.clock.Clock.schedule_once`` to run it
in the following frame)::
To dismiss the loading screen explicitly in your code, use the `android`
module::
from jnius import autoclass
activity = autoclass('org.kivy.android.PythonActivity').mActivity
activity.removeLoadingScreen()
from android import loadingscreen
loadingscreen.hide_loading_screen()
This problem does not affect the Pygame bootstrap, as it uses a
different splash screen method.
You can call it e.g. using ``kivy.clock.Clock.schedule_once`` to run it
in the first active frame of your app, or use the app build method.
Handling the back button
@ -222,3 +141,307 @@ With Kivy, add an ``on_pause`` method to your App class, which returns True::
With the webview bootstrap, pausing should work automatically.
Under SDL2, you can handle the `appropriate events <https://wiki.libsdl.org/SDL_EventType>`__ (see SDL_APP_WILLENTERBACKGROUND etc.).
Observing Activity result
~~~~~~~~~~~~~~~~~~~~~~~~~
.. module:: android.activity
The default PythonActivity has a observer pattern for `onActivityResult <http://developer.android.com/reference/android/app/Activity.html#onActivityResult(int, int, android.content.Intent)>`_ and `onNewIntent <http://developer.android.com/reference/android/app/Activity.html#onNewIntent(android.content.Intent)>`_.
.. function:: bind(eventname=callback, ...)
This allows you to bind a callback to an Android event:
- ``on_new_intent`` is the event associated to the onNewIntent java call
- ``on_activity_result`` is the event associated to the onActivityResult java call
.. warning::
This method is not thread-safe. Call it in the mainthread of your app. (tips: use kivy.clock.mainthread decorator)
.. function:: unbind(eventname=callback, ...)
Unregister a previously registered callback with :func:`bind`.
Example::
# This example is a snippet from an NFC p2p app implemented with Kivy.
from android import activity
def on_new_intent(self, intent):
if intent.getAction() != NfcAdapter.ACTION_NDEF_DISCOVERED:
return
rawmsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if not rawmsgs:
return
for message in rawmsgs:
message = cast(NdefMessage, message)
payload = message.getRecords()[0].getPayload()
print('payload: {}'.format(''.join(map(chr, payload))))
def nfc_enable(self):
activity.bind(on_new_intent=self.on_new_intent)
# ...
def nfc_disable(self):
activity.unbind(on_new_intent=self.on_new_intent)
# ...
Activity lifecycle handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. module:: android.activity
The Android ``Application`` class provides the `ActivityLifecycleCallbacks
<https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks>`_
interface where callbacks can be registered corresponding to `activity
lifecycle
<https://developer.android.com/guide/components/activities/activity-lifecycle>`_
changes. These callbacks can be used to implement logic in the Python app when
the activity changes lifecycle states.
Note that some of the callbacks are not useful in the Python app. For example,
an `onActivityCreated` callback will never be run since the the activity's
`onCreate` callback will complete before the Python app is running. Similarly,
saving instance state in an `onActivitySaveInstanceState` callback will not be
helpful since the Python app doesn't have access to the restored instance
state.
.. function:: register_activity_lifecycle_callbacks(callbackname=callback, ...)
This allows you to bind a callbacks to Activity lifecycle state changes.
The callback names correspond to ``ActivityLifecycleCallbacks`` method
names such as ``onActivityStarted``. See the `ActivityLifecycleCallbacks
<https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks>`_
documentation for names and function signatures for the callbacks.
.. function:: unregister_activity_lifecycle_callbacks(instance)
Unregister a ``ActivityLifecycleCallbacks`` instance previously registered
with :func:`register_activity_lifecycle_callbacks`.
Example::
from android.activity import register_activity_lifecycle_callbacks
def on_activity_stopped(activity):
print('Activity is stopping')
register_activity_lifecycle_callbacks(
onActivityStopped=on_activity_stopped,
)
Receiving Broadcast message
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. module:: android.broadcast
Implementation of the android `BroadcastReceiver
<http://developer.android.com/reference/android/content/BroadcastReceiver.html>`_.
You can specify the callback that will receive the broadcast event, and actions
or categories filters.
.. class:: BroadcastReceiver
.. warning::
The callback will be called in another thread than the main thread. In
that thread, be careful not to access OpenGL or something like that.
.. method:: __init__(callback, actions=None, categories=None)
:param callback: function or method that will receive the event. Will
receive the context and intent as argument.
:param actions: list of strings that represent an action.
:param categories: list of strings that represent a category.
For actions and categories, the string must be in lower case, without the prefix::
# In java: Intent.ACTION_HEADSET_PLUG
# In python: 'headset_plug'
.. method:: start()
Register the receiver with all the actions and categories, and start
handling events.
.. method:: stop()
Unregister the receiver with all the actions and categories, and stop
handling events.
Example::
class TestApp(App):
def build(self):
self.br = BroadcastReceiver(
self.on_broadcast, actions=['headset_plug'])
self.br.start()
# ...
def on_broadcast(self, context, intent):
extras = intent.getExtras()
headset_state = bool(extras.get('state'))
if headset_state:
print('The headset is plugged')
else:
print('The headset is unplugged')
# Don't forget to stop and restart the receiver when the app is going
# to pause / resume mode
def on_pause(self):
self.br.stop()
return True
def on_resume(self):
self.br.start()
Runnable
~~~~~~~~
.. module:: android.runnable
:class:`Runnable` is a wrapper around the Java `Runnable
<http://developer.android.com/reference/java/lang/Runnable.html>`_ class. This
class can be used to schedule a call of a Python function into the
`PythonActivity` thread.
Example::
from android.runnable import Runnable
def helloworld(arg):
print 'Called from PythonActivity with arg:', arg
Runnable(helloworld)('hello')
Or use our decorator::
from android.runnable import run_on_ui_thread
@run_on_ui_thread
def helloworld(arg):
print 'Called from PythonActivity with arg:', arg
helloworld('arg1')
This can be used to prevent errors like:
- W/System.err( 9514): java.lang.RuntimeException: Can't create handler
inside thread that has not called Looper.prepare()
- NullPointerException in ActivityThread.currentActivityThread()
.. warning::
Because the python function is called from the PythonActivity thread, you
need to be careful about your own calls.
Advanced Android API use
------------------------
.. _reference-label-for-android-module:
`android` for Android API access
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As mentioned above, the ``android`` Python module provides a simple
wrapper around many native Android APIS, and it is *included by default*
unless you blacklist it.
The available functionality of this module is not separately documented.
You can read the source `on
Github
<https://github.com/kivy/python-for-android/tree/master/pythonforandroid/recipes/android/src/android>`__.
Also please note you can replicate most functionality without it using
`pyjnius`. (see below)
`Plyer` - a more comprehensive API wrapper
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Plyer provides a more thorough wrapper than `android` for a much larger
area of platform-specific APIs, supporting not only Android but also
iOS and desktop operating systems.
(Though plyer is a work in progress and not all
platforms support all Plyer calls yet)
Plyer does not support all APIs yet, but you can always use Pyjnius to
call anything that is currently missing.
You can include Plyer in your APKs by adding the `Plyer` recipe to
your build requirements, e.g. :code:`--requirements=plyer`.
You should check the `Plyer documentation <https://plyer.readthedocs.io/en/stable/>`_ for details of all supported
facades (platform APIs), but as an example the following is how you
would achieve vibration as described in the Pyjnius section above::
from plyer.vibrator import vibrate
vibrate(10) # in Plyer, the argument is in seconds
This is obviously *much* less verbose than with Pyjnius!
`Pyjnius` - raw lowlevel API access
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pyjnius lets you call the Android API directly from Python Pyjnius is
works by dynamically wrapping Java classes, so you don't have to wait
for any particular feature to be pre-supported.
This is particularly useful when `android` and `plyer` don't already
provide a convenient access to the API, or you need more control.
You can include Pyjnius in your APKs by adding `pyjnius` to your build
requirements, e.g. :code:`--requirements=flask,pyjnius`. It is
automatically included in any APK containing Kivy, in which case you
don't need to specify it manually.
The basic mechanism of Pyjnius is the `autoclass` command, which wraps
a Java class. For instance, here is the code to vibrate your device::
from jnius import autoclass
# We need a reference to the Java activity running the current
# application, this reference is stored automatically by
# Kivy's PythonActivity bootstrap
# This one works with SDL2
PythonActivity = autoclass('org.kivy.android.PythonActivity')
activity = PythonActivity.mActivity
Context = autoclass('android.content.Context')
vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE)
vibrator.vibrate(10000) # the argument is in milliseconds
Things to note here are:
- The class that must be wrapped depends on the bootstrap. This is
because Pyjnius is using the bootstrap's java source code to get a
reference to the current activity, which the bootstraps store in the
``mActivity`` static variable. This difference isn't always
important, but it's important to know about.
- The code closely follows the Java API - this is exactly the same set
of function calls that you'd use to achieve the same thing from Java
code.
- This is quite verbose - it's a lot of lines to achieve a simple
vibration!
These emphasise both the advantages and disadvantage of Pyjnius; you
*can* achieve just about any API call with it (though the syntax is
sometimes a little more involved, particularly if making Java classes
from Python code), but it's not Pythonic and it's not short. These are
problems that Plyer, explained below, attempts to address.
You can check the `Pyjnius documentation <https://pyjnius.readthedocs.io/en/stable/>`_ for further details.

View file

@ -3,7 +3,7 @@ Bootstraps
==========
This page is about creating new bootstrap backends. For build options
of existing bootstraps (i.e. with SDL2, Pygame, Webview etc.), see
of existing bootstraps (i.e. with SDL2, Webview, etc.), see
:ref:`build options <bootstrap_build_options>`.
python-for-android (p4a) supports multiple *bootstraps*. These fulfill a
@ -14,13 +14,14 @@ components such as Android source code and various build files.
This page describes the basics of how bootstraps work so that you can
create and use your own if you like, making it easy to build new kinds
of Python project for Android.
of Python projects for Android.
Creating a new bootstrap
------------------------
A bootstrap class consists of just a few basic components, though one of them must do a lot of work.
A bootstrap class consists of just a few basic components, though one of them
must do a lot of work.
For instance, the SDL2 bootstrap looks like the following::

View file

@ -8,44 +8,13 @@ This page contains instructions for using different build options.
Python versions
---------------
python2
~~~~~~~
python-for-android supports using Python 3.7 or higher. To explicitly select a Python
version in your requirements, use e.g. ``--requirements=python3==3.7.1,hostpython3==3.7.1``.
Select this by adding it in your requirements, e.g. ``--requirements=python2``.
The last python-for-android version supporting Python2 was `v2019.10.06 <https://github.com/kivy/python-for-android/archive/v2019.10.06.zip>`__
This option builds Python 2.7.2 for your selected Android
architecture. There are no special requirements, all the building is
done locally.
The python2 build is also the way python-for-android originally
worked, even in the old toolchain.
python3
~~~~~~~
.. warning::
Python3 support is experimental, and some of these details
may change as it is improved and fully stabilised.
.. note:: You must manually download the `CrystaX NDK
<https://www.crystax.net/android/ndk>`__ and tell
python-for-android to use it with ``--ndk-dir /path/to/NDK``.
Select this by adding the ``python3crystax`` recipe to your
requirements, e.g. ``--requirements=python3crystax``.
This uses the prebuilt Python from the `CrystaX NDK
<https://www.crystax.net/android/ndk>`__, a drop-in replacement for
Google's official NDK which includes many improvements. You
*must* use the CrystaX NDK 10.3.0 or higher when building with
python3. You can get it `here
<https://www.crystax.net/en/download>`__.
The python3crystax build is handled quite differently to python2 so
there may be bugs or surprising behaviours. If you come across any,
feel free to `open an issue
<https://github.com/kivy/python-for-android>`__.
Python-for-android no longer supports building for Python 3 using the CrystaX
NDK. The last python-for-android version supporting CrystaX was `0.7.0 <https://github.com/kivy/python-for-android/archive/0.7.0.zip>`__
.. _bootstrap_build_options:
@ -65,7 +34,7 @@ sdl2
~~~~
Use this with ``--bootstrap=sdl2``, or just include the
``sdl2`` recipe, e.g. ``--requirements=sdl2,python2``.
``sdl2`` recipe, e.g. ``--requirements=sdl2,python3``.
SDL2 is a popular cross-platform depelopment library, particularly for
games. It has its own Android project support, which
@ -85,7 +54,71 @@ The sdl2 bootstrap supports the following additional command line
options (this list may not be exhaustive):
- ``--private``: The directory containing your project files.
- ``--package``: The Java package name for your project. Choose e.g. ``org.example.yourapp``.
- ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``.
- ``--name``: The app name.
- ``--version``: The version number.
- ``--orientation``: Usually one of ``portait``, ``landscape``,
``sensor`` to automatically rotate according to the device
orientation, or ``user`` to do the same but obeying the user's
settings. The full list of valid options is given under
``android:screenOrientation`` in the `Android documentation
<https://developer.android.com/guide/topics/manifest/activity-element.html>`__.
- ``--icon``: A path to the png file to use as the application icon.
- ``--permission``: A permission name for the app,
e.g. ``--permission VIBRATE``. For multiple permissions, add
multiple ``--permission`` arguments.
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
- ``--presplash``: A path to the image file to use as a screen while
the application is loading.
- ``--presplash-color``: The presplash screen background color, of the
form ``#RRGGBB`` or a color name ``red``, ``green``, ``blue`` etc.
- ``--presplash-lottie``: use a lottie (json) file as a presplash animation. If
used, this will replace the static presplash image.
- ``--wakelock``: If the argument is included, the application will
prevent the device from sleeping.
- ``--window``: If the argument is included, the application will not
cover the Android status bar.
- ``--blacklist``: The path to a file containing blacklisted patterns
that will be excluded from the final APK. Defaults to ``./blacklist.txt``.
- ``--whitelist``: The path to a file containing whitelisted patterns
that will be included in the APK even if also blacklisted.
- ``--add-jar``: The path to a .jar file to include in the APK. To
include multiple jar files, pass this argument multiple times.
- ``--intent-filters``: A file path containing intent filter xml to be
included in AndroidManifest.xml.
- ``--service``: A service name and the Python script it should
run. See :ref:`arbitrary_scripts_services`.
- ``--add-source``: Add a source directory to the app's Java code.
- ``--no-byte-compile-python``: Skip byte compile for .py files.
- ``--enable-androidx``: Enable AndroidX support library.
- ``--add-resource``: Put this file or directory in the apk res directory.
webview
~~~~~~~
You can use this with ``--bootstrap=webview``, or include the
``webviewjni`` recipe, e.g. ``--requirements=webviewjni,python3``.
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.
.. note:: Your Flask script must start the webserver *without*
:code:``debug=True``. Debug mode doesn't seem to work on
Android due to use of a subprocess.
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.
- ``--private``: The directory containing your project files.
- ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``.
- ``--name``: The app name.
- ``--version``: The version number.
- ``--orientation``: Usually one of ``portait``, ``landscape``,
@ -99,66 +132,6 @@ options (this list may not be exhaustive):
e.g. ``--permission VIBRATE``. For multiple permissions, add
multiple ``--permission`` arguments.
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
- ``--presplash``: A path to the image file to use as a screen while
the application is loading.
- ``--presplash-color``: The presplash screen background color, of the
form ``#RRGGBB`` or a color name ``red``, ``green``, ``blue`` etc.
- ``--wakelock``: If the argument is included, the application will
prevent the device from sleeping.
- ``--window``: If the argument is included, the application will not
cover the Android status bar.
- ``--blacklist``: The path to a file containing blacklisted patterns
that will be excluded from the final APK. Defaults to ``./blacklist.txt``.
- ``--whitelist``: The path to a file containing whitelisted patterns
that will be included in the APK even if also blacklisted.
- ``--add-jar``: The path to a .jar file to include in the APK. To
include multiple jar files, pass this argument multiple times.
- ``--intent-filters``: A file path containing intent filter xml to be
included in AndroidManifest.xml.
- ``--service``: A service name and the Python script it should
run. See :ref:`arbitrary_scripts_services`.
- ``--add-source``: Add a source directory to the app's Java code.
- ``--no-compile-pyo``: Do not optimise .py files to .pyo.
webview
~~~~~~~
You can use this with ``--bootstrap=webview``, or include the
``webviewjni`` recipe, e.g. ``--requirements=webviewjni,python2``.
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.
.. note:: Your Flask script must start the webserver *without*
:code:``debug=True``. Debug mode doesn't seem to work on
Android due to use of a subprocess.
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.
- ``--private``: The directory containing your project files.
- ``--package``: The Java package name for your project. Choose e.g. ``org.example.yourapp``.
- ``--name``: The app name.
- ``--version``: The version number.
- ``--orientation``: Usually one of ``portait``, ``landscape``,
``sensor`` to automatically rotate according to the device
orientation, or ``user`` to do the same but obeying the user's
settings. The full list of valid options is given under
``android:screenOrientation`` in the `Android documentation
<https://developer.android.com/guide/topics/manifest/activity-element.html>`__.
- ``--icon``: A path to the png file to use as the application icon.
- ``-- permission``: A permission name for the app,
e.g. ``--permission VIBRATE``. For multiple permissions, add
multiple ``--permission`` arguments.
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
- ``--presplash``: A path to the image file to use as a screen while
the application is loading.
- ``--presplash-color``: The presplash screen background color, of the
@ -182,58 +155,48 @@ ready.
access. Defaults to 5000.
pygame
~~~~~~
service_library
~~~~~~~~~~~~~~~
You can use this with ``--bootstrap=pygame``, or simply include the
``pygame`` recipe in your ``--requirements``.
You can use this with ``--bootstrap=service_library`` option.
The pygame bootstrap is the original backend used by Kivy, and still
works fine for use with Kivy apps. It may also work for pure pygame
apps, but hasn't been developed with this in mind.
This bootstrap will eventually be deprecated in favour of sdl2, but
not before the sdl2 bootstrap includes all the features that would be
lost.
Build options
%%%%%%%%%%%%%
The pygame bootstrap supports the following additional command line
options (this list may not be exhaustive):
This bootstrap can be used together with ``aar`` output target to generate
a library, containing Python services that can be used with other build
systems and frameworks.
- ``--private``: The directory containing your project files.
- ``--dir``: The directory containing your project files if you want
them to be unpacked to the external storage directory rather than
the app private directory.
- ``--package``: The Java package name for your project. Choose e.g. ``org.example.yourapp``.
- ``--name``: The app name.
- ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``.
- ``--name``: The library name.
- ``--version``: The version number.
- ``--orientation``: One of ``portait``, ``landscape`` or ``sensor``
to automatically rotate according to the device orientation.
- ``--icon``: A path to the png file to use as the application icon.
- ``--ignore-path``: A path to ignore when including the app
files. Pass multiple times to ignore multiple paths.
- ``-- permission``: A permission name for the app,
e.g. ``--permission VIBRATE``. For multiple permissions, add
multiple ``--permission`` arguments.
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
- ``--presplash``: A path to the image file to use as a screen while
the application is loading.
- ``--wakelock``: If the argument is included, the application will
prevent the device from sleeping.
- ``--window``: If the argument is included, the application will not
cover the Android status bar.
- ``--blacklist``: The path to a file containing blacklisted patterns
that will be excluded from the final APK. Defaults to ``./blacklist.txt``.
- ``--whitelist``: The path to a file containing whitelisted patterns
that will be included in the APK even if also blacklisted.
- ``--add-jar``: The path to a .jar file to include in the APK. To
include multiple jar files, pass this argument multiple times.
- ``--intent-filters``: A file path containing intent filter xml to be
included in AndroidManifest.xml.
- ``--service``: A service name and the Python script it should
run. See :ref:`arbitrary_scripts_services`.
- ``--blacklist``: The path to a file containing blacklisted patterns
that will be excluded from the final AAR. Defaults to ``./blacklist.txt``.
- ``--whitelist``: The path to a file containing whitelisted patterns
that will be included in the AAR even if also blacklisted.
- ``--add-jar``: The path to a .jar file to include in the APK. To
include multiple jar files, pass this argument multiple times.
- ``add-source``: Add a source directory to the app's Java code.
- ``--compile-pyo``: Optimise .py files to .pyo.
- ``--resource``: A key=value pair to add in the string.xml resource file.
Requirements blacklist (APK size optimization)
----------------------------------------------
To optimize the size of the `.apk` file that p4a builds for you,
you can **blacklist** certain core components. Per default, p4a
will add python *with batteries included* as would be expected on
desktop, including openssl, sqlite3 and other components you may
not use.
To blacklist an item, specify the ``--blacklist-requirements`` option::
p4a apk ... --blacklist-requirements=sqlite3
At the moment, the following core components can be blacklisted
(if you don't want to use them) to decrease APK size:
- ``android`` disables p4a's android module (see :ref:`reference-label-for-android-module`)
- ``libffi`` disables ctypes stdlib module
- ``openssl`` disables ssl stdlib module
- ``sqlite3`` disables sqlite3 stdlib module

View file

@ -67,19 +67,19 @@ supply those that you need.
distribution must contain, as a comma separated list. These must be
names of recipes or the pypi names of Python modules.
``--force_build BOOL``
``--force-build BOOL``
Whether the distribution must be compiled from scratch.
``--arch``
The architecture to build for. Currently only one architecture can be
targeted at a time, and a given distribution can only include one architecture.
The architecture to build for. You can specify multiple architectures to build for
at the same time. As an example ``p4a ... --arch arm64-v8a --arch armeabi-v7a ...``
will build a distribution for both ``arm64-v8a`` and ``armeabi-v7a``.
``--bootstrap BOOTSTRAP``
The Java bootstrap to use for your application. You mostly don't
need to worry about this or set it manually, as an appropriate
bootstrap will be chosen from your ``--requirements``. Current
choices are ``sdl2`` or ``pygame``; ``sdl2`` is experimental but
preferable where possible.
choices are ``sdl2`` (used with Kivy and most other apps) or ``webview``.
.. note:: These options are preliminary. Others will include toggles

View file

@ -1,8 +1,231 @@
Contributing
============
Development and Contributing
============================
The development of python-for-android is managed by the Kivy team `via
Github <https://github.com/kivy/python-for-android>`_.
Issues and pull requests are welcome via the integrated `issue tracker
<https://github.com/kivy/python-for-android/issues>`_.
Read on for more information about how we manage development and
releases, but don't worry about the details! Pull requests are welcome
and we'll deal with the rest.
Development model
-----------------
python-for-android is developed using the following model:
- The ``master`` branch always represents the latest stable release.
- The ``develop`` branch is the most up to date with new contributions.
- Releases happen periodically, and consist of merging the current ``develop`` branch into ``master``.
For reference, this is based on a `Git flow
<https://nvie.com/posts/a-successful-git-branching-model/>`__ model,
although we don't follow this religiously.
Versioning
----------
python-for-android releases currently use `calendar versioning
<https://calver.org/>`__. Release numbers are of the form
YYYY.MM.DD. We aim to create a new release every four weeks, but more
frequent releases are also possible.
We use calendar versioning because in practice, changes in
python-for-android are often driven by updates or adjustments in the
Android build tools. It's usually best for users to be working from
the latest release. We try to maintain backwards compatibility even
while internals are changing.
Creating a new release
----------------------
New releases follow these steps:
- Create a new branch ``release-YYYY.MM.DD`` based on the ``develop`` branch.
- ``git checkout -b release-YYYY.MM.DD develop``
- Create a Github pull request to merge ``release-YYYY.MM.DD`` into ``master``.
- Complete all steps in the `release checklist <release_checklist_>`_,
and document this in the pull request (copy the checklist into the PR text)
At this point, wait for reviewer approval and conclude any discussion that arises. To complete the release:
- Merge the release branch to the ``master`` branch.
- Also merge the release branch to the ``develop`` branch.
- Tag the release commit in ``master``, with tag ``vYYYY.MM.DD``. Include a short summary of the changes.
- Release distributions and PyPI upload should be `handled by the CI
<https://github.com/kivy/python-for-android/blob/v2020.04.29/.travis.yml#L60-L70>`_.
- Add to the Github release page (see e.g. `this example <https://github.com/kivy/python-for-android/releases/tag/v2019.06.06>`__):
- The python-for-android README summary
- A short list of major changes in this release, if any
- A changelog summarising merge commits since the last release
- The release sdist and wheel(s)
.. _release_checklist:
Release checklist
~~~~~~~~~~~~~~~~~
::
- [ ] Check that the builds are passing
- [ ] [GitHub Action](https://github.com/kivy/python-for-android/actions)
- [ ] Run the tests locally via `tox`: this performs some long-running tests that are skipped on github-actions.
- [ ] Build and run the [on_device_unit_tests](https://github.com/kivy/python-for-android/tree/master/testapps/on_device_unit_tests) app using buildozer. Check that they all pass.
- [ ] Build (or download from github actions) and run the following [testapps](https://github.com/kivy/python-for-android/tree/master/testapps/on_device_unit_tests) for arch `armeabi-v7a` and `arm64-v8a`:
- [ ] on_device_unit_tests
- [ ] `armeabi-v7a` (`cd testapps/on_device_unit_tests && PYTHONPATH=.:../../ python3 setup.py apk --ndk-dir=<your-ndk-dir> --sdk-dir=<your-sdk-dir> --arch=armeabi-v7a --debug`)
- [ ] `arm64-v8a` (`cd testapps/on_device_unit_tests && PYTHONPATH=.:../../ python3 setup.py apk --ndk-dir=<your-ndk-dir> --sdk-dir=<your-sdk-dir> --arch=arm64-v8a --debug`)
- [ ] Check that the version number is correct
How python-for-android uses `pip`
---------------------------------
*Last update: July 2019*
This section is meant to provide a quick summary how
p4a (=python-for-android) uses pip and python packages in
its build process.
**It is written for a python
packagers point of view, not for regular end users or
contributors,** to assist with making pip developers and
other packaging experts aware of p4a's packaging needs.
Please note this section just attempts to neutrally list the
current mechanisms, so some of this isn't necessarily meant
to stay but just how things work inside p4a in
this very moment.
Basic concepts
~~~~~~~~~~~~~~
*(This part repeats other parts of the docs, for the sake of
making this a more independent read)*
p4a builds & packages a python application for use on Android.
It does this by providing a Java wrapper, and for graphical applications
an SDL2-based wrapper which can be used with the kivy UI toolkit if
desired (or alternatively just plain PySDL2). Any such python application
will of course have further library dependencies to do its work.
p4a supports two types of package dependencies for a project:
**Recipe:** install script in custom p4a format. Can either install
C/C++ or other things that cannot be pulled in via pip, or things
that can be installed via pip but break on android by default.
These are maintained primarily inside the p4a source tree by p4a
contributors and interested folks.
**Python package:** any random pip python package can be directly
installed if it doesn't need adjustments to work for Android.
p4a will map any dependency to an internal recipe if present, and
otherwise use pip to obtain it regularly from whatever external source.
Install process regarding packages
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The install/build process of a p4a project, as triggered by the
`p4a apk` command, roughly works as follows in regards to python
packages:
1. The user has specified a project folder to install. This is either
just a folder with python scripts and a `main.py`, or it may
also have a `pyproject.toml` for a more standardized install.
2. Dependencies are collected: they can be either specified via
``--requirements`` as a list of names or pip-style URLs, or p4a
can optionally scan them from a project folder via the
pep517 library (if there is a `pyproject.toml` or `setup.py`).
3. The collected dependencies are mapped to p4a's recipes if any are
available for them, otherwise they're kept around as external
regular package references.
4. All the dependencies mapped to recipes are built via p4a's internal
mechanisms to build these recipes. (This may or may not indirectly
use pip, depending on whether the recipe wraps a python package
or not and uses pip to install or not.)
5. **If the user has specified to install the project in standardized
ways,** then the `setup.py`/whatever build system
of the project will be run. This happens with cross compilation set up
(`CC`/`CFLAGS`/... set to use the
proper toolchain) and a custom site-packages location.
The actual comand is a simple `pip install .` in the project folder
with some extra options: e.g. all dependencies that were already
installed by recipes will be pinned with a `-c` constraints file
to make sure pip won't install them, and build isolation will be
disabled via ``--no-build-isolation`` so pip doesn't reinstall
recipe-packages on its own.
**If the user has not specified to use standardized build approaches**,
p4a will simply install all the remaining dependencies that weren't
mapped to recipes directly and just plain copy in the user project
without installing. Any `setup.py` or `pyproject.toml` of the user
project will then be ignored in this step.
6. Google's gradle is invoked to package it all up into an `.apk`.
Overall process / package relevant notes for p4a
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here are some common things worth knowing about python-for-android's
dealing with python packages:
- Packages will work fine without a recipe if they would also build
on Linux ARM, don't use any API not available in the NDK if they
use native code, and don't use any weird compiler flags the toolchain
doesn't like if they use native code. The package also needs to
work with cross compilation.
- There is currently no easy way for a package to know it is being
cross-compiled (at least that we know of) other than examining the
`CC` compiler that was set, or that it is being cross-compiled for
Android specifically. If that breaks a package it currently needs
to be worked around with a recipe.
- If a package does **not** work, p4a developers will often create a
recipe instead of getting upstream to fix it because p4a simply
is too niche.
- Most packages without native code will just work out of the box.
Many with native code tend not to, especially if complex, e.g. numpy.
- Anything mapped to a p4a recipe cannot be just reinstalled by pip,
specifically also not inside build isolation as a dependency.
(It *may* work if the patches of the recipe are just relevant
to fix runtime issues.)
Therefore as of now, the best way to deal with this limitation seems
to be to keep build isolation always off.
Ideas for the future regarding packaging
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- We in overall prefer to use the recipe mechanism less if we can.
In overall the recipes are just a collection of workarounds.
It may look quite hacky from the outside, since p4a
version pins recipe-wrapped packages usually to make the patches reliably
apply. This creates work for the recipes to be kept up-to-date, and
obviously this approach doesn't scale too well. However, it has ended
up as a quite practical interims solution until better ways are found.
- Obviously, it would be nice if packages could know they are being
cross-compiled, and for Android specifically. We aren't currently aware
of a good mechanism for that.
- If pip could actually run the recipes (instead of p4a wrapping pip and
doing so) then this might even allow build isolation to work - but
this might be too complex to get working. It might be more practical
to just gradually reduce the reliance on recipes instead and make
more packages work out of the box. This has been done e.g. with
improvements to the cross-compile environment being set up automatically,
and we're open for any ideas on how to improve this.

View file

@ -2,8 +2,68 @@
distutils/setuptools integration
================================
Instead of running p4a via the command line, you can integrate with
distutils and setup.py.
Have `p4a apk` run setup.py (replaces ``--requirements``)
---------------------------------------------------------
If your project has a `setup.py` file, then it can be executed by
`p4a` when your app is packaged such that your app properly ends up
in the packaged site-packages. (Use ``--use-setup-py`` to enable this,
``--ignore-setup-py`` to prevent it)
This is functionality to run **setup.py INSIDE `p4a apk`,** as opposed
to the other section below, which is about running
*p4a inside setup.py*.
This however has these caveats:
- **Only your ``main.py`` from your app's ``--private`` data is copied
into the .apk!** Everything else needs to be installed by your
``setup.py`` into the site-packages, or it won't be packaged.
- All dependencies that map to recipes can only be pinned to exact
versions, all other constraints will either just plain not work
or even cause build errors. (Sorry, our internal processing is
just not smart enough to honor them properly at this point)
- The dependency analysis at the start may be quite slow and delay
your build
Reasons why you would want to use a `setup.py` to be processed (and
omit specifying ``--requirements``):
- You want to use a more standard mechanism to specify dependencies
instead of ``--requirements``
- You already use a `setup.py` for other platforms
- Your application imports itself
in a way that won't work unless installed to site-packages)
Reasons **not** to use a `setup.py` (that is to use the usual
``--requirements`` mechanism instead):
- You don't use a `setup.py` yet, and prefer the simplicity of
just specifying ``--requirements``
- Your `setup.py` assumes a desktop platform and pulls in
Android-incompatible dependencies, and you are not willing
to change this, or you want to keep it separate from Android
deployment for other organizational reasons
- You need data files to be around that aren't installed by
your `setup.py` into the site-packages folder
Use your setup.py to call p4a
-----------------------------
Instead of running p4a via the command line, you can call it via
`setup.py` instead, by it integrating with distutils and setup.py.
This is functionality to run **p4a INSIDE setup.py,** as opposed
to the other section above, which is about running
*setup.py inside `p4a apk`*.
The base command is::
@ -35,7 +95,7 @@ The Android package name uses ``org.test.lowercaseappname``
if not set explicitly.
The ``--private`` argument is set automatically using the
package_data, you should *not* set this manually.
package_data. You should *not* set this manually.
The target architecture defaults to ``--armeabi``.
@ -44,7 +104,7 @@ All of these automatic arguments can be overridden by passing them manually on t
python setup.py apk --name="Testapp Setup" --version=2.5
Adding p4a arguments in setup.py
--------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Instead of providing extra arguments on the command line, you can
store them in setup.py by passing the ``options`` parameter to
@ -54,7 +114,7 @@ store them in setup.py by passing the ``options`` parameter to
from setuptools import find_packages
options = {'apk': {'debug': None, # use None for arguments that don't pass a value
'requirements': 'sdl2,pyjnius,kivy,python2',
'requirements': 'sdl2,pyjnius,kivy,python3',
'android-api': 19,
'ndk-dir': '/path/to/ndk',
'dist-name': 'bdisttest',
@ -79,7 +139,7 @@ setup.py apk``. Any options passed on the command line will override
these values.
Adding p4a arguments in setup.cfg
---------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can also provide p4a arguments in the setup.cfg file, as normal
for distutils. The syntax is::

68
p4a/doc/source/docker.rst Normal file
View file

@ -0,0 +1,68 @@
.. _docker:
Docker
======
Currently we use a containerized build for testing Python for Android recipes.
Docker supports three big platforms either directly with the kernel or via
using headless VirtualBox and a small distro to run itself on.
While this is not the actively supported way to build applications, if you are
willing to play with the approach, you can use the ``Dockerfile`` to build
the Docker image we use for CI builds and create an Android
application with that in a container. This approach allows you to build Android
applications on all platforms Docker engine supports. These steps assume you
already have Docker preinstalled and set up.
.. warning::
This approach is highly space unfriendly! The more layers (``commit``) or
even Docker images (``build``) you create the more space it'll consume.
Within the Docker image there is Android SDK and NDK + various dependencies.
Within the custom diff made by building the distribution there is another
big chunk of space eaten. The very basic stuff such as a distribution with:
CPython 3, setuptools, Python for Android ``android`` module, SDL2 (+ deps),
PyJNIus and Kivy takes almost 2 GB. Check your free space first!
1. Clone the repository::
git clone https://github.com/kivy/python-for-android
2. Build the image with name ``p4a``::
docker build --tag p4a .
.. note::
You need to be in the ``python-for-android`` for the Docker build context
and you can optionally use ``--file`` flag to specify the path to the
``Dockerfile`` location.
3. Create a container from ``p4a`` image with copied ``testapps`` folder
in the image mounted to the same one in the cloned repo on the host::
docker run \
--interactive \
--tty \
--volume ".../testapps":/home/user/testapps \
p4a sh -c
'. venv/bin/activate \
&& cd testapps \
&& python setup_testapp_python3.py apk \
--sdk-dir $ANDROID_SDK_HOME \
--ndk-dir $ANDROID_NDK_HOME'
.. note::
On Windows you might need to use quotes and forward-slash path for volume
"/c/Users/.../python-for-android/testapps":/home/user/testapps
.. warning::
On Windows ``gradlew`` will attempt to use 'bash\r' command which is
a result of Windows line endings. For that you'll need to install
``dos2unix`` package into the image.
4. Preserve the distribution you've already built (optional, but recommended):
docker commit $(docker ps --last=1 --quiet) my_p4a_dist
5. Find the ``.APK`` file on this location::
ls -lah testapps

View file

@ -4,7 +4,7 @@
"private": true,
"dependencies": {},
"devDependencies": {
"grunt": "~0.4.1",
"grunt": ">=1.3.0",
"grunt-contrib-sass": "~0.7.2",
"grunt-contrib-watch": "~0.4.3",
"grunt-contrib-connect": "0.5.0",

View file

@ -2,16 +2,16 @@ python-for-android
==================
python-for-android is an open source build tool to let you package
Python code into standalone android APKs that can be passed around,
Python code into standalone android APKs. These can be passed around,
installed, or uploaded to marketplaces such as the Play Store just
like any other Android app. This tool was originally developed for the
`Kivy cross-platform graphical framework <http://kivy.org/#home>`_,
but now supports multiple bootstraps and can be easily extended to
package other types of Python app for Android.
package other types of Python apps for Android.
python-for-android supports two major operations; first, it can
compile the Python interpreter, its dependencies, backend libraries
and python code for Android devices. This stage is fully customisable,
and python code for Android devices. This stage is fully customisable:
you can install as many or few components as you like. The result is
a standalone Android project which can be used to generate any number
of different APKs, even with different names, icons, Python code etc.
@ -29,15 +29,16 @@ Contents
quickstart
buildoptions
commands
apis
launcher
distutils
recipes
bootstraps
services
apis
troubleshooting
launcher
docker
contribute
old_toolchain/index.rst
testing_pull_requests
Indices and tables

View file

@ -4,7 +4,7 @@ Launcher
========
The Kivy Launcher is an Android application that can run any Kivy app
stored in `kivy` folder on SD Card. You can download the latest stable
stored in the `kivy` folder on the SD Card. You can download the latest stable
version for your android device from the
`Play Store <https://play.google.com/store/apps/details?id=org.kivy.pygame>`_.
@ -13,7 +13,7 @@ permissions, usually listed in the description in the store. Those
aren't always enough for an application to run or even launch if you
work with other dependencies that are not packaged.
The Kivy Launcher is intended for quick and simple testing, for
The Kivy Launcher is intended for quick and simple testing. For
anything more advanced we recommend building your own APK with
python-for-android.
@ -22,7 +22,7 @@ Building
The Kivy Launcher is built using python-for-android. To get the most recent
versions of packages you need to clean them first, so that the packager won't
grab an old (cached) package instead of fresh one.
grab an old (cached) package instead of a fresh one.
.. highlight:: none
@ -36,7 +36,7 @@ grab an old (cached) package instead of fresh one.
--name="App name" \
--version=x.y.z \
--android_api XY \
--bootstrap=pygame or sdl2 \
--bootstrap=sdl2 \
--launcher \
--minsdk 13
@ -48,7 +48,7 @@ grab an old (cached) package instead of fresh one.
.. warning::
Do not use any of `--private`, `--public`, `--dir` or other arguments for
adding `main.py` or `main.pyo` to the app. The argument `--launcher` is
adding `main.py` or `main.pyc` to the app. The argument `--launcher` is
above them and tells the p4a to build the launcher version of the APK.
Usage
@ -78,8 +78,8 @@ to change other settings.
After you set your `android.txt` file, you can now run the launcher
and start any available app from the list.
To differentiate between apps in ``/sdcard/kivy`` you can include an icon
named ``icon.png`` to the folder. The icon should be a square.
To differentiate between apps in ``/sdcard/kivy``, you can include an icon
named ``icon.png`` in the folder. The icon should be a square.
Release on the market
---------------------
@ -91,12 +91,8 @@ it changes quickly and needs testing.
Source code
-----------
.. |renpy| replace:: pygame org.renpy.android
.. |kivy| replace:: sdl2 org.kivy.android
.. _renpy:
https://github.com/kivy/python-for-android/tree/master/\
pythonforandroid/bootstraps/pygame/build/src/org/renpy/android
.. _sdl2:
https://github.com/kivy/python-for-android/tree/master/\
pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android

View file

@ -9,27 +9,37 @@ for android as p4a in this documentation.
Concepts
--------
- requirements: For p4a, your applications dependencies are
requirements similar to the standard `requirements.txt`, but with
one difference: p4a will search for a recipe first instead of
installing requirements with pip.
*Basic:*
- recipe: A recipe is a file that defines how to compile a
requirement. Any libraries that have a Python extension *must* have
a recipe in p4a, or compilation will fail. If there is no recipe for
a requirement, it will be downloaded using pip.
- **requirements:** For p4a, all your app's dependencies must be specified
via ``--requirements`` similar to the standard `requirements.txt`.
(Unless you specify them via a `setup.py`/`install_requires`)
All dependencies will be mapped to "recipes" if any exist, so that
many common libraries will just work. See "recipe" below for details.
- build: A build refers to a compiled recipe.
- **distribution:** A distribution is the final "build" of your
compiled project + requirements, as an Android project assembled by
p4a that can be turned directly into an APK. p4a can contain multiple
distributions with different sets of requirements.
- distribution: A distribution is the final "build" of all your
compiled requirements, as an Android project that can be turned
directly into an APK. p4a can contain multiple distributions with
different sets of requirements.
- **build:** A build refers to a compiled recipe or distribution.
- bootstrap: A bootstrap is the app backend that will start your
application. Your application could use SDL2 as a base, or Pygame,
or a web backend like Flask with a WebView bootstrap. Different
bootstraps can have different build options.
- **bootstrap:** A bootstrap is the app backend that will start your
application. The default for graphical applications is SDL2.
You can also use e.g. the webview for web apps, or service_only/service_library for
background services. Different bootstraps have different additional
build options.
*Advanced:*
- **recipe:**
A recipe is a file telling p4a how to install a requirement
that isn't by default fully Android compatible.
This is often necessary for Cython or C/C++-using python extensions.
p4a has recipes for many common libraries already included, and any
dependency you specified will be automatically mapped to its recipe.
If a dependency doesn't work and has no recipe included in p4a,
then it may need one to work.
Installation
@ -38,7 +48,7 @@ Installation
Installing p4a
~~~~~~~~~~~~~~
p4a is now available on on Pypi, so you can install it using pip::
p4a is now available on Pypi, so you can install it using pip::
pip install python-for-android
@ -51,37 +61,54 @@ Installing Dependencies
p4a has several dependencies that must be installed:
- git
- ant
- python2
- autoconf (for libffi and other recipes)
- automake
- ccache (optional)
- cmake (required for some native code recipes like jpeg's recipe)
- cython (can be installed via pip)
- a Java JDK (e.g. openjdk-7)
- zlib (including 32 bit)
- gcc
- git
- libncurses (including 32 bit)
- libtool (for libffi and recipes)
- libssl-dev (for TLS/SSL support on hostpython3 and recipe)
- openjdk-8
- patch
- python3
- unzip
- virtualenv (can be installed via pip)
- ccache (optional)
- autoconf (for ffpyplayer_codecs recipe)
- libtool (for ffpyplayer_codecs recipe)
- zlib (including 32 bit)
- zip
On recent versions of Ubuntu and its derivatives you may be able to
install most of these with::
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y build-essential ccache git zlib1g-dev python2.7 python2.7-dev libncurses5:i386 libstdc++6:i386 zlib1g:i386 openjdk-7-jdk unzip ant ccache autoconf libtool
sudo apt-get install -y build-essential ccache git zlib1g-dev python3 python3-dev libncurses5:i386 libstdc++6:i386 zlib1g:i386 openjdk-8-jdk unzip ant ccache autoconf libtool libssl-dev
On Arch Linux (64 bit) you should be able to run the following to
On Arch Linux you should be able to run the following to
install most of the dependencies (note: this list may not be
complete). gcc-multilib will conflict with (and replace) gcc if not
already installed. If your installation is already 32-bit, install the
same packages but without ``lib32-`` or ``-multilib``::
complete)::
sudo pacman -S jdk7-openjdk python2 python2-pip python2-kivy mesa-libgl lib32-mesa-libgl lib32-sdl2 lib32-sdl2_image lib32-sdl2_mixer sdl2_ttf unzip gcc-multilib gcc-libs-multilib
sudo pacman -S core/autoconf core/automake core/gcc core/make core/patch core/pkgconf extra/cmake extra/jdk8-openjdk extra/python-pip extra/unzip extra/zip
On macOS::
brew install autoconf automake libtool openssl pkg-config
brew tap homebrew/cask-versions
brew install --cask homebrew/cask-versions/adoptopenjdk8
Installing Android SDK
~~~~~~~~~~~~~~~~~~~~~~
.. warning::
python-for-android is often picky about the **SDK/NDK versions.**
Pick the recommended ones from below to avoid problems.
Basic SDK install
`````````````````
You need to download and unpack the Android SDK and NDK to a directory (let's say $HOME/Documents/):
- `Android SDK <https://developer.android.com/studio/index.html>`_
@ -93,53 +120,81 @@ named ``tools``, and you will need to run extra commands to install
the SDK packages needed.
For Android NDK, note that modern releases will only work on a 64-bit
operating system. If you are using a 32-bit distribution (or hardware),
the latest useable NDK version is r10e, which can be downloaded here:
operating system. **The minimal, and recommended, NDK version to use is r25b:**
- `Legacy 32-bit Linux NDK r10e <http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86.bin>`_
- `Go to ndk downloads page <https://developer.android.com/ndk/downloads/>`_
- Windows users should create a virtual machine with an GNU Linux os
installed, and then you can follow the described instructions from within
your virtual machine.
First, install a platform to target (you can also replace ``19`` with
a different platform number, this will be used again later)::
$SDK_DIR/tools/bin/sdkmanager "platforms;android-19"
Platform and build tools
````````````````````````
First, install an API platform to target. **The recommended *target* API
level is 27**, you can replace it with a different number but
keep in mind other API versions are less well-tested and older devices
are still supported down to the **recommended specified *minimum*
API/NDK API level 21**::
$SDK_DIR/tools/bin/sdkmanager "platforms;android-27"
Second, install the build-tools. You can use
``$SDK_DIR/tools/bin/sdkmanager --list`` to see all the
possibilities, but 26.0.2 is the latest version at the time of writing::
possibilities, but 28.0.2 is the latest version at the time of writing::
$SDK_DIR/tools/bin/sdkmanager "build-tools;26.0.2"
$SDK_DIR/tools/bin/sdkmanager "build-tools;28.0.2"
Then, you can edit your ``~/.bashrc`` or other favorite shell to include new environment variables necessary for building on android::
Configure p4a to use your SDK/NDK
`````````````````````````````````
Then, you can edit your ``~/.bashrc`` or other favorite shell to include new environment
variables necessary for building on android::
# Adjust the paths!
export ANDROIDSDK="$HOME/Documents/android-sdk-21"
export ANDROIDNDK="$HOME/Documents/android-ndk-r10e"
export ANDROIDAPI="19" # Minimum API version your application require
export ANDROIDSDK="$HOME/Documents/android-sdk-27"
export ANDROIDNDK="$HOME/Documents/android-ndk-r23b"
export ANDROIDAPI="27" # Target API version of your application
export NDKAPI="21" # Minimum supported API version of your application
export ANDROIDNDKVER="r10e" # Version of the NDK you installed
You have the possibility to configure on any command the PATH to the SDK, NDK and Android API using:
- :code:`--sdk_dir PATH` as an equivalent of `$ANDROIDSDK`
- :code:`--ndk_dir PATH` as an equivalent of `$ANDROIDNDK`
- :code:`--android_api VERSION` as an equivalent of `$ANDROIDAPI`
- :code:`--ndk_version PATH` as an equivalent of `$ANDROIDNDKVER`
- :code:`--sdk-dir PATH` as an equivalent of `$ANDROIDSDK`
- :code:`--ndk-dir PATH` as an equivalent of `$ANDROIDNDK`
- :code:`--android-api VERSION` as an equivalent of `$ANDROIDAPI`
- :code:`--ndk-api VERSION` as an equivalent of `$NDKAPI`
- :code:`--ndk-version VERSION` as an equivalent of `$ANDROIDNDKVER`
Usage
-----
Build a Kivy application
~~~~~~~~~~~~~~~~~~~~~~~~
Build a Kivy or SDL2 application
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To build your application, you need to have a name, version, a package
identifier, and explicitly write the bootstrap you want to use, as
well as the requirements::
To build your application, you need to specify name, version, a package
identifier, the bootstrap you want to use (`sdl2` for kivy or sdl2 apps)
and the requirements::
p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My application" --version 0.1 --bootstrap=sdl2 --requirements=python2,kivy
p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My application" --version 0.1 --bootstrap=sdl2 --requirements=python3,kivy
This will first build a distribution that contains `python2` and `kivy`, and using a SDL2 bootstrap. Python2 is here explicitely written as kivy can work with python2 or python3.
**Note on** ``--requirements``: **you must add all
libraries/dependencies your app needs to run.**
Example: ``--requirements=python3,kivy,vispy``. For an SDL2 app,
`kivy` is not needed, but you need to add any wrappers you might
use (e.g. `pysdl2`).
This `p4a apk ...` command builds a distribution with `python3`,
`kivy`, and everything else you specified in the requirements.
It will be packaged using a SDL2 bootstrap, and produce
an `.apk` file.
*Compatibility notes:*
- Python 2 is no longer supported by python-for-android. The last release supporting Python 2 was v2019.10.06.
You can also use ``--bootstrap=pygame``, but this bootstrap is deprecated for use with Kivy and SDL2 is preferred.
Build a WebView application
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -150,25 +205,44 @@ well as the requirements::
p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My WebView Application" --version 0.1 --bootstrap=webview --requirements=flask --port=5000
**Please note as with kivy/SDL2, you need to specify all your
additional requirements/dependencies.**
You can also replace flask with another web framework.
Replace ``--port=5000`` with the port on which your app will serve a
website. The default for Flask is 5000.
Build an SDL2 based application
Build a Service library archive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This includes e.g. `PySDL2
<https://pysdl2.readthedocs.io/en/latest/>`__.
To build an android archive (.aar), containing an android service , you need a name, version, package identifier, explicitly use the
service_library bootstrap, and declare service entry point (See :ref:`services <arbitrary_scripts_services>` for more options), as well as the requirements and arch(s)::
To build your application, you need to have a name, version, a package
identifier, and explicitly write the sdl2 bootstrap, as well as the
requirements::
p4a aar --private $HOME/code/myapp --package=org.example.myapp --name "My library" --version 0.1 --bootstrap=service_library --requirements=python3 --release --service=myservice:service.py --arch=arm64-v8a --arch=armeabi-v7a
p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My SDL2 application" --version 0.1 --bootstrap=sdl2 --requirements=your_requirements
Add your required modules in place of ``your_requirements``,
e.g. ``--requirements=pysdl2`` or ``--requirements=vispy``.
You can then call the generated Java entrypoint(s) for your Python service(s) in other apk build frameworks.
Exporting the Android App Bundle (aab) for distributing it on Google Play
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Starting from August 2021 for new apps and from November 2021 for updates to existings apps,
Google Play Console will require the Android App Bundle instead of the long lived apk.
python-for-android handles by itself the needed work to accomplish the new requirements:
p4a aab --private $HOME/code/myapp --package=org.example.myapp --name="My App" --version 0.1 --bootstrap=sdl2 --requirements=python3,kivy --arch=arm64-v8a --arch=armeabi-v7a --release
This `p4a aab ...` command builds a distribution with `python3`,
`kivy`, and everything else you specified in the requirements.
It will be packaged using a SDL2 bootstrap, and produce
an `.aab` file that contains binaries for both `armeabi-v7a` and `arm64-v8a` ABIs.
The Android App Bundle, is supposed to be used for distributing your app.
If you need to test it locally, on your device, you can use `bundletool <https://developer.android.com/studio/command-line/bundletool>`
Other options
~~~~~~~~~~~~~
@ -195,8 +269,8 @@ Getting help
If something goes wrong and you don't know how to fix it, add the
``--debug`` option and post the output log to the `kivy-users Google
group <https://groups.google.com/forum/#!forum/kivy-users>`__ or irc
channel #kivy at irc.freenode.net .
group <https://groups.google.com/forum/#!forum/kivy-users>`__ or the
kivy `#support Discord channel <https://chat.kivy.org/>`_.
See :doc:`troubleshooting` for more information.
@ -258,9 +332,28 @@ command line. For example, you can add the options you would always
include such as::
--dist_name my_example
--android_api 19
--android_api 27
--requirements kivy,openssl
Overriding recipes sources
~~~~~~~~~~~~~~~~~~~~~~~~~~
You can override the source of any recipe using the
``$P4A_recipename_DIR`` environment variable. For instance, to test
your own Kivy branch you might set::
export P4A_kivy_DIR=/home/username/kivy
The specified directory will be copied into python-for-android instead
of downloading from the normal url specified in the recipe.
setup.py file (experimental)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If your application is also packaged for desktop using `setup.py`,
you may want to use your `setup.py` instead of the
``--requirements`` option to avoid specifying things twice.
For that purpose, check out :doc:`distutils`
Going further
~~~~~~~~~~~~~

View file

@ -12,9 +12,8 @@ to take care of compilation for any compiled components, as these must
be compiled for Android with the correct architecture.
python-for-android comes with many recipes for popular modules. No
recipe is necessary to use of Python modules with no
compiled components; these are installed automaticaly via pip.
recipe is necessary for Python modules which have no
compiled components; these are installed automatically via pip.
If you are new to building recipes, it is recommended that you first
read all of this page, at least up to the Recipe reference
documentation. The different recipe sections include a number of
@ -42,7 +41,7 @@ The basic declaration of a recipe is as follows::
patches = ['some_fix.patch'] # Paths relative to the recipe dir
depends = ['kivy', 'sdl2'] # These are just examples
conflicts = ['pygame']
conflicts = ['generickndkbuild']
recipe = YourRecipe()
@ -62,21 +61,21 @@ when the recipe is imported.
The actual build process takes place via three core methods::
def prebuild_arch(self, arch):
super(YourRecipe, self).prebuild_arch(arch)
super().prebuild_arch(arch)
# Do any pre-initialisation
def build_arch(self, arch):
super(YourRecipe, self).build_arch(arch)
super().build_arch(arch)
# Do the main recipe build
def postbuild_arch(self, arch):
super(YourRecipe, self).build_arch(arch)
super().build_arch(arch)
# Do any clearing up
These methods are always run in the listed order; prebuild, then
build, then postbuild.
If you defined an url for your recipe, you do *not* need to manually
If you defined a url for your recipe, you do *not* need to manually
download it, this is handled automatically.
The recipe will automatically be built in a special isolated build
@ -87,9 +86,9 @@ context manager defined in toolchain.py::
from pythonforandroid.toolchain import current_directory
def build_arch(self, arch):
super(YourRecipe, self).build_arch(arch)
super().build_arch(arch)
with current_directory(self.get_build_dir(arch.arch)):
with open('example_file.txt', 'w'):
with open('example_file.txt', 'w') as fileh:
fileh.write('This is written to a file within the build dir')
The argument to each method, ``arch``, is an object relating to the
@ -179,7 +178,7 @@ environment for any processes that you call. It is convenient to do
this using the ``sh`` module as follows::
def build_arch(self, arch):
super(YourRecipe, self).build_arch(arch)
super().build_arch(arch)
env = self.get_recipe_env(arch)
sh.echo('$PATH', _env=env) # Will print the PATH entry from the
# env dict
@ -192,12 +191,12 @@ its current status::
shprint(sh.echo, '$PATH', _env=env)
You can also override the ``get_recipe_env`` method to add new env
vars for the use of your recipe. For instance, the Kivy recipe does
vars for use in your recipe. For instance, the Kivy recipe does
the following when compiling for SDL2, in order to tell Kivy what
backend to use::
def get_recipe_env(self, arch):
env = super(KivySDL2Recipe, self).get_recipe_env(arch)
env = super().get_recipe_env(arch)
env['USE_SDL2'] = '1'
env['KIVY_SDL2_PATH'] = ':'.join([
@ -252,12 +251,12 @@ install`` with an appropriate environment.
For instance, the following is all that's necessary to create a recipe
for the Vispy module::
from pythonforandroid.toolchain import PythonRecipe
from pythonforandroid.recipe import PythonRecipe
class VispyRecipe(PythonRecipe):
version = 'master'
url = 'https://github.com/vispy/vispy/archive/{version}.zip'
depends = ['python2', 'numpy']
depends = ['python3', 'numpy']
site_packages_name = 'vispy'
@ -273,7 +272,7 @@ Python installation.
For reference, the code that accomplishes this is the following::
def build_arch(self, arch):
super(PythonRecipe, self).build_arch(arch)
super().build_arch(arch)
self.install_python_package()
def install_python_package(self):
@ -307,14 +306,14 @@ the cython components and to install the Python module just like a
normal PythonRecipe.
For instance, the following is all that's necessary to make a recipe
for Kivy (in this case, depending on Pygame rather than SDL2)::
for Kivy::
class KivyRecipe(CythonRecipe):
version = 'stable'
url = 'https://github.com/kivy/kivy/archive/{version}.zip'
name = 'kivy'
version = 'stable'
url = 'https://github.com/kivy/kivy/archive/{version}.zip'
name = 'kivy'
depends = ['pygame', 'pyjnius', 'android']
depends = ['sdl2', 'pyjnius']
recipe = KivyRecipe()
@ -350,12 +349,12 @@ For reference, the code that accomplishes this is the following::
shprint(sh.find, build_lib[0], '-name', '*.o', '-exec',
env['STRIP'], '{}', ';', _env=env)
The failing build and manual cythonisation is necessary, first to
The failing build and manual cythonisation is necessary, firstly to
make sure that any .pyx files have been generated by setup.py, and
second because cython isn't installed in the hostpython build.
secondly because cython isn't installed in the hostpython build.
This may actually fail if the setup.py tries to import cython before
making any pyx files (in which case it crashes too early), although
making any .pyx files (in which case it crashes too early), although
this is probably not usually an issue. If this happens to you, try
patching to remove this import or make it fail quietly.
@ -377,7 +376,7 @@ Using an NDKRecipe
------------------
If you are writing a recipe not for a Python module but for something
that would normall go in the JNI dir of an Android project (i.e. it
that would normally go in the JNI dir of an Android project (i.e. it
has an ``Application.mk`` and ``Android.mk`` that the Android build
system can use), you can use an NDKRecipe to automatically set it
up. The NDKRecipe overrides the normal ``get_build_dir`` method to
@ -427,7 +426,7 @@ overrides if you do not use them::
url = 'http://example.com/example-{version}.tar.gz'
# {version} will be replaced with self.version when downloading
depends = ['python2', 'numpy'] # A list of any other recipe names
depends = ['python3', 'numpy'] # A list of any other recipe names
# that must be built before this
# one
@ -435,29 +434,29 @@ overrides if you do not use them::
# alongside this one
def get_recipe_env(self, arch):
env = super(YourRecipe, self).get_recipe_env()
env = super().get_recipe_env(arch)
# Manipulate the env here if you want
return env
def should_build(self):
def should_build(self, arch):
# Add a check for whether the recipe is already built if you
# want, and return False if it is.
return True
def prebuild_arch(self, arch):
super(YourRecipe, self).prebuild_arch(self)
super().prebuild_arch(self)
# Do any extra prebuilding you want, e.g.:
self.apply_patch('path/to/patch.patch')
def build_arch(self, arch):
super(YourRecipe, self).build_arch(self)
super().build_arch(self)
# Build the code. Make sure to use the right build dir, e.g.
with current_directory(self.get_build_dir(arch.arch)):
sh.ls('-lathr') # Or run some commands that actually do
# something
def postbuild_arch(self, arch):
super(YourRecipe, self).prebuild_arch(self)
super().prebuild_arch(self)
# Do anything you want after the build, e.g. deleting
# unnecessary files such as documentation

View file

@ -8,9 +8,14 @@ possible to use normal multiprocessing on Android. Services are also
the only way to run code when your app is not currently opened by the user.
Services must be declared when building your APK. Each one
will have its own main.py file with the Python script to be run. You
can communicate with the service process from your app using e.g. `osc
<https://pypi.python.org/pypi/python-osc>`__ or (a heavier option)
will have its own main.py file with the Python script to be run.
Please note that python-for-android explicitly runs services as separated
processes by having a colon ":" in the beginning of the name assigned to
the ``android:process`` attribute of the ``AndroidManifest.xml`` file.
This is not the default behavior, see `Android service documentation
<https://developer.android.com/guide/topics/manifest/service-element>`__.
You can communicate with the service process from your app using e.g.
`osc <https://pypi.python.org/pypi/python-osc>`__ or (a heavier option)
`twisted <https://twistedmatrix.com/trac/>`__.
Service creation
@ -21,18 +26,16 @@ There are two ways to have services included in your APK.
Service folder
~~~~~~~~~~~~~~
This basic method works with both the new SDL2 and old Pygame
bootstraps. It is recommended to use the second method (below) where
possible.
This is the older method of handling services. It is
recommended to use the second method (below) where possible.
Create a folder named ``service`` in your app directory, and add a
file ``service/main.py``. This file should contain the Python code
that you want the service to run.
To start the service, use the :code:`start_service` function from the
:code:`android` module (included automatically with the Pygame
bootstrap, you must add it to the requirements manually with SDL2 if
you wish to use this method)::
:code:`android` module (you may need to add ``android`` to your app
requirements)::
import android
android.start_service(title='service name',
@ -44,38 +47,67 @@ you wish to use this method)::
Arbitrary service scripts
~~~~~~~~~~~~~~~~~~~~~~~~~
.. note:: This service method is *not supported* by the Pygame bootstrap.
This method is recommended for non-trivial use of services as it is
more flexible, supporting multiple services and a wider range of
options.
To create the service, create a python script with your service code
and add a :code:`--service=myservice:/path/to/myservice.py` argument
when calling python-for-android. The ``myservice`` name before the
colon is the name of the service class, via which you will interact
with it later. You can add multiple
:code:`--service` arguments to include multiple services, which you
will later be able to stop and start from your app.
and add a :code:`--service=myservice:PATH_TO_SERVICE_PY` argument
when calling python-for-android, or in buildozer.spec, a
:code:`services = myservice:PATH_TO_SERVICE_PY` [app] setting.
The ``myservice`` name before the colon is the name of the service
class, via which you will interact with it later.
The ``PATH_TO_SERVICE_PY`` is the relative path to the service entry point (like ``services/myservice.py``)
You can optionally specify the following parameters:
- :code:`:foreground` for launching a service as an Android foreground service
- :code:`:sticky` for launching a service that gets restarted by the Android OS on exit/error
Full command with all the optional parameters included would be:
:code:`--service=myservice:services/myservice.py:foreground:sticky`
You can add multiple
:code:`--service` arguments to include multiple services, or separate
them with a comma in buildozer.spec, all of which you will later be
able to stop and start from your app.
To run the services (i.e. starting them from within your main app
code), you must use PyJNIus to interact with the java class
python-for-android creates for each one, as follows::
from jnius import autoclass
service = autoclass('your.package.name.ServiceMyservice')
service = autoclass('your.package.domain.package.name.ServiceMyservice')
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
argument = ''
service.start(mActivity, argument)
Here, ``your.package.name`` refers to the package identifier of your
APK as set by the ``--package`` argument to python-for-android, and
the name of the service is ``ServiceYourservicename``, in which
``Yourservicename`` is the identifier passed to the ``--service``
argument with the first letter upper case. You must also pass the
Here, ``your.package.domain.package.name`` refers to the package identifier
of your APK.
If you are using buildozer, the identifier is set by the ``package.name``
and ``package.domain`` values in your buildozer.spec file.
The name of the service is ``ServiceMyservice``, where ``Myservice``
is the name specied by one of the ``services`` values, but with the first
letter upper case.
If you are using python-for-android directly, the identifier is set by the ``--package``
argument to python-for-android. The name of the service is ``ServiceMyservice``,
where ``Myservice`` is the identifier that was previously passed to the ``--service``
argument, but with the first letter upper case. You must also pass the
``argument`` parameter even if (as here) it is an empty string. If you
do pass it, the service can make use of this argument.
The service argument is made available to your service via the
'PYTHON_SERVICE_ARGUMENT' environment variable. It is exposed as a simple
string, so if you want to pass in multiple values, we would recommend using
the json module to encode and decode more complex data.
::
from os import environ
argument = environ.get('PYTHON_SERVICE_ARGUMENT', '')
Services support a range of options and interactions not yet
documented here but all accessible via calling other methods of the
``service`` reference.
@ -87,3 +119,14 @@ documented here but all accessible via calling other methods of the
your service folder you must use e.g. ``import service.module``
instead of ``import module``, if the service file is in the
``service/`` folder.
Service auto-restart
~~~~~~~~~~~~~~~~~~~~
It is possible to make services restart automatically when they exit by
calling ``setAutoRestartService(True)`` on the service object.
The call to this method should be done within the service code::
from jnius import autoclass
PythonService = autoclass('org.kivy.android.PythonService')
PythonService.mService.setAutoRestartService(True)

View file

@ -0,0 +1,226 @@
Testing an python-for-android pull request
==========================================
In order to test a pull request, we recommend to consider the following points:
#. of course, check if the overall thing makes sense
#. is the CI passing? if not what specifically fails
#. is it working locally at compile time?
#. is it working on device at runtime?
This document will focus on the third point:
`is it working locally at compile time?` so we will give some hints about how
to proceed in order to create a local copy of the pull requests and build an
apk. We expect that the contributors has enough criteria/knowledge to perform
the other steps mentioned, so let's begin...
To create an apk from a python-for-android pull request we contemplate three
possible scenarios:
- using python-for-android commands directly from the pull request files
that we want to test, without installing it (the recommended way for most
of the test cases)
- installing python-for-android using the github's branch of the pull request
- using buildozer and a custom app
We will explain the first two methods using one of the distributed
python-for-android test apps and we assume that you already have the
python-for-android dependencies installed. For the `buildozer` method we also
expect that you already have a a properly working app to test and a working
installation/configuration of `buildozer`. There is one step that it's shared
with all the testing methods that we propose in here...we named it
`Common steps`.
Common steps
^^^^^^^^^^^^
The first step to do it's to get a copy of the pull request, we can do it of
several ways, and that it will depend of the circumstances but all the methods
presented here will do the job, so...
Fetch the pull request by number
--------------------------------
For the example, we will use `1901` for the example) and the pull request
branch that we will use is `feature-fix-numpy`, then you will use a variation
of the following git command:
`git fetch origin pull/<#>/head:<local_branch_name>`, e.g.:
.. code-block:: bash
git fetch upstream pull/1901/head:feature-fix-numpy
.. note:: Notice that we fetch from `upstream`, since that is the original
project, where the pull request is supposed to be
.. tip:: The amount of work of some users maybe worth it to add his remote
to your fork's git configuration, to do so with the imaginary
github user `Obi-Wan Kenobi` which nickname is `obiwankenobi`, you
will do:
.. code-block:: bash
git remote add obiwankenobi https://github.com/obiwankenobi/python-for-android.git
And to fetch the pull request branch that we put as example, you
would do:
.. code-block:: bash
git fetch obiwankenobi
git checkout obiwankenobi/feature-fix-numpy
Clone the pull request branch from the user's fork
--------------------------------------------------
Sometimes you may prefer to use directly the fork of the user, so you will get
the nickname of the user who created the pull request, let's take the same
imaginary user than before `obiwankenobi`:
.. code-block:: bash
git clone -b feature-fix-numpy \
--single-branch \
https://github.com/obiwankenobi/python-for-android.git \
p4a-feature-fix-numpy
Here's the above command explained line by line:
- `git clone -b feature-fix-numpy`: we tell git that we want to clone the
branch named `feature-fix-numpy`
- `--single-branch`: we tell git that we only want that branch
- `https://github.com/obiwankenobi/python-for-android.git`: noticed the
nickname of the user that created the pull request: `obiwankenobi` in the
middle of the line? that should be changed as needed for each pull
request that you want to test
- `p4a-feature-fix-numpy`: the name of the cloned repository, so we can
have multiple clones of different prs in the same folder
.. note:: You can view the author/branch information looking at the
subtitle of the pull request, near the pull request status (expected
an `open` status)
Using python-for-android commands directly from the pull request files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Enter inside the directory of the cloned repository in the above
step and run p4a command with proper args, e.g. (to test an modified
`pycryptodome` recipe)
.. code-block:: bash
cd p4a-feature-fix-numpy
PYTHONPATH=. python3 -m pythonforandroid.toolchain apk \
--private=testapps/on_device_unit_tests/test_app \
--dist-name=dist_unit_tests_app_pycryptodome \
--package=org.kivy \
--name=unit_tests_app_pycryptodome \
--version=0.1 \
--requirements=sdl2,pyjnius,kivy,python3,pycryptodome \
--ndk-dir=/media/DEVEL/Android/android-ndk-r20 \
--sdk-dir=/media/DEVEL/Android/android-sdk-linux \
--android-api=27 \
--arch=arm64-v8a \
--permission=VIBRATE \
--debug
Things that you should know:
- The example above will build an test app we will make use of the files of
the `on device unit tests` test app but we don't use the setup
file to build it so we must tell python-for-android what we want via
arguments
- be sure to at least edit the following arguments when running the above
command, since the default set in there it's unlikely that match your
installation:
- `--ndk-dir`: An absolute path to your android's NDK dir
- `--sdk-dir`: An absolute path to your android's SDK dir
- `--debug`: this one enables the debug mode of python-for-android,
which will show all log messages of the build. You can omit this
one but it's worth it to be mentioned, since this it's useful to us
when trying to find the source of the problem when things goes
wrong
- The apk generated by the above command should be located at the root of
of the cloned repository, were you run the command to build the apk
- The testapps distributed with python-for-android are located at
`testapps` folder under the main folder project
- All the builds of python-for-android are located at
`~/.local/share/python-for-android`
- You should have a downloaded copy of the android's NDK and SDK
Installing python-for-android using the github's branch of the pull request
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Enter inside the directory of the cloned repository mentioned in
`Common steps` and install it via pip, e.g.:
.. code-block:: bash
cd p4a-feature-fix-numpy
pip3 install . --upgrade --user
- Now, go inside the `testapps/on_device_unit_tests` directory (we assume that
you still are inside the cloned repository)
.. code-block:: bash
cd testapps/on_device_unit_tests
- Run the build of the apk via the freshly installed copy of python-for-android
by running a similar command than below
.. code-block:: bash
python3 setup.py apk \
--ndk-dir=/media/DEVEL/Android/android-ndk-r20 \
--sdk-dir=/media/DEVEL/Android/android-sdk-linux \
--android-api=27 \
--arch=arm64-v8a \
--debug
Things that you should know:
- In the example above, we override some variables that are set in
`setup.py`, you could also override them by editing this file
- be sure to at least edit the following arguments when running the above
command, since the default set in there it's unlikely that match your
installation:
- `--ndk-dir`: An absolute path to your android's NDK dir
- `--sdk-dir`: An absolute path to your android's SDK dir
.. tip:: if you don't want to mess up with the system's python, you could do
the same steps but inside a virtualenv
.. warning:: Once you finish the pull request tests remember to go back to the
master or develop versions of python-for-android, since you just
installed the python-for-android files of the `pull request`
Using buildozer with a custom app
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Edit your `buildozer.spec` file. You should search for the key
`p4a.source_dir` and set the right value so in the example posted in
`Common steps` it would look like this::
p4a.source_dir = /home/user/p4a_pull_requests/p4a-feature-fix-numpy
- Run you buildozer command as usual, e.g.::
buildozer android debug p4a --dist-name=dist-test-feature-fix-numpy
.. note:: this method has the advantage, can be run without installing the
pull request version of python-for-android nor the android's
dependencies but has one problem...when things goes wrong you must
determine if it's a buildozer issue or a python-for-android one
.. warning:: Once you finish the pull request tests remember to comment/edit
the `p4a.source_dir` constant that you just edited to test the
pull request
.. tip:: this method it's useful for developing pull requests since you can
edit `p4a.source_dir` to point to your python-for-android fork and you
can test any branch you want only switching branches with:
`git checkout <branch-name>` from inside your python-for-android fork

View file

@ -10,8 +10,8 @@ Add the ``--debug`` option to any python-for-android command to see
full debug output including the output of all the external tools used
in the compilation and packaging steps.
If reporting a problem by email or irc, it is usually helpful to
include this full log, via e.g. a `pastebin
If reporting a problem by email or Discord, it is usually helpful to
include this full log, e.g. via a `pastebin
<http://paste.ubuntu.com/>`_ or `Github gist
<https://gist.github.com/>`_.
@ -23,7 +23,7 @@ get help with any problems using the same channels as Kivy itself:
- by email to the `kivy-users Google group
<https://groups.google.com/forum/#!forum/kivy-users>`_
- by irc in the #kivy room at irc.freenode.net
- on `#support Discord channel <https://chat.kivy.org/>`_
If you find a bug, you can also post an issue on the
`python-for-android Github page
@ -72,7 +72,7 @@ particular.
Unpacking an APK
----------------
It is sometimes useful to unpack a pacakged APK to see what is inside,
It is sometimes useful to unpack a packaged APK to see what is inside,
especially when debugging python-for-android itself.
APKs are just zip files, so you can extract the contents easily::
@ -85,33 +85,35 @@ At the top level, this will always contain the same set of files::
AndroidManifest.xml classes.dex META-INF res
assets lib YourApk.apk resources.arsc
The Python distribution is in the assets folder::
The user app data (code, images, fonts ..) is packaged into a single tarball contained in the assets folder::
$ cd assets
$ ls
private.mp3
private.tar
``private.mp3`` is actually a tarball containing all your packaged
data, and the Python distribution. Extract it::
``private.tar`` is a tarball containing all your packaged
data. Extract it::
$ tar xf private.mp3
$ tar xf private.tar
This will reveal all the Python-related files::
This will reveal all the user app data (the files shown below are from the touchtracer demo)::
$ ls
android_runnable.pyo include interpreter_subprocess main.kv pipinterface.kv settings.pyo
assets __init__.pyo interpreterwrapper.pyo main.pyo pipinterface.pyo utils.pyo
editor.kv interpreter.kv lib menu.kv private.mp3 widgets.pyo
editor.pyo interpreter.pyo libpymodules.so menu.pyo settings.kv
README.txt android.txt icon.png main.pyc p4a_env_vars.txt particle.png
private.tar touchtracer.kv
Most of these files have been included by the user (in this case, they
come from one of my own apps), the rest relate to the python
distribution.
Due to how We're required to ship ABI-specific things in Android App Bundle,
the Python installation is packaged separately, as (most of it) is ABI-specific.
For example, the Python installation for ``arm64-v8a`` is available in ``lib/arm64-v8a/libpybundle.so``
``libpybundle.so`` is a tarball (but named like a library for packaging requirements), that contains our ``_python_bundle``::
$ tar xf libpybundle.so
$ cd _python_bundle
$ ls
modules site-packages stdlib.zip
With Python 2, the Python installation can mostly be found in the
``lib`` folder. With Python 3 (using the ``python3crystax`` recipe),
the Python installation can be found in a folder named
``crystax_python``.
Common errors
@ -134,8 +136,8 @@ AttributeError: 'Context' object has no attribute 'hostpython'
This is a known bug in some releases. To work around it, add your
python requirement explicitly,
e.g. :code:`--requirements=python2,kivy`. This also applies when using
buildozer, in which case add python2 to your buildozer.spec requirements.
e.g. :code:`--requirements=python3,kivy`. This also applies when using
buildozer, in which case add python3 to your buildozer.spec requirements.
linkname too long
~~~~~~~~~~~~~~~~~
@ -147,26 +149,6 @@ the build (e.g. if buildozer was previously used). Removing this
directory should fix the problem, and is desirable anyway since you
don't want it in the APK.
Exception in thread "main" java.lang.UnsupportedClassVersionError: com/android/dx/command/Main : Unsupported major.minor version 52.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This occurs due to a java version mismatch, it should be fixed by
installing Java 8 (e.g. the openjdk-8-jdk package on Ubuntu).
JNI DETECTED ERROR IN APPLICATION: static jfieldID 0x0000000 not valid for class java.lang.Class<org.renpy.android.PythonActivity>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This error appears in the logcat log if you try to access
``org.renpy.android.PythonActivity`` from within the new toolchain. To
fix it, change your code to reference
``org.kivy.android.PythonActivity`` instead.
websocket-client: if you see errors relating to 'SSL not available'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ensure you have the package backports.ssl-match-hostname in the buildozer requirements, since Kivy targets python 2.7.x
You may also need sslopt={"cert_reqs": ssl.CERT_NONE} as a parameter to ws.run_forever() if you get an error relating to host verification
Requested API target 19 is not available, install it with the SDK android tool
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -178,3 +160,27 @@ version).
If using buildozer this should be done automatically, but as a
workaround you can run these from
``~/.buildozer/android/platform/android-sdk-20/tools/android``.
ModuleNotFoundError: No module named '_ctypes'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You do not have the libffi headers available to python-for-android, so you need to install them. On Ubuntu and derivatives these come from the `libffi-dev` package.
After installing the headers, clean the build (`p4a clean builds`, or with buildozer delete the `.buildozer` directory within your app directory) and run python-for-android again.
SSLError("Can't connect to HTTPS URL because the SSL module is not available.")
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Your `hostpython3` was compiled without SSL support. You need to install the SSL development files before rebuilding the `hostpython3` recipe.
Remember to always clean the build before rebuilding (`p4a clean builds`, or with buildozer `buildozer android clean`).
On Ubuntu and derivatives::
apt install libssl-dev
p4a clean builds # or with: buildozer `buildozer android clean
On macOS::
brew install openssl
sudo ln -sfn /usr/local/opt/openssl /usr/local/ssl
p4a clean builds # or with: buildozer `buildozer android clean

View file

@ -1,2 +1 @@
__version__ = '0.5'
__version__ = '2022.09.04'

View file

@ -0,0 +1,83 @@
import sys
import os
class AndroidNDK:
"""
This class is used to get the current NDK information.
"""
ndk_dir = ""
def __init__(self, ndk_dir):
self.ndk_dir = ndk_dir
@property
def host_tag(self):
"""
Returns the host tag for the current system.
Note: The host tag is ``darwin-x86_64`` even on Apple Silicon macs.
"""
return f"{sys.platform}-x86_64"
@property
def llvm_prebuilt_dir(self):
return os.path.join(
self.ndk_dir, "toolchains", "llvm", "prebuilt", self.host_tag
)
@property
def llvm_bin_dir(self):
return os.path.join(self.llvm_prebuilt_dir, "bin")
@property
def clang(self):
return os.path.join(self.llvm_bin_dir, "clang")
@property
def clang_cxx(self):
return os.path.join(self.llvm_bin_dir, "clang++")
@property
def llvm_binutils_prefix(self):
return os.path.join(self.llvm_bin_dir, "llvm-")
@property
def llvm_ar(self):
return f"{self.llvm_binutils_prefix}ar"
@property
def llvm_ranlib(self):
return f"{self.llvm_binutils_prefix}ranlib"
@property
def llvm_objcopy(self):
return f"{self.llvm_binutils_prefix}objcopy"
@property
def llvm_objdump(self):
return f"{self.llvm_binutils_prefix}objdump"
@property
def llvm_readelf(self):
return f"{self.llvm_binutils_prefix}readelf"
@property
def llvm_strip(self):
return f"{self.llvm_binutils_prefix}strip"
@property
def sysroot(self):
return os.path.join(self.llvm_prebuilt_dir, "sysroot")
@property
def sysroot_include_dir(self):
return os.path.join(self.sysroot, "usr", "include")
@property
def sysroot_lib_dir(self):
return os.path.join(self.sysroot, "usr", "lib")
@property
def libcxx_include_dir(self):
return os.path.join(self.sysroot_include_dir, "c++", "v1")

View file

@ -1,22 +1,46 @@
from distutils.spawn import find_executable
from os import environ
from os.path import (exists, join, dirname, split)
from glob import glob
from os.path import join
from multiprocessing import cpu_count
from pythonforandroid.recipe import Recipe
from pythonforandroid.util import BuildInterruptingException, build_platform
class Arch(object):
toolchain_prefix = None
'''The prefix for the toolchain dir in the NDK.'''
class Arch:
command_prefix = None
'''The prefix for NDK commands such as gcc.'''
arch = ""
'''Name of the arch such as: `armeabi-v7a`, `arm64-v8a`, `x86`...'''
arch_cflags = []
'''Specific arch `cflags`, expect to be overwrote in subclass if needed.'''
common_cflags = [
'-target {target}',
'-fomit-frame-pointer'
]
common_cppflags = [
'-DANDROID',
'-I{ctx.ndk.sysroot_include_dir}',
'-I{python_includes}',
]
common_ldflags = ['-L{ctx_libs_dir}']
common_ldlibs = ['-lm']
common_ldshared = [
'-pthread',
'-shared',
'-Wl,-O1',
'-Wl,-Bsymbolic-functions',
]
def __init__(self, ctx):
super(Arch, self).__init__()
self.ctx = ctx
# Allows injecting additional linker paths used by any recipe.
@ -28,6 +52,14 @@ class Arch(object):
def __str__(self):
return self.arch
@property
def ndk_lib_dir(self):
return join(self.ctx.ndk.sysroot_lib_dir, self.command_prefix)
@property
def ndk_lib_dir_versioned(self):
return join(self.ndk_lib_dir, str(self.ctx.ndk_api))
@property
def include_dirs(self):
return [
@ -38,216 +70,235 @@ class Arch(object):
@property
def target(self):
target_data = self.command_prefix.split('-')
return '-'.join(
[target_data[0], 'none', target_data[1], target_data[2]])
# As of NDK r19, the toolchains installed by default with the
# NDK may be used in-place. The make_standalone_toolchain.py script
# is no longer needed for interfacing with arbitrary build systems.
# See: https://developer.android.com/ndk/guides/other_build_systems
return '{triplet}{ndk_api}'.format(
triplet=self.command_prefix, ndk_api=self.ctx.ndk_api
)
def get_env(self, with_flags_in_cc=True, clang=False):
@property
def clang_exe(self):
"""Full path of the clang compiler depending on the android's ndk
version used."""
return self.get_clang_exe()
@property
def clang_exe_cxx(self):
"""Full path of the clang++ compiler depending on the android's ndk
version used."""
return self.get_clang_exe(plus_plus=True)
def get_clang_exe(self, with_target=False, plus_plus=False):
"""Returns the full path of the clang/clang++ compiler, supports two
kwargs:
- `with_target`: prepend `target` to clang
- `plus_plus`: will return the clang++ compiler (defaults to `False`)
"""
compiler = 'clang'
if with_target:
compiler = '{target}-{compiler}'.format(
target=self.target, compiler=compiler
)
if plus_plus:
compiler += '++'
return join(self.ctx.ndk.llvm_bin_dir, compiler)
def get_env(self, with_flags_in_cc=True):
env = {}
cflags = [
'-DANDROID',
'-fomit-frame-pointer',
'-D__ANDROID_API__={}'.format(self.ctx.ndk_api)]
if not clang:
cflags.append('-mandroid')
else:
cflags.append('-target ' + self.target)
toolchain = '{android_host}-{toolchain_version}'.format(
android_host=self.ctx.toolchain_prefix,
toolchain_version=self.ctx.toolchain_version)
toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain,
'prebuilt', build_platform)
cflags.append('-gcc-toolchain {}'.format(toolchain))
# HOME: User's home directory
#
# Many tools including p4a store outputs in the user's home
# directory. This is found from the HOME environment variable
# and falls back to the system account database. Setting HOME
# can be used to globally divert these tools to use a different
# path. Furthermore, in containerized environments the user may
# not exist in the account database, so if HOME isn't set than
# these tools will fail.
if 'HOME' in environ:
env['HOME'] = environ['HOME']
env['CFLAGS'] = ' '.join(cflags)
# CFLAGS/CXXFLAGS: the processor flags
env['CFLAGS'] = ' '.join(self.common_cflags).format(target=self.target)
if self.arch_cflags:
# each architecture may have has his own CFLAGS
env['CFLAGS'] += ' ' + ' '.join(self.arch_cflags)
env['CXXFLAGS'] = env['CFLAGS']
# Link the extra global link paths first before anything else
# CPPFLAGS (for macros and includes)
env['CPPFLAGS'] = ' '.join(self.common_cppflags).format(
ctx=self.ctx,
command_prefix=self.command_prefix,
python_includes=join(
self.ctx.get_python_install_dir(self.arch),
'include/python{}'.format(self.ctx.python_recipe.version[0:3]),
),
)
# LDFLAGS: Link the extra global link paths first before anything else
# (such that overriding system libraries with them is possible)
env['LDFLAGS'] = ' ' + " ".join([
"-L'" + l.replace("'", "'\"'\"'") + "'" # no shlex.quote in py2
for l in self.extra_global_link_paths
]) + ' '
env['LDFLAGS'] = (
' '
+ " ".join(
[
"-L'"
+ link_path.replace("'", "'\"'\"'")
+ "'" # no shlex.quote in py2
for link_path in self.extra_global_link_paths
]
)
+ ' ' + ' '.join(self.common_ldflags).format(
ctx_libs_dir=self.ctx.get_libs_dir(self.arch)
)
)
sysroot = join(self.ctx._ndk_dir, 'sysroot')
if exists(sysroot):
# post-15 NDK per
# https://android.googlesource.com/platform/ndk/+/ndk-r15-release/docs/UnifiedHeaders.md
env['CFLAGS'] += ' -isystem {}/sysroot/usr/include/{}'.format(
self.ctx.ndk_dir, self.ctx.toolchain_prefix)
env['CFLAGS'] += ' -I{}/sysroot/usr/include/{}'.format(
self.ctx.ndk_dir, self.command_prefix)
else:
sysroot = self.ctx.ndk_platform
env['CFLAGS'] += ' -I{}'.format(self.ctx.ndk_platform)
env['CFLAGS'] += ' -isysroot {} '.format(sysroot)
env['CFLAGS'] += '-I' + join(self.ctx.get_python_install_dir(),
'include/python{}'.format(
self.ctx.python_recipe.version[0:3])
)
env['LDFLAGS'] += '--sysroot={} '.format(self.ctx.ndk_platform)
env["CXXFLAGS"] = env["CFLAGS"]
env["LDFLAGS"] += " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)])
if self.ctx.ndk == 'crystax':
env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch)
toolchain_prefix = self.ctx.toolchain_prefix
toolchain_version = self.ctx.toolchain_version
command_prefix = self.command_prefix
env['TOOLCHAIN_PREFIX'] = toolchain_prefix
env['TOOLCHAIN_VERSION'] = toolchain_version
# LDLIBS: Library flags or names given to compilers when they are
# supposed to invoke the linker.
env['LDLIBS'] = ' '.join(self.common_ldlibs)
# CCACHE
ccache = ''
if self.ctx.ccache and bool(int(environ.get('USE_CCACHE', '1'))):
# print('ccache found, will optimize builds')
ccache = self.ctx.ccache + ' '
env['USE_CCACHE'] = '1'
env['NDK_CCACHE'] = self.ctx.ccache
env.update({k: v for k, v in environ.items() if k.startswith('CCACHE_')})
env.update(
{k: v for k, v in environ.items() if k.startswith('CCACHE_')}
)
if clang:
llvm_dirname = split(
glob(join(self.ctx.ndk_dir, 'toolchains', 'llvm*'))[-1])[-1]
clang_path = join(self.ctx.ndk_dir, 'toolchains', llvm_dirname,
'prebuilt', build_platform, 'bin')
environ['PATH'] = '{clang_path}:{path}'.format(
clang_path=clang_path, path=environ['PATH'])
exe = join(clang_path, 'clang')
execxx = join(clang_path, 'clang++')
else:
exe = '{command_prefix}-gcc'.format(command_prefix=command_prefix)
execxx = '{command_prefix}-g++'.format(command_prefix=command_prefix)
cc = find_executable(exe, path=environ['PATH'])
# Compiler: `CC` and `CXX` (and make sure that the compiler exists)
env['PATH'] = self.ctx.env['PATH']
cc = find_executable(self.clang_exe, path=env['PATH'])
if cc is None:
print('Searching path are: {!r}'.format(environ['PATH']))
print('Searching path are: {!r}'.format(env['PATH']))
raise BuildInterruptingException(
'Couldn\'t find executable for CC. This indicates a '
'problem locating the {} executable in the Android '
'NDK, not that you don\'t have a normal compiler '
'installed. Exiting.'.format(exe))
'installed. Exiting.'.format(self.clang_exe))
if with_flags_in_cc:
env['CC'] = '{ccache}{exe} {cflags}'.format(
exe=exe,
exe=self.clang_exe,
ccache=ccache,
cflags=env['CFLAGS'])
env['CXX'] = '{ccache}{execxx} {cxxflags}'.format(
execxx=execxx,
execxx=self.clang_exe_cxx,
ccache=ccache,
cxxflags=env['CXXFLAGS'])
else:
env['CC'] = '{ccache}{exe}'.format(
exe=exe,
exe=self.clang_exe,
ccache=ccache)
env['CXX'] = '{ccache}{execxx}'.format(
execxx=execxx,
execxx=self.clang_exe_cxx,
ccache=ccache)
env['AR'] = '{}-ar'.format(command_prefix)
env['RANLIB'] = '{}-ranlib'.format(command_prefix)
env['LD'] = '{}-ld'.format(command_prefix)
env['LDSHARED'] = env["CC"] + " -pthread -shared " +\
"-Wl,-O1 -Wl,-Bsymbolic-functions "
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
# For crystax python, we can't use the host python headers:
env["CFLAGS"] += ' -I{}/sources/python/{}/include/python/'.\
format(self.ctx.ndk_dir, self.ctx.python_recipe.version[0:3])
env['STRIP'] = '{}-strip --strip-unneeded'.format(command_prefix)
env['MAKE'] = 'make -j5'
env['READELF'] = '{}-readelf'.format(command_prefix)
env['NM'] = '{}-nm'.format(command_prefix)
# Android's LLVM binutils
env['AR'] = self.ctx.ndk.llvm_ar
env['RANLIB'] = self.ctx.ndk.llvm_ranlib
env['STRIP'] = f'{self.ctx.ndk.llvm_strip} --strip-unneeded'
env['READELF'] = self.ctx.ndk.llvm_readelf
env['OBJCOPY'] = self.ctx.ndk.llvm_objcopy
env['MAKE'] = 'make -j{}'.format(str(cpu_count()))
# Android's arch/toolchain
env['ARCH'] = self.arch
env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api))
# Custom linker options
env['LDSHARED'] = env['CC'] + ' ' + ' '.join(self.common_ldshared)
# Host python (used by some recipes)
hostpython_recipe = Recipe.get_recipe(
'host' + self.ctx.python_recipe.name, self.ctx)
env['BUILDLIB_PATH'] = join(
hostpython_recipe.get_build_dir(self.arch),
'build', 'lib.{}-{}'.format(
build_platform, self.ctx.python_recipe.major_minor_version_string)
'native-build',
'build',
'lib.{}-{}'.format(
build_platform,
self.ctx.python_recipe.major_minor_version_string,
),
)
env['PATH'] = environ['PATH']
env['ARCH'] = self.arch
env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api))
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
env['CRYSTAX_PYTHON_VERSION'] = self.ctx.python_recipe.version
# for reproducible builds
if 'SOURCE_DATE_EPOCH' in environ:
for k in 'LC_ALL TZ SOURCE_DATE_EPOCH PYTHONHASHSEED BUILD_DATE BUILD_TIME'.split():
if k in environ:
env[k] = environ[k]
return env
class ArchARM(Arch):
arch = "armeabi"
toolchain_prefix = 'arm-linux-androideabi'
command_prefix = 'arm-linux-androideabi'
platform_dir = 'arch-arm'
@property
def target(self):
target_data = self.command_prefix.split('-')
return '-'.join(
['armv7a', 'none', target_data[1], target_data[2]])
return '{triplet}{ndk_api}'.format(
triplet='-'.join(['armv7a', target_data[1], target_data[2]]),
ndk_api=self.ctx.ndk_api,
)
class ArchARMv7_a(ArchARM):
arch = 'armeabi-v7a'
def get_env(self, with_flags_in_cc=True, clang=False):
env = super(ArchARMv7_a, self).get_env(with_flags_in_cc, clang=clang)
env['CFLAGS'] = (env['CFLAGS'] +
(' -march=armv7-a -mfloat-abi=softfp '
'-mfpu=vfp -mthumb'))
env['CXXFLAGS'] = env['CFLAGS']
return env
arch_cflags = [
'-march=armv7-a',
'-mfloat-abi=softfp',
'-mfpu=vfp',
'-mthumb',
'-fPIC',
]
class Archx86(Arch):
arch = 'x86'
toolchain_prefix = 'x86'
command_prefix = 'i686-linux-android'
platform_dir = 'arch-x86'
def get_env(self, with_flags_in_cc=True, clang=False):
env = super(Archx86, self).get_env(with_flags_in_cc, clang=clang)
env['CFLAGS'] = (env['CFLAGS'] +
' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32')
env['CXXFLAGS'] = env['CFLAGS']
return env
arch_cflags = [
'-march=i686',
'-mssse3',
'-mfpmath=sse',
'-m32',
'-fPIC',
]
class Archx86_64(Arch):
arch = 'x86_64'
toolchain_prefix = 'x86_64'
command_prefix = 'x86_64-linux-android'
platform_dir = 'arch-x86_64'
def get_env(self, with_flags_in_cc=True, clang=False):
env = super(Archx86_64, self).get_env(with_flags_in_cc, clang=clang)
env['CFLAGS'] = (env['CFLAGS'] +
' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel')
env['CXXFLAGS'] = env['CFLAGS']
return env
arch_cflags = [
'-march=x86-64',
'-msse4.2',
'-mpopcnt',
'-m64',
'-fPIC',
]
class ArchAarch_64(Arch):
arch = 'arm64-v8a'
toolchain_prefix = 'aarch64-linux-android'
command_prefix = 'aarch64-linux-android'
platform_dir = 'arch-arm64'
arch_cflags = [
'-march=armv8-a',
'-fPIC'
# '-I' + join(dirname(__file__), 'includes', 'arm64-v8a'),
]
def get_env(self, with_flags_in_cc=True, clang=False):
env = super(ArchAarch_64, self).get_env(with_flags_in_cc, clang=clang)
incpath = ' -I' + join(dirname(__file__), 'includes', 'arm64-v8a')
env['EXTRA_CFLAGS'] = incpath
env['CFLAGS'] += incpath
env['CXXFLAGS'] += incpath
if with_flags_in_cc:
env['CC'] += incpath
env['CXX'] += incpath
return env
# Note: This `EXTRA_CFLAGS` below should target the commented `include`
# above in `arch_cflags`. The original lines were added during the Sdl2's
# bootstrap creation, and modified/commented during the migration to the
# NDK r19 build system, because it seems that we don't need it anymore,
# do we need them?
# def get_env(self, with_flags_in_cc=True):
# env = super().get_env(with_flags_in_cc)
# env['EXTRA_CFLAGS'] = self.arch_cflags[-1]
# return env

View file

@ -1,6 +1,4 @@
from __future__ import print_function
from setuptools import Command
from pythonforandroid import toolchain
import sys
from os.path import realpath, join, exists, dirname, curdir, basename, split
@ -16,16 +14,16 @@ def argv_contains(t):
return False
class BdistAPK(Command):
description = 'Create an APK with python-for-android'
class Bdist(Command):
user_options = []
package_type = None
def initialize_options(self):
for option in self.user_options:
setattr(self, option[0].strip('=').replace('-', '_'), None)
option_dict = self.distribution.get_option_dict('apk')
option_dict = self.distribution.get_option_dict(self.package_type)
# This is a hack, we probably aren't supposed to loop through
# the option_dict so early because distutils does exactly the
@ -34,10 +32,9 @@ class BdistAPK(Command):
for (option, (source, value)) in option_dict.items():
setattr(self, option, str(value))
def finalize_options(self):
setup_options = self.distribution.get_option_dict('apk')
setup_options = self.distribution.get_option_dict(self.package_type)
for (option, (source, value)) in setup_options.items():
if source == 'command line':
continue
@ -70,16 +67,15 @@ class BdistAPK(Command):
sys.argv.append('--version={}'.format(version))
if not argv_contains('--arch'):
arch = 'arm64-v8a'
arch = 'armeabi-v7a'
self.arch = arch
sys.argv.append('--arch={}'.format(arch))
def run(self):
self.prepare_build_dir()
from pythonforandroid.toolchain import main
sys.argv[1] = 'apk'
from pythonforandroid.entrypoints import main
sys.argv[1] = self.package_type
main()
def prepare_build_dir(self):
@ -112,7 +108,7 @@ class BdistAPK(Command):
makedirs(new_dir)
print('Including {}'.format(filen))
copyfile(filen, join(bdist_dir, filen))
if basename(filen) in ('main.py', 'main.pyo'):
if basename(filen) in ('main.py', 'main.pyc'):
main_py_dirs.append(filen)
# This feels ridiculous, but how else to define the main.py dir?
@ -123,7 +119,7 @@ class BdistAPK(Command):
exit(1)
if len(main_py_dirs) > 1:
print('WARNING: Multiple main.py dirs found, using the shortest path')
main_py_dirs.sort(key=lambda j: len(split(j)))
main_py_dirs = sorted(main_py_dirs, key=lambda j: len(split(j)))
if not argv_contains('--launcher'):
sys.argv.append('--private={}'.format(
@ -131,18 +127,39 @@ class BdistAPK(Command):
)
class BdistAPK(Bdist):
"""distutil command handler for 'apk'."""
description = 'Create an APK with python-for-android'
package_type = 'apk'
class BdistAAR(Bdist):
"""distutil command handler for 'aar'."""
description = 'Create an AAR with python-for-android'
package_type = 'aar'
class BdistAAB(Bdist):
"""distutil command handler for 'aab'."""
description = 'Create an AAB with python-for-android'
package_type = 'aab'
def _set_user_options():
# This seems like a silly way to do things, but not sure if there's a
# better way to pass arbitrary options onwards to p4a
user_options = [('requirements=', None, None),]
user_options = [('requirements=', None, None), ]
for i, arg in enumerate(sys.argv):
if arg.startswith('--'):
if ('=' in arg or
(i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))):
(i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))):
user_options.append((arg[2:].split('=')[0] + '=', None, None))
else:
user_options.append((arg[2:], None, None))
BdistAPK.user_options = user_options
BdistAAB.user_options = user_options
BdistAAR.user_options = user_options
_set_user_options()

259
p4a/pythonforandroid/bootstrap.py Normal file → Executable file
View file

@ -1,20 +1,20 @@
import functools
import glob
import importlib
import os
from os.path import (join, dirname, isdir, normpath, splitext, basename)
from os import listdir, walk, sep
import sh
import shlex
import glob
import importlib
import os
import shutil
from pythonforandroid.logger import (warning, shprint, info, logger,
debug)
from pythonforandroid.util import (current_directory, ensure_dir,
temp_directory)
from pythonforandroid.logger import (shprint, info, logger, debug)
from pythonforandroid.util import (
current_directory, ensure_dir, temp_directory, BuildInterruptingException)
from pythonforandroid.recipe import Recipe
def copy_files(src_root, dest_root, override=True):
def copy_files(src_root, dest_root, override=True, symlink=False):
for root, dirnames, filenames in walk(src_root):
for filename in filenames:
subdir = normpath(root.replace(src_root, ""))
@ -29,12 +29,44 @@ def copy_files(src_root, dest_root, override=True):
if override and os.path.exists(dest_file):
os.unlink(dest_file)
if not os.path.exists(dest_file):
shutil.copy(src_file, dest_file)
if symlink:
os.symlink(src_file, dest_file)
else:
shutil.copy(src_file, dest_file)
else:
os.makedirs(dest_file)
class Bootstrap(object):
default_recipe_priorities = [
"webview", "sdl2", "service_only" # last is highest
]
# ^^ NOTE: these are just the default priorities if no special rules
# apply (which you can find in the code below), so basically if no
# known graphical lib or web lib is used - in which case service_only
# is the most reasonable guess.
def _cmp_bootstraps_by_priority(a, b):
def rank_bootstrap(bootstrap):
""" Returns a ranking index for each bootstrap,
with higher priority ranked with higher number. """
if bootstrap.name in default_recipe_priorities:
return default_recipe_priorities.index(bootstrap.name) + 1
return 0
# Rank bootstraps in order:
rank_a = rank_bootstrap(a)
rank_b = rank_bootstrap(b)
if rank_a != rank_b:
return (rank_b - rank_a)
else:
if a.name < b.name: # alphabetic sort for determinism
return -1
else:
return 1
class Bootstrap:
'''An Android project template, containing recipe stuff for
compilation and templated fields for APK info.
'''
@ -45,15 +77,11 @@ class Bootstrap(object):
bootstrap_dir = None
build_dir = None
dist_dir = None
dist_name = None
distribution = None
# All bootstraps should include Python in some way:
recipe_depends = [
("python2", "python2legacy", "python3", "python3crystax"),
'android',
]
recipe_depends = ['python3', 'android']
can_be_chosen_automatically = True
'''Determines whether the bootstrap can be chosen as one that
@ -70,9 +98,9 @@ class Bootstrap(object):
def dist_dir(self):
'''The dist dir at which to place the finished distribution.'''
if self.distribution is None:
warning('Tried to access {}.dist_dir, but {}.distribution '
'is None'.format(self, self))
exit(1)
raise BuildInterruptingException(
'Internal error: tried to access {}.dist_dir, but {}.distribution '
'is None'.format(self, self))
return self.distribution.dist_dir
@property
@ -84,7 +112,7 @@ class Bootstrap(object):
and optional dependencies are being used,
and returns a list of these.'''
recipes = []
built_recipes = self.ctx.recipe_build_order
built_recipes = self.ctx.recipe_build_order or []
for recipe in self.recipe_depends:
if isinstance(recipe, (tuple, list)):
for alternative in recipe:
@ -104,70 +132,102 @@ class Bootstrap(object):
def get_dist_dir(self, name):
return join(self.ctx.dist_dir, name)
def get_common_dir(self):
return os.path.abspath(join(self.bootstrap_dir, "..", 'common'))
@property
def name(self):
modname = self.__class__.__module__
return modname.split(".", 2)[-1]
def get_bootstrap_dirs(self):
"""get all bootstrap directories, following the MRO path"""
# get all bootstrap names along the __mro__, cutting off Bootstrap and object
classes = self.__class__.__mro__[:-2]
bootstrap_names = [cls.name for cls in classes] + ['common']
bootstrap_dirs = [
join(self.ctx.root_dir, 'bootstraps', bootstrap_name)
for bootstrap_name in reversed(bootstrap_names)
]
return bootstrap_dirs
def _copy_in_final_files(self):
if self.name == "sdl2":
# Get the paths for copying SDL2's java source code:
sdl2_recipe = Recipe.get_recipe("sdl2", self.ctx)
sdl2_build_dir = sdl2_recipe.get_jni_dir()
src_dir = join(sdl2_build_dir, "SDL", "android-project",
"app", "src", "main", "java",
"org", "libsdl", "app")
target_dir = join(self.dist_dir, 'src', 'main', 'java', 'org',
'libsdl', 'app')
# Do actual copying:
info('Copying in SDL2 .java files from: ' + str(src_dir))
if not os.path.exists(target_dir):
os.makedirs(target_dir)
copy_files(src_dir, target_dir, override=True)
def prepare_build_dir(self):
'''Ensure that a build dir exists for the recipe. This same single
dir will be used for building all different archs.'''
"""Ensure that a build dir exists for the recipe. This same single
dir will be used for building all different archs."""
bootstrap_dirs = self.get_bootstrap_dirs()
# now do a cumulative copy of all bootstrap dirs
self.build_dir = self.get_build_dir()
self.common_dir = self.get_common_dir()
copy_files(join(self.bootstrap_dir, 'build'), self.build_dir)
copy_files(join(self.common_dir, 'build'), self.build_dir,
override=False)
if self.ctx.symlink_java_src:
info('Symlinking java src instead of copying')
shprint(sh.rm, '-r', join(self.build_dir, 'src'))
shprint(sh.mkdir, join(self.build_dir, 'src'))
for dirn in listdir(join(self.bootstrap_dir, 'build', 'src')):
shprint(sh.ln, '-s', join(self.bootstrap_dir, 'build', 'src', dirn),
join(self.build_dir, 'src'))
for bootstrap_dir in bootstrap_dirs:
copy_files(join(bootstrap_dir, 'build'), self.build_dir, symlink=self.ctx.symlink_bootstrap_files)
with current_directory(self.build_dir):
with open('project.properties', 'w') as fileh:
fileh.write('target=android-{}'.format(self.ctx.android_api))
def prepare_dist_dir(self, name):
def prepare_dist_dir(self):
ensure_dir(self.dist_dir)
def run_distribute(self):
def assemble_distribution(self):
''' Copies all the files into the distribution (this function is
overridden by the specific bootstrap classes to do this)
and add in the distribution info.
'''
self._copy_in_final_files()
self.distribution.save_info(self.dist_dir)
@classmethod
def list_bootstraps(cls):
def all_bootstraps(cls):
'''Find all the available bootstraps and return them.'''
forbidden_dirs = ('__pycache__', 'common')
bootstraps_dir = join(dirname(__file__), 'bootstraps')
result = set()
for name in listdir(bootstraps_dir):
if name in forbidden_dirs:
continue
filen = join(bootstraps_dir, name)
if isdir(filen):
yield name
result.add(name)
return result
@classmethod
def get_bootstrap_from_recipes(cls, recipes, ctx):
'''Returns a bootstrap whose recipe requirements do not conflict with
the given recipes.'''
def get_usable_bootstraps_for_recipes(cls, recipes, ctx):
'''Returns all bootstrap whose recipe requirements do not conflict
with the given recipes, in no particular order.'''
info('Trying to find a bootstrap that matches the given recipes.')
bootstraps = [cls.get_bootstrap(name, ctx)
for name in cls.list_bootstraps()]
acceptable_bootstraps = []
for name in cls.all_bootstraps()]
acceptable_bootstraps = set()
# Find out which bootstraps are acceptable:
for bs in bootstraps:
if not bs.can_be_chosen_automatically:
continue
possible_dependency_lists = expand_dependencies(bs.recipe_depends)
possible_dependency_lists = expand_dependencies(bs.recipe_depends, ctx)
for possible_dependencies in possible_dependency_lists:
ok = True
# Check if the bootstap's dependencies have an internal conflict:
for recipe in possible_dependencies:
recipe = Recipe.get_recipe(recipe, ctx)
if any([conflict in recipes for conflict in recipe.conflicts]):
if any(conflict in recipes for conflict in recipe.conflicts):
ok = False
break
# Check if bootstrap's dependencies conflict with chosen
# packages:
for recipe in recipes:
try:
recipe = Recipe.get_recipe(recipe, ctx)
@ -175,19 +235,63 @@ class Bootstrap(object):
conflicts = []
else:
conflicts = recipe.conflicts
if any([conflict in possible_dependencies
for conflict in conflicts]):
if any(conflict in possible_dependencies
for conflict in conflicts):
ok = False
break
if ok and bs not in acceptable_bootstraps:
acceptable_bootstraps.append(bs)
acceptable_bootstraps.add(bs)
info('Found {} acceptable bootstraps: {}'.format(
len(acceptable_bootstraps),
[bs.name for bs in acceptable_bootstraps]))
if acceptable_bootstraps:
info('Using the first of these: {}'
.format(acceptable_bootstraps[0].name))
return acceptable_bootstraps[0]
return acceptable_bootstraps
@classmethod
def get_bootstrap_from_recipes(cls, recipes, ctx):
'''Picks a single recommended default bootstrap out of
all_usable_bootstraps_from_recipes() for the given reicpes,
and returns it.'''
known_web_packages = {"flask"} # to pick webview over service_only
recipes_with_deps_lists = expand_dependencies(recipes, ctx)
acceptable_bootstraps = cls.get_usable_bootstraps_for_recipes(
recipes, ctx
)
def have_dependency_in_recipes(dep):
for dep_list in recipes_with_deps_lists:
if dep in dep_list:
return True
return False
# Special rule: return SDL2 bootstrap if there's an sdl2 dep:
if (have_dependency_in_recipes("sdl2") and
"sdl2" in [b.name for b in acceptable_bootstraps]
):
info('Using sdl2 bootstrap since it is in dependencies')
return cls.get_bootstrap("sdl2", ctx)
# Special rule: return "webview" if we depend on common web recipe:
for possible_web_dep in known_web_packages:
if have_dependency_in_recipes(possible_web_dep):
# We have a web package dep!
if "webview" in [b.name for b in acceptable_bootstraps]:
info('Using webview bootstrap since common web packages '
'were found {}'.format(
known_web_packages.intersection(recipes)
))
return cls.get_bootstrap("webview", ctx)
prioritized_acceptable_bootstraps = sorted(
list(acceptable_bootstraps),
key=functools.cmp_to_key(_cmp_bootstraps_by_priority)
)
if prioritized_acceptable_bootstraps:
info('Using the highest ranked/first of these: {}'
.format(prioritized_acceptable_bootstraps[0].name))
return prioritized_acceptable_bootstraps[0]
return None
@classmethod
@ -218,15 +322,16 @@ class Bootstrap(object):
tgt_dir = join(dest_dir, arch.arch)
ensure_dir(tgt_dir)
for src_dir in src_dirs:
for lib in glob.glob(join(src_dir, wildcard)):
shprint(sh.cp, '-a', lib, tgt_dir)
libs = glob.glob(join(src_dir, wildcard))
if libs:
shprint(sh.cp, '-a', *libs, tgt_dir)
def distribute_javaclasses(self, javaclass_dir, dest_dir="src"):
'''Copy existing javaclasses from build dir to current dist dir.'''
info('Copying java files')
ensure_dir(dest_dir)
for filename in glob.glob(javaclass_dir):
shprint(sh.cp, '-a', filename, dest_dir)
filenames = glob.glob(javaclass_dir)
shprint(sh.cp, '-a', *filenames, dest_dir)
def distribute_aars(self, arch):
'''Process existing .aar bundles and copy to current dist dir.'''
@ -259,24 +364,18 @@ class Bootstrap(object):
debug(" to {}".format(so_tgt_dir))
ensure_dir(so_tgt_dir)
so_files = glob.glob(join(so_src_dir, '*.so'))
for f in so_files:
shprint(sh.cp, '-a', f, so_tgt_dir)
shprint(sh.cp, '-a', *so_files, so_tgt_dir)
def strip_libraries(self, arch):
info('Stripping libraries')
if self.ctx.python_recipe.from_crystax:
info('Python was loaded from CrystaX, skipping strip')
return
env = arch.get_env()
tokens = shlex.split(env['STRIP'])
strip = sh.Command(tokens[0])
if len(tokens) > 1:
strip = strip.bake(tokens[1:])
libs_dir = join(self.dist_dir, '_python_bundle',
libs_dir = join(self.dist_dir, f'_python_bundle__{arch.arch}',
'_python_bundle', 'modules')
if self.ctx.python_recipe.name == 'python2legacy':
libs_dir = join(self.dist_dir, 'private')
filens = shprint(sh.find, libs_dir, join(self.dist_dir, 'libs'),
'-iname', '*.so', _env=env).stdout.decode('utf-8')
@ -301,9 +400,31 @@ class Bootstrap(object):
shprint(sh.rm, '-rf', d)
def expand_dependencies(recipes):
def expand_dependencies(recipes, ctx):
""" This function expands to lists of all different available
alternative recipe combinations, with the dependencies added in
ONLY for all the not-with-alternative recipes.
(So this is like the deps graph very simplified and incomplete, but
hopefully good enough for most basic bootstrap compatibility checks)
"""
# Add in all the deps of recipes where there is no alternative:
recipes_with_deps = list(recipes)
for entry in recipes:
if not isinstance(entry, (tuple, list)) or len(entry) == 1:
if isinstance(entry, (tuple, list)):
entry = entry[0]
try:
recipe = Recipe.get_recipe(entry, ctx)
recipes_with_deps += recipe.depends
except ValueError:
# it's a pure python package without a recipe, so we
# don't know the dependencies...skipping for now
pass
# Split up lists by available alternatives:
recipe_lists = [[]]
for recipe in recipes:
for recipe in recipes_with_deps:
if isinstance(recipe, (tuple, list)):
new_recipe_lists = []
for alternative in recipe:
@ -313,6 +434,6 @@ def expand_dependencies(recipes):
new_recipe_lists.append(new_list)
recipe_lists = new_recipe_lists
else:
for old_list in recipe_lists:
old_list.append(recipe)
for existing_list in recipe_lists:
existing_list.append(recipe)
return recipe_lists

View file

@ -1,13 +1,13 @@
#!/usr/bin/env python2.7
from __future__ import print_function
#!/usr/bin/env python3
from gzip import GzipFile
import hashlib
import json
from os.path import (
dirname, join, isfile, realpath,
relpath, split, exists, basename
)
from os import listdir, makedirs, remove
from os import environ, listdir, makedirs, remove
import os
import shlex
import shutil
@ -16,19 +16,20 @@ import sys
import tarfile
import tempfile
import time
from zipfile import ZipFile
from distutils.version import LooseVersion
from fnmatch import fnmatch
import jinja2
def get_dist_info_for(key):
def get_dist_info_for(key, error_if_missing=True):
try:
with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
info = json.load(fileh)
value = str(info[key])
value = info[key]
except (OSError, KeyError) as e:
if not error_if_missing:
return None
print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
"from dist_info.json: " + str(e))
sys.exit(1)
@ -39,10 +40,6 @@ def get_hostpython():
return get_dist_info_for('hostpython')
def get_python_version():
return get_dist_info_for('python_version')
def get_bootstrap_name():
return get_dist_info_for('bootstrap')
@ -57,7 +54,6 @@ else:
curdir = dirname(__file__)
PYTHON = get_hostpython()
PYTHON_VERSION = get_python_version()
if PYTHON is not None and not exists(PYTHON):
PYTHON = None
@ -72,29 +68,23 @@ BLACKLIST_PATTERNS = [
'~',
'*.bak',
'*.swp',
# Android artifacts
'*.apk',
'*.aab',
]
# pyc/py
if PYTHON is not None:
BLACKLIST_PATTERNS.append('*.py')
if PYTHON_VERSION and int(PYTHON_VERSION[0]) == 2:
# we only blacklist `.pyc` for python2 because in python3 the compiled
# extension is `.pyc` (.pyo files not exists for python >= 3.6)
BLACKLIST_PATTERNS.append('*.pyc')
WHITELIST_PATTERNS = []
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
WHITELIST_PATTERNS.append('pyconfig.h')
python_files = []
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
join(curdir, 'templates')))
def try_unlink(fn):
if exists(fn):
os.unlink(fn)
DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity'
DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService'
def ensure_dir(path):
@ -154,75 +144,33 @@ def listfiles(d):
yield fn
def make_python_zip():
'''
Search for all the python related files, and construct the pythonXX.zip
According to
# http://randomsplat.com/id5-cross-compiling-python-for-embedded-linux.html
site-packages, config and lib-dynload will be not included.
'''
if not exists('private'):
print('No compiled python is present to zip, skipping.')
return
global python_files
d = realpath(join('private', 'lib', 'python2.7'))
def select(fn):
if is_blacklist(fn):
return False
fn = realpath(fn)
assert(fn.startswith(d))
fn = fn[len(d):]
if (fn.startswith('/site-packages/')
or fn.startswith('/config/')
or fn.startswith('/lib-dynload/')
or fn.startswith('/libpymodules.so')):
return False
return fn
# get a list of all python file
python_files = [x for x in listfiles(d) if select(x)]
# create the final zipfile
zfn = join('private', 'lib', 'python27.zip')
zf = ZipFile(zfn, 'w')
# put all the python files in it
for fn in python_files:
afn = fn[len(d):]
zf.write(fn, afn)
zf.close()
def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True):
def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True):
'''
Make a zip file `fn` from the contents of source_dis.
'''
# selector function
def select(fn):
rfn = realpath(fn)
for p in ignore_path:
if p.endswith('/'):
p = p[:-1]
if rfn.startswith(p):
return False
if rfn in python_files:
return False
return not is_blacklist(fn)
def clean(tinfo):
"""cleaning function (for reproducible builds)"""
tinfo.uid = tinfo.gid = 0
tinfo.uname = tinfo.gname = ''
tinfo.mtime = 0
return tinfo
# get the files and relpath file of all the directory we asked for
files = []
for sd in source_dirs:
sd = realpath(sd)
compile_dir(sd, optimize_python=optimize_python)
files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd)
if select(x)]
for fn in listfiles(sd):
if is_blacklist(fn):
continue
if fn.endswith('.py') and byte_compile_python:
fn = compile_py_file(fn, optimize_python=optimize_python)
files.append((fn, relpath(realpath(fn), sd)))
files.sort() # deterministic
# create tar.gz of thoses files
tf = tarfile.open(tfn, 'w:gz', format=tarfile.USTAR_FORMAT)
gf = GzipFile(tfn, 'wb', mtime=0) # deterministic
tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT)
dirs = []
for fn, afn in files:
dn = dirname(afn)
@ -238,25 +186,24 @@ def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True):
dirs.append(d)
tinfo = tarfile.TarInfo(d)
tinfo.type = tarfile.DIRTYPE
clean(tinfo)
tf.addfile(tinfo)
# put the file
tf.add(fn, afn)
tf.add(fn, afn, filter=clean)
tf.close()
gf.close()
def compile_dir(dfn, optimize_python=True):
def compile_py_file(python_file, optimize_python=True):
'''
Compile *.py in directory `dfn` to *.pyo
Compile python_file to *.pyc and return the filename of the *.pyc file.
'''
if PYTHON is None:
return
if int(PYTHON_VERSION[0]) >= 3:
args = [PYTHON, '-m', 'compileall', '-b', '-f', dfn]
else:
args = [PYTHON, '-m', 'compileall', '-f', dfn]
args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file]
if optimize_python:
# -OO = strip docstrings
args.insert(1, '-OO')
@ -268,16 +215,18 @@ def compile_dir(dfn, optimize_python=True):
'error, see logs above')
exit(1)
return ".".join([os.path.splitext(python_file)[0], "pyc"])
def make_package(args):
# If no launcher is specified, require a main.py/main.pyo:
# If no launcher is specified, require a main.py/main.pyc:
if (get_bootstrap_name() != "sdl" or args.launcher is None) and \
get_bootstrap_name() != "webview":
get_bootstrap_name() not in ["webview", "service_library"]:
# (webview doesn't need an entrypoint, apparently)
if args.private is None or (
not exists(join(realpath(args.private), 'main.py')) and
not exists(join(realpath(args.private), 'main.pyo'))):
print('''BUILD FAILURE: No main.py(o) found in your app directory. This
not exists(join(realpath(args.private), 'main.pyc'))):
print('''BUILD FAILURE: No main.py(c) found in your app directory. This
file must exist to act as the entry point for you app. If your app is
started by a file with a different name, rename it to main.py or add a
main.py that loads it.''')
@ -286,53 +235,159 @@ main.py that loads it.''')
assets_dir = "src/main/assets"
# Delete the old assets.
try_unlink(join(assets_dir, 'public.mp3'))
try_unlink(join(assets_dir, 'private.mp3'))
shutil.rmtree(assets_dir, ignore_errors=True)
ensure_dir(assets_dir)
# In order to speedup import and initial depack,
# construct a python27.zip
make_python_zip()
# Add extra environment variable file into tar-able directory:
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
if hasattr(args, "window"):
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
if hasattr(args, "orientation"):
f.write("P4A_ORIENTATION=" + str(args.orientation) + "\n")
f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
# Package up the private data (public not supported).
tar_dirs = [env_vars_tarpath]
if args.private:
tar_dirs.append(args.private)
for python_bundle_dir in ('private', 'crystax_python', '_python_bundle'):
if exists(python_bundle_dir):
tar_dirs.append(python_bundle_dir)
if get_bootstrap_name() == "webview":
tar_dirs.append('webview_includes')
if args.private or args.launcher:
make_tar(
join(assets_dir, 'private.mp3'), tar_dirs, args.ignore_path,
optimize_python=args.optimize_python)
use_setup_py = get_dist_info_for("use_setup_py",
error_if_missing=False) is True
private_tar_dirs = [env_vars_tarpath]
_temp_dirs_to_clean = []
try:
if args.private:
if not use_setup_py or (
not exists(join(args.private, "setup.py")) and
not exists(join(args.private, "pyproject.toml"))
):
print('No setup.py/pyproject.toml used, copying '
'full private data into .apk.')
private_tar_dirs.append(args.private)
else:
print("Copying main.py's ONLY, since other app data is "
"expected in site-packages.")
main_py_only_dir = tempfile.mkdtemp()
_temp_dirs_to_clean.append(main_py_only_dir)
# Check all main.py files we need to copy:
copy_paths = ["main.py", join("service", "main.py")]
for copy_path in copy_paths:
variants = [
copy_path,
copy_path.partition(".")[0] + ".pyc",
]
# Check in all variants with all possible endings:
for variant in variants:
if exists(join(args.private, variant)):
# Make sure surrounding directly exists:
dir_path = os.path.dirname(variant)
if (len(dir_path) > 0 and
not exists(
join(main_py_only_dir, dir_path)
)):
os.mkdir(join(main_py_only_dir, dir_path))
# Copy actual file:
shutil.copyfile(
join(args.private, variant),
join(main_py_only_dir, variant),
)
# Append directory with all main.py's to result apk paths:
private_tar_dirs.append(main_py_only_dir)
if get_bootstrap_name() == "webview":
for asset in listdir('webview_includes'):
shutil.copy(join('webview_includes', asset), join(assets_dir, asset))
for asset in args.assets:
asset_src, asset_dest = asset.split(":")
if isfile(realpath(asset_src)):
ensure_dir(dirname(join(assets_dir, asset_dest)))
shutil.copy(realpath(asset_src), join(assets_dir, asset_dest))
else:
shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest))
if args.private or args.launcher:
for arch in get_dist_info_for("archs"):
libs_dir = f"libs/{arch}"
make_tar(
join(libs_dir, "libpybundle.so"),
[f"_python_bundle__{arch}"],
byte_compile_python=args.byte_compile_python,
optimize_python=args.optimize_python,
)
make_tar(
join(assets_dir, "private.tar"),
private_tar_dirs,
byte_compile_python=args.byte_compile_python,
optimize_python=args.optimize_python,
)
finally:
for directory in _temp_dirs_to_clean:
shutil.rmtree(directory)
# Remove extra env vars tar-able directory:
shutil.rmtree(env_vars_tarpath)
# Prepare some variables for templating process
res_dir = "src/main/res"
res_dir_initial = "src/res_initial"
# make res_dir stateless
if exists(res_dir_initial):
shutil.rmtree(res_dir, ignore_errors=True)
shutil.copytree(res_dir_initial, res_dir)
else:
shutil.copytree(res_dir, res_dir_initial)
# Add user resouces
for resource in args.resources:
resource_src, resource_dest = resource.split(":")
if isfile(realpath(resource_src)):
ensure_dir(dirname(join(res_dir, resource_dest)))
shutil.copy(realpath(resource_src), join(res_dir, resource_dest))
else:
shutil.copytree(realpath(resource_src),
join(res_dir, resource_dest), dirs_exist_ok=True)
default_icon = 'templates/kivy-icon.png'
default_presplash = 'templates/kivy-presplash.jpg'
shutil.copy(
args.icon or default_icon,
join(res_dir, 'drawable/icon.png')
join(res_dir, 'mipmap/icon.png')
)
if args.icon_fg and args.icon_bg:
shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png'))
shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png'))
with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd:
fd.write("""<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/icon_background"/>
<foreground android:drawable="@mipmap/icon_foreground"/>
</adaptive-icon>
""")
elif args.icon_fg or args.icon_bg:
print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. "
"Ignoring.")
if get_bootstrap_name() != "service_only":
shutil.copy(
args.presplash or default_presplash,
join(res_dir, 'drawable/presplash.jpg')
)
lottie_splashscreen = join(res_dir, 'raw/splashscreen.json')
if args.presplash_lottie:
shutil.copy(
'templates/lottie.xml',
join(res_dir, 'layout/lottie.xml')
)
ensure_dir(join(res_dir, 'raw'))
shutil.copy(
args.presplash_lottie,
join(res_dir, 'raw/splashscreen.json')
)
else:
if exists(lottie_splashscreen):
remove(lottie_splashscreen)
remove(join(res_dir, 'layout/lottie.xml'))
shutil.copy(
args.presplash or default_presplash,
join(res_dir, 'drawable/presplash.jpg')
)
# If extra Java jars were requested, copy them into the libs directory
jars = []
@ -360,17 +415,17 @@ main.py that loads it.''')
version_code = 0
if not args.numeric_version:
# Set version code in format (arch-minsdk-app_version)
with open(join(dirname(__file__), 'dist_info.json'), 'r') as dist_info:
dist_data = json.load(dist_info)
arch = dist_data["archs"][0]
arch_dict = {"x86_64": "9", "arm64-v8a": "8", "armeabi-v7a": "7", "x86": "6"}
arch_code = arch_dict.get(arch, '1')
"""
Set version code in format (10 + minsdk + app_version)
Historically versioning was (arch + minsdk + app_version),
with arch expressed with a single digit from 6 to 9.
Since the multi-arch support, has been changed to 10.
"""
min_sdk = args.min_sdk_version
for i in args.version.split('.'):
version_code *= 100
version_code += int(i)
args.numeric_version = "{}{}{}".format(arch_code, min_sdk, version_code)
args.numeric_version = "{}{}{}".format("10", min_sdk, version_code)
if args.intent_filters:
with open(args.intent_filters) as fd:
@ -387,6 +442,9 @@ main.py that loads it.''')
for spec in args.extra_source_dirs:
if ':' in spec:
specdir, specincludes = spec.split(':')
print('WARNING: Currently gradle builds only support including source '
'directories, so when building using gradle all files in '
'{} will be included.'.format(specdir))
else:
specdir = spec
specincludes = '**'
@ -402,6 +460,7 @@ main.py that loads it.''')
service = True
service_names = []
base_service_class = args.service_class_name.split('.')[-1]
for sid, spec in enumerate(args.services):
spec = spec.split(':')
name = spec[0]
@ -426,6 +485,7 @@ main.py that loads it.''')
foreground=foreground,
sticky=sticky,
service_id=sid + 1,
base_service_class=base_service_class,
)
# Find the SDK directory and target API
@ -447,19 +507,37 @@ main.py that loads it.''')
# Try to build with the newest available build tools
ignored = {".DS_Store", ".ds_store"}
build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
build_tools_versions.sort(key=LooseVersion)
build_tools_versions = sorted(build_tools_versions,
key=LooseVersion)
build_tools_version = build_tools_versions[-1]
# Folder name for launcher (used by SDL2 bootstrap)
url_scheme = 'kivy'
# Copy backup rules file if specified and update the argument
res_xml_dir = join(res_dir, 'xml')
if args.backup_rules:
ensure_dir(res_xml_dir)
shutil.copy(join(args.private, args.backup_rules), res_xml_dir)
args.backup_rules = split(args.backup_rules)[1][:-4]
# Copy res_xml files to src/main/res/xml
if args.res_xmls:
ensure_dir(res_xml_dir)
for xmlpath in args.res_xmls:
if not os.path.exists(xmlpath):
xmlpath = join(args.private, xmlpath)
shutil.copy(xmlpath, res_xml_dir)
# Render out android manifest:
manifest_path = "src/main/AndroidManifest.xml"
render_args = {
"args": args,
"service": service,
"service_names": service_names,
"android_api": android_api
"android_api": android_api,
"debug": "debug" in args.build_mode,
"native_services": args.native_services
}
if get_bootstrap_name() == "sdl2":
render_args["url_scheme"] = url_scheme
@ -482,9 +560,17 @@ main.py that loads it.''')
aars=aars,
jars=jars,
android_api=android_api,
build_tools_version=build_tools_version
build_tools_version=build_tools_version,
debug_build="debug" in args.build_mode,
is_library=(get_bootstrap_name() == 'service_library'),
)
# gradle properties
render(
'gradle.tmpl.properties',
'gradle.properties',
args=args)
# ant build templates
render(
'build.tmpl.xml',
@ -493,9 +579,18 @@ main.py that loads it.''')
versioned_name=versioned_name)
# String resources:
timestamp = time.time()
if 'SOURCE_DATE_EPOCH' in environ:
# for reproducible builds
timestamp = int(environ['SOURCE_DATE_EPOCH'])
private_version = "{} {} {}".format(
args.version,
args.numeric_version,
timestamp
)
render_args = {
"args": args,
"private_version": str(time.time())
"private_version": hashlib.sha1(private_version.encode()).hexdigest()
}
if get_bootstrap_name() == "sdl2":
render_args["url_scheme"] = url_scheme
@ -527,27 +622,31 @@ main.py that loads it.''')
for patch_name in os.listdir(join('src', 'patches')):
patch_path = join('src', 'patches', patch_name)
print("Applying patch: " + str(patch_path))
# -N: insist this is FORWARD patch, don't reverse apply
# -p1: strip first path component
# -t: batch mode, don't ask questions
patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path]
try:
subprocess.check_output([
# -N: insist this is FORWARd patch, don't reverse apply
# -p1: strip first path component
# -t: batch mode, don't ask questions
"patch", "-N", "-p1", "-t", "-i", patch_path
])
# Use a dry run to establish whether the patch is already applied.
# If we don't check this, the patch may be partially applied (which is bad!)
subprocess.check_output(patch_command + ["--dry-run"])
except subprocess.CalledProcessError as e:
if e.returncode == 1:
# Return code 1 means it didn't apply, this will
# usually mean it is already applied.
print("Warning: failed to apply patch (" +
"exit code 1), " +
"assuming it is already applied: " +
str(patch_path)
)
# Return code 1 means not all hunks could be applied, this usually
# means the patch is already applied.
print("Warning: failed to apply patch (exit code 1), "
"assuming it is already applied: ",
str(patch_path))
else:
raise e
else:
# The dry run worked, so do the real thing
subprocess.check_output(patch_command)
def parse_args(args=None):
def parse_args_and_make_package(args=None):
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
# Get the default minsdk, equal to the NDK API that this dist is built against
@ -602,16 +701,36 @@ tools directory of the Android SDK.
help='Custom key=value to add in application metadata')
ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
ap.add_argument('--asset', dest='assets',
action="append", default=[],
metavar="/path/to/source:dest",
help='Put this in the assets folder at assets/dest')
ap.add_argument('--resource', dest='resources',
action="append", default=[],
metavar="/path/to/source:kind/asset",
help='Put this in the res folder at res/kind')
ap.add_argument('--icon', dest='icon',
help=('A png file to use as the icon for '
'the application.'))
ap.add_argument('--icon-fg', dest='icon_fg',
help=('A png file to use as the foreground of the adaptive icon '
'for the application.'))
ap.add_argument('--icon-bg', dest='icon_bg',
help=('A png file to use as the background of the adaptive icon '
'for the application.'))
ap.add_argument('--service', dest='services', action='append', default=[],
help='Declare a new service entrypoint: '
'NAME:PATH_TO_PY[:foreground]')
ap.add_argument('--native-service', dest='native_services', action='append', default=[],
help='Declare a new native service: '
'package.name.service')
if get_bootstrap_name() != "service_only":
ap.add_argument('--presplash', dest='presplash',
help=('A jpeg file to use as a screen while the '
'application is loading.'))
ap.add_argument('--presplash-lottie', dest='presplash_lottie',
help=('A lottie (json) file to use as an animation while the '
'application is loading.'))
ap.add_argument('--presplash-color',
dest='presplash_color',
default='#000000',
@ -636,6 +755,28 @@ tools directory of the Android SDK.
'https://developer.android.com/guide/'
'topics/manifest/'
'activity-element.html'))
ap.add_argument('--enable-androidx', dest='enable_androidx',
action='store_true',
help=('Enable the AndroidX support library, '
'requires api = 28 or greater'))
ap.add_argument('--android-entrypoint', dest='android_entrypoint',
default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
help='Defines which java class will be used for startup, usually a subclass of PythonActivity')
ap.add_argument('--android-apptheme', dest='android_apptheme',
default='@android:style/Theme.NoTitleBar',
help='Defines which app theme should be selected for the main activity')
ap.add_argument('--add-compile-option', dest='compile_options', default=[],
action='append', help='add compile options to gradle.build')
ap.add_argument('--add-gradle-repository', dest='gradle_repositories',
default=[],
action='append',
help='Ddd a repository for gradle')
ap.add_argument('--add-packaging-option', dest='packaging_options',
default=[],
action='append',
help='Dndroid packaging options')
ap.add_argument('--wakelock', dest='wakelock', action='store_true',
help=('Indicate if the application needs the device '
'to stay on'))
@ -647,6 +788,13 @@ tools directory of the Android SDK.
default=join(curdir, 'whitelist.txt'),
help=('Use a whitelist file to prevent blacklisting of '
'file in the final APK'))
ap.add_argument('--release', dest='build_mode', action='store_const',
const='release', default='debug',
help='Build your app as a non-debug release build. '
'(Disables gdb debugging among other things)')
ap.add_argument('--with-debug-symbols', dest='with_debug_symbols',
action='store_const', const=True, default=False,
help='Will keep debug symbols from `.so` files.')
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 '
@ -674,6 +822,8 @@ tools directory of the Android SDK.
'filename containing xml. The filename should be '
'located relative to the python-for-android '
'directory'))
ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[],
help='Add files to res/xml directory (for example device-filters)', nargs='+')
ap.add_argument('--with-billing', dest='billing_pubkey',
help='If set, the billing service will be added (not implemented)')
ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
@ -685,8 +835,6 @@ tools directory of the Android SDK.
ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
action='store_true',
help='Use the system python during compileall if possible.')
ap.add_argument('--no-compile-pyo', dest='no_compile_pyo', action='store_true',
help='Do not optimise .py files to .pyo.')
ap.add_argument('--sign', action='store_true',
help=('Try to sign the APK with your credentials. You must set '
'the appropriate environment variables.'))
@ -698,10 +846,33 @@ tools directory of the Android SDK.
help='Set the launch mode of the main activity in the manifest.')
ap.add_argument('--allow-backup', dest='allow_backup', default='true',
help="if set to 'false', then android won't backup the application.")
ap.add_argument('--backup-rules', dest='backup_rules', default='',
help=('Backup rules for Android Auto Backup. Argument is a '
'filename containing xml. The filename should be '
'located relative to the private directory containing your source code '
'files (containing your main.py entrypoint). '
'See https://developer.android.com/guide/topics/data/'
'autobackup#IncludingFiles for more information'))
ap.add_argument('--no-byte-compile-python', dest='byte_compile_python',
action='store_false', default=True,
help='Skip byte compile for .py files.')
ap.add_argument('--no-optimize-python', dest='optimize_python',
action='store_false', default=True,
help=('Whether to compile to optimised .pyo files, using -OO '
help=('Whether to compile to optimised .pyc files, using -OO '
'(strips docstrings and asserts)'))
ap.add_argument('--extra-manifest-xml', default='',
help=('Extra xml to write directly inside the <manifest> element of'
'AndroidManifest.xml'))
ap.add_argument('--extra-manifest-application-arguments', default='',
help='Extra arguments to be added to the <manifest><application> tag of'
'AndroidManifest.xml')
ap.add_argument('--manifest-placeholders', dest='manifest_placeholders',
default='[:]', help=('Inject build variables into the manifest '
'via the manifestPlaceholders property'))
ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS,
help='Use that parameter if you need to implement your own PythonServive Java class')
ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
help='The full java class name of the main activity')
# Put together arguments, and add those from .p4a config file:
if args is None:
@ -721,7 +892,6 @@ tools directory of the Android SDK.
_read_configuration()
args = ap.parse_args(args)
args.ignore_path = []
if args.name and args.name[0] == '"' and args.name[-1] == '"':
args.name = args.name[1:-1]
@ -751,21 +921,19 @@ tools directory of the Android SDK.
if args.permissions and isinstance(args.permissions[0], list):
args.permissions = [p for perm in args.permissions for p in perm]
if args.res_xmls and isinstance(args.res_xmls[0], list):
args.res_xmls = [x for res in args.res_xmls for x in res]
if args.try_system_python_compile:
# Hardcoding python2.7 is okay for now, as python3 skips the
# compilation anyway
if not exists('crystax_python'):
python_executable = 'python2.7'
try:
subprocess.call([python_executable, '--version'])
except (OSError, subprocess.CalledProcessError):
pass
else:
PYTHON = python_executable
if args.no_compile_pyo:
PYTHON = None
BLACKLIST_PATTERNS.remove('*.py')
python_executable = 'python2.7'
try:
subprocess.call([python_executable, '--version'])
except (OSError, subprocess.CalledProcessError):
pass
else:
PYTHON = python_executable
if args.blacklist:
with open(args.blacklist) as fd:
@ -791,4 +959,4 @@ tools directory of the Android SDK.
if __name__ == "__main__":
parse_args()
parse_args_and_make_package()

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip

View file

@ -21,7 +21,3 @@ LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog $(EXTRA_LDLIBS)
LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS)
include $(BUILD_SHARED_LIBRARY)
ifdef CRYSTAX_PYTHON_VERSION
$(call import-module,python/$(CRYSTAX_PYTHON_VERSION))
endif

View file

@ -15,15 +15,11 @@
#include <errno.h>
#include "bootstrap_name.h"
#ifndef BOOTSTRAP_USES_NO_SDL_HEADERS
#include "SDL.h"
#ifndef BOOTSTRAP_NAME_PYGAME
#include "SDL_opengles2.h"
#endif
#endif
#ifdef BOOTSTRAP_NAME_PYGAME
#include "jniwrapperstuff.h"
#endif
#include "android/log.h"
#define ENTRYPOINT_MAXLEN 128
@ -169,26 +165,14 @@ int main(int argc, char *argv[]) {
// Set up the python path
char paths[256];
char crystax_python_dir[256];
snprintf(crystax_python_dir, 256,
"%s/crystax_python", getenv("ANDROID_UNPACK"));
char python_bundle_dir[256];
snprintf(python_bundle_dir, 256,
"%s/_python_bundle", getenv("ANDROID_UNPACK"));
if (dir_exists(crystax_python_dir) || dir_exists(python_bundle_dir)) {
if (dir_exists(crystax_python_dir)) {
LOGP("crystax_python exists");
snprintf(paths, 256,
"%s/stdlib.zip:%s/modules",
crystax_python_dir, crystax_python_dir);
}
if (dir_exists(python_bundle_dir)) {
LOGP("_python_bundle dir exists");
snprintf(paths, 256,
"%s/stdlib.zip:%s/modules",
python_bundle_dir, python_bundle_dir);
}
if (dir_exists(python_bundle_dir)) {
LOGP("_python_bundle dir exists");
snprintf(paths, 256,
"%s/stdlib.zip:%s/modules",
python_bundle_dir, python_bundle_dir);
LOGP("calculated paths to be...");
LOGP(paths);
@ -200,24 +184,11 @@ int main(int argc, char *argv[]) {
LOGP("set wchar paths...");
} else {
// We do not expect to see crystax_python any more, so no point
// reminding the user about it. If it does exist, we'll have
// logged it earlier.
LOGP("_python_bundle does not exist");
LOGP("_python_bundle does not exist...this not looks good, all python"
" recipes should have this folder, should we expect a crash soon?");
}
Py_Initialize();
#if PY_MAJOR_VERSION < 3
// Can't Py_SetPath in python2 but we can set PySys_SetPath, which must
// be applied after Py_Initialize rather than before like Py_SetPath
#if PY_MICRO_VERSION >= 15
// Only for python native-build
PySys_SetPath(paths);
#endif
PySys_SetArgv(argc, argv);
#endif
LOGP("Initialized python");
/* ensure threads will work.
@ -236,34 +207,8 @@ int main(int argc, char *argv[]) {
* 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.
* This is only the case if we are using the python2legacy recipe
*/
LOGP("Setting up python from ANDROID_APP_PATH");
PyRun_SimpleString("private = posix.environ['ANDROID_APP_PATH']\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");
}
char add_site_packages_dir[256];
if (dir_exists(crystax_python_dir)) {
snprintf(add_site_packages_dir, 256,
"sys.path.append('%s/site-packages')",
crystax_python_dir);
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");
}
if (dir_exists(python_bundle_dir)) {
snprintf(add_site_packages_dir, 256,
@ -281,13 +226,13 @@ int main(int argc, char *argv[]) {
PyRun_SimpleString(
"class LogFile(object):\n"
" def __init__(self):\n"
" self.buffer = ''\n"
" self.__buffer = ''\n"
" def write(self, s):\n"
" s = self.buffer + s\n"
" lines = s.split(\"\\n\")\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"
" androidembed.log(l.replace('\\x00', ''))\n"
" self.__buffer = lines[-1]\n"
" def flush(self):\n"
" return\n"
"sys.stdout = sys.stderr = LogFile()\n"
@ -306,14 +251,10 @@ int main(int argc, char *argv[]) {
*/
LOGP("Run user program, change dir and execute entrypoint");
/* Get the entrypoint, search the .pyo then .py
/* Get the entrypoint, search the .pyc then .py
*/
char *dot = strrchr(env_entrypoint, '.');
#if PY_MAJOR_VERSION > 2
char *ext = ".pyc";
#else
char *ext = ".pyo";
#endif
if (dot <= 0) {
LOGP("Invalid entrypoint, abort.");
return -1;
@ -329,21 +270,17 @@ int main(int argc, char *argv[]) {
entrypoint[strlen(env_entrypoint) - 1] = '\0';
LOGP(entrypoint);
if (!file_exists(entrypoint)) {
LOGP("Entrypoint not found (.pyc/.pyo, fallback on .py), abort");
LOGP("Entrypoint not found (.pyc, 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 */
/* if .py is passed, check the pyc version first */
strcpy(entrypoint, env_entrypoint);
entrypoint[strlen(env_entrypoint) + 1] = '\0';
#if PY_MAJOR_VERSION > 2
entrypoint[strlen(env_entrypoint)] = 'c';
#else
entrypoint[strlen(env_entrypoint)] = 'o';
#endif
if (!file_exists(entrypoint)) {
/* fallback on pure python version */
if (!file_exists(env_entrypoint)) {
@ -353,7 +290,7 @@ int main(int argc, char *argv[]) {
strcpy(entrypoint, env_entrypoint);
}
} else {
LOGP("Entrypoint have an invalid extension (must be .py or .pyc/.pyo), abort.");
LOGP("Entrypoint have an invalid extension (must be .py or .pyc), abort.");
return -1;
}
// LOGP("Entrypoint is:");
@ -374,8 +311,7 @@ int main(int argc, char *argv[]) {
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 */
if (PyFile_WriteString("\n", f))
PyErr_Clear();
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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
*
* <pre>
* 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
* </pre>
*
*
* File Types
*
* <pre>
* 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
* </pre>
*
*
*
* Ustar header
*
* <pre>
* 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
* </pre>
*/
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;
}
}

View file

@ -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<br>
* 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;
}
}

View file

@ -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] );
}
}
}
}

View file

@ -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();
}
}

View file

@ -14,10 +14,10 @@ import android.app.PendingIntent;
import android.os.Process;
import java.io.File;
import org.kivy.android.PythonUtil;
import org.renpy.android.Hardware;
//imports for channel definition
import android.app.NotificationManager;
import android.app.NotificationChannel;
import android.graphics.Color;
public class PythonService extends Service implements Runnable {
@ -33,6 +33,8 @@ public class PythonService extends Service implements Runnable {
private String serviceEntrypoint;
// Argument to pass to Python code,
private String pythonServiceArgument;
public static PythonService mService = null;
private Intent startIntent = null;
@ -42,10 +44,6 @@ public class PythonService extends Service implements Runnable {
autoRestartService = restart;
}
public boolean canDisplayNotification() {
return true;
}
public int startType() {
return START_NOT_STICKY;
}
@ -64,10 +62,15 @@ public class PythonService extends Service implements Runnable {
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;
return startType();
}
//intent is null if OS restarts a STICKY service
if (intent == null) {
Context context = getApplicationContext();
intent = getThisDefaultIntent(context, "");
}
startIntent = intent;
startIntent = intent;
Bundle extras = intent.getExtras();
androidPrivate = extras.getString("androidPrivate");
androidArgument = extras.getString("androidArgument");
@ -75,28 +78,38 @@ public class PythonService extends Service implements Runnable {
pythonName = extras.getString("pythonName");
pythonHome = extras.getString("pythonHome");
pythonPath = extras.getString("pythonPath");
boolean serviceStartAsForeground = (
extras.getString("serviceStartAsForeground").equals("true")
);
pythonServiceArgument = extras.getString("pythonServiceArgument");
pythonThread = new Thread(this);
pythonThread.start();
if (canDisplayNotification()) {
if (serviceStartAsForeground) {
doStartForeground(extras);
}
return startType();
}
protected int getServiceId() {
return 1;
}
protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) {
return null;
}
protected void doStartForeground(Bundle extras) {
String serviceTitle = extras.getString("serviceTitle");
String serviceDescription = extras.getString("serviceDescription");
Notification notification;
Context context = getApplicationContext();
Intent contextIntent = new Intent(context, PythonActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
notification = new Notification(
context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis());
try {
@ -109,14 +122,26 @@ public class PythonService extends Service implements Runnable {
IllegalArgumentException | InvocationTargetException e) {
}
} else {
Notification.Builder builder = new Notification.Builder(context);
// for android 8+ we need to create our own channel
// https://stackoverflow.com/questions/47531742/startforeground-fail-after-upgrade-to-android-8-1
String NOTIFICATION_CHANNEL_ID = "org.kivy.p4a"; //TODO: make this configurable
String channelName = "Background Service"; //TODO: make this configurable
NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName,
NotificationManager.IMPORTANCE_NONE);
chan.setLightColor(Color.BLUE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(chan);
Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setContentTitle(serviceTitle);
builder.setContentText(serviceDescription);
builder.setContentIntent(pIntent);
builder.setSmallIcon(context.getApplicationInfo().icon);
notification = builder.build();
}
startForeground(1, notification);
startForeground(getServiceId(), notification);
}
@Override
@ -137,7 +162,10 @@ public class PythonService extends Service implements Runnable {
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
stopSelf();
//sticky servcie runtime/restart is managed by the OS. leave it running when app is closed
if (startType() != START_STICKY) {
stopSelf();
}
}
@Override

View file

@ -1,12 +1,20 @@
package org.kivy.android;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.io.FilenameFilter;
import java.util.regex.Pattern;
import org.renpy.android.AssetExtract;
public class PythonUtil {
private static final String TAG = "pythonutil";
@ -32,21 +40,25 @@ public class PythonUtil {
protected static ArrayList<String> getLibraries(File libsDir) {
ArrayList<String> libsList = new ArrayList<String>();
addLibraryIfExists(libsList, "crystax", libsDir);
addLibraryIfExists(libsList, "sqlite3", libsDir);
addLibraryIfExists(libsList, "ffi", libsDir);
addLibraryIfExists(libsList, "png16", libsDir);
addLibraryIfExists(libsList, "ssl.*", libsDir);
addLibraryIfExists(libsList, "crypto.*", libsDir);
libsList.add("python2.7");
addLibraryIfExists(libsList, "SDL2", libsDir);
addLibraryIfExists(libsList, "SDL2_image", libsDir);
addLibraryIfExists(libsList, "SDL2_mixer", libsDir);
addLibraryIfExists(libsList, "SDL2_ttf", libsDir);
libsList.add("python3.5m");
libsList.add("python3.6m");
libsList.add("python3.7m");
libsList.add("python3.8");
libsList.add("python3.9");
libsList.add("main");
return libsList;
}
public static void loadLibraries(File filesDir, File libsDir) {
String filesDirPath = filesDir.getAbsolutePath();
boolean foundPython = false;
for (String lib : getLibraries(libsDir)) {
@ -61,8 +73,8 @@ public class PythonUtil {
// load, and it has failed, give a more
// general error
Log.v(TAG, "Library loading error: " + e.getMessage());
if (lib.startsWith("python3.7") && !foundPython) {
throw new java.lang.RuntimeException("Could not load any libpythonXXX.so");
if (lib.startsWith("python3.9") && !foundPython) {
throw new RuntimeException("Could not load any libpythonXXX.so");
} else if (lib.startsWith("python")) {
continue;
} else {
@ -73,5 +85,174 @@ public class PythonUtil {
}
Log.v(TAG, "Loaded everything!");
}
}
public static String getAppRoot(Context ctx) {
String appRoot = ctx.getFilesDir().getAbsolutePath() + "/app";
return appRoot;
}
public static String getResourceString(Context ctx, String name) {
// Taken from org.renpy.android.ResourceManager
Resources res = ctx.getResources();
int id = res.getIdentifier(name, "string", ctx.getPackageName());
return res.getString(id);
}
/**
* Show an error using a toast. (Only makes sense from non-UI threads.)
*/
protected static void toastError(final Activity activity, final String msg) {
activity.runOnUiThread(new Runnable () {
public void run() {
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show();
}
});
// Wait to show the error.
synchronized (activity) {
try {
activity.wait(1000);
} catch (InterruptedException e) {
}
}
}
protected static void recursiveDelete(File f) {
if (f.isDirectory()) {
for (File r : f.listFiles()) {
recursiveDelete(r);
}
}
f.delete();
}
public static void unpackAsset(
Context ctx,
final String resource,
File target,
boolean cleanup_on_version_update) {
Log.v(TAG, "Unpacking " + resource + " " + target.getName());
// The version of data in memory and on disk.
String dataVersion = getResourceString(ctx, resource + "_version");
String diskVersion = null;
Log.v(TAG, "Data version is " + dataVersion);
// If no version, no unpacking is necessary.
if (dataVersion == null) {
return;
}
// Check the current disk version, if any.
String filesDir = target.getAbsolutePath();
String diskVersionFn = filesDir + "/" + resource + ".version";
try {
byte buf[] = new byte[64];
InputStream is = new FileInputStream(diskVersionFn);
int len = is.read(buf);
diskVersion = new String(buf, 0, len);
is.close();
} catch (Exception e) {
diskVersion = "";
}
// If the disk data is out of date, extract it and write the version file.
if (! dataVersion.equals(diskVersion)) {
Log.v(TAG, "Extracting " + resource + " assets.");
if (cleanup_on_version_update) {
recursiveDelete(target);
}
target.mkdirs();
AssetExtract ae = new AssetExtract(ctx);
if (!ae.extractTar(resource + ".tar", target.getAbsolutePath(), "private")) {
String msg = "Could not extract " + resource + " data.";
if (ctx instanceof Activity) {
toastError((Activity)ctx, msg);
} else {
Log.v(TAG, msg);
}
}
try {
// Write .nomedia.
new File(target, ".nomedia").createNewFile();
// Write version file.
FileOutputStream os = new FileOutputStream(diskVersionFn);
os.write(dataVersion.getBytes());
os.close();
} catch (Exception e) {
Log.w(TAG, e);
}
}
}
public static void unpackPyBundle(
Context ctx,
final String resource,
File target,
boolean cleanup_on_version_update) {
Log.v(TAG, "Unpacking " + resource + " " + target.getName());
// The version of data in memory and on disk.
String dataVersion = getResourceString(ctx, "private_version");
String diskVersion = null;
Log.v(TAG, "Data version is " + dataVersion);
// If no version, no unpacking is necessary.
if (dataVersion == null) {
return;
}
// Check the current disk version, if any.
String filesDir = target.getAbsolutePath();
String diskVersionFn = filesDir + "/" + "libpybundle" + ".version";
try {
byte buf[] = new byte[64];
InputStream is = new FileInputStream(diskVersionFn);
int len = is.read(buf);
diskVersion = new String(buf, 0, len);
is.close();
} catch (Exception e) {
diskVersion = "";
}
if (! dataVersion.equals(diskVersion)) {
// If the disk data is out of date, extract it and write the version file.
Log.v(TAG, "Extracting " + resource + " assets.");
if (cleanup_on_version_update) {
recursiveDelete(target);
}
target.mkdirs();
AssetExtract ae = new AssetExtract(ctx);
if (!ae.extractTar(resource + ".so", target.getAbsolutePath(), "pybundle")) {
String msg = "Could not extract " + resource + " data.";
if (ctx instanceof Activity) {
toastError((Activity)ctx, msg);
} else {
Log.v(TAG, msg);
}
}
try {
// Write version file.
FileOutputStream os = new FileOutputStream(diskVersionFn);
os.write(dataVersion.getBytes());
os.close();
} catch (Exception e) {
Log.w(TAG, e);
}
}
}
}

View file

@ -2,36 +2,34 @@
// spaces amount
package org.renpy.android;
import java.io.*;
import android.app.Activity;
import android.content.Context;
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.OutputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.File;
import java.io.FileInputStream;
import java.util.zip.GZIPInputStream;
import android.content.res.AssetManager;
import org.kamranzafar.jtar.*;
import org.kamranzafar.jtar.TarEntry;
import org.kamranzafar.jtar.TarInputStream;
public class AssetExtract {
private AssetManager mAssetManager = null;
private Activity mActivity = null;
public AssetExtract(Activity act) {
mActivity = act;
mAssetManager = act.getAssets();
public AssetExtract(Context context) {
mAssetManager = context.getAssets();
}
public boolean extractTar(String asset, String target) {
public boolean extractTar(String asset, String target, String method) {
byte buf[] = new byte[1024 * 1024];
@ -39,7 +37,12 @@ public class AssetExtract {
TarInputStream tis = null;
try {
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
if(method == "private"){
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
} else if (method == "pybundle") {
assetStream = new FileInputStream(asset);
}
tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192));
} catch (IOException e) {
Log.e("python", "opening up extract tar", e);
@ -51,7 +54,7 @@ public class AssetExtract {
try {
entry = tis.getNextEntry();
} catch ( java.io.IOException e ) {
} catch ( IOException e ) {
Log.e("python", "extracting tar", e);
return false;
}
@ -76,8 +79,7 @@ public class AssetExtract {
try {
out = new BufferedOutputStream(new FileOutputStream(path), 8192);
} catch ( FileNotFoundException e ) {
} catch ( SecurityException e ) { };
} catch ( FileNotFoundException | SecurityException e ) {}
if ( out == null ) {
Log.e("python", "could not open " + path);
@ -97,7 +99,7 @@ public class AssetExtract {
out.flush();
out.close();
} catch ( java.io.IOException e ) {
} catch ( IOException e ) {
Log.e("python", "extracting zip", e);
return false;
}

View file

@ -0,0 +1,279 @@
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.View;
import java.util.List;
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;
public static final float defaultRv[] = { 0f, 0f, 0f };
/**
* 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<Sensor> 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 {
return defaultRv;
}
}
}
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() {
if ( accelerometerSensor == null )
return defaultRv;
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() {
if ( orientationSensor == null )
return defaultRv;
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() {
if ( magneticFieldSensor == null )
return defaultRv;
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<ScanResult> 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 <list> of scanResults
}
}, i);
}
public static String scanWifi() {
// Now you can call this and it should execute the broadcastReceiver's
// onReceive()
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);
}
}

View file

@ -1,8 +1,7 @@
/**
* 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.
* change. So this is the next best thing.
*/
package org.renpy.android;

View file

@ -1,18 +1,11 @@
package {{ args.package }};
import android.os.Build;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
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;
import {{ args.service_class_name }};
public class Service{{ name|capitalize }} extends PythonService {
public class Service{{ name|capitalize }} extends {{ base_service_class }} {
{% if sticky %}
@Override
public int startType() {
@ -20,54 +13,35 @@ public class Service{{ name|capitalize }} extends PythonService {
}
{% endif %}
{% if not foreground %}
@Override
public boolean canDisplayNotification() {
return false;
}
{% endif %}
@Override
protected void doStartForeground(Bundle extras) {
Notification notification;
Context context = getApplicationContext();
Intent contextIntent = new Intent(context, PythonActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
notification = new Notification(
context.getApplicationInfo().icon, "{{ args.name }}", System.currentTimeMillis());
try {
// prevent using NotificationCompat, this saves 100kb on apk
Method func = notification.getClass().getMethod(
"setLatestEventInfo", Context.class, CharSequence.class,
CharSequence.class, PendingIntent.class);
func.invoke(notification, context, "{{ args.name }}", "{{ name| capitalize }}", pIntent);
} catch (NoSuchMethodException | IllegalAccessException |
IllegalArgumentException | InvocationTargetException e) {
}
} else {
Notification.Builder builder = new Notification.Builder(context);
builder.setContentTitle("{{ args.name }}");
builder.setContentText("{{ name| capitalize }}");
builder.setContentIntent(pIntent);
builder.setSmallIcon(context.getApplicationInfo().icon);
notification = builder.build();
}
startForeground({{ service_id }}, notification);
protected int getServiceId() {
return {{ service_id }};
}
static public void start(Context ctx, String pythonServiceArgument) {
Intent intent = getDefaultIntent(ctx, pythonServiceArgument);
ctx.startService(intent);
}
static public Intent getDefaultIntent(Context ctx, String pythonServiceArgument) {
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
intent.putExtra("androidArgument", argument);
intent.putExtra("serviceTitle", "{{ args.name }}");
intent.putExtra("serviceDescription", "{{ name|capitalize }}");
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
intent.putExtra("pythonName", "{{ name }}");
intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}");
intent.putExtra("pythonHome", argument);
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
ctx.startService(intent);
return intent;
}
@Override
protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) {
return Service{{ name|capitalize }}.getDefaultIntent(ctx, pythonServiceArgument);
}
static public void stop(Context ctx) {

View file

@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.android.tools.build:gradle:7.1.2'
}
}
@ -13,24 +13,46 @@ allprojects {
repositories {
google()
jcenter()
flatDir {
dirs 'libs'
}
{%- for repo in args.gradle_repositories %}
{{repo}}
{%- endfor %}
flatDir {
dirs 'libs'
}
}
}
{% if is_library %}
apply plugin: 'com.android.library'
{% else %}
apply plugin: 'com.android.application'
{% endif %}
android {
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
defaultConfig {
minSdkVersion {{ args.min_sdk_version }}
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }}
versionName '{{ args.version }}'
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
defaultConfig {
minSdkVersion {{ args.min_sdk_version }}
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }}
versionName '{{ args.version }}'
manifestPlaceholders = {{ args.manifest_placeholders}}
}
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
{% if debug_build -%}
doNotStrip '**/*.so'
{% else %}
exclude 'lib/**/gdbserver'
exclude 'lib/**/gdb.setup'
{%- endif %}
}
{% if args.sign -%}
signingConfigs {
release {
@ -40,41 +62,73 @@ android {
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
}
}
{%- endif %}
buildTypes {
debug {
}
release {
{% if args.sign -%}
signingConfig signingConfigs.release
{%- endif %}
}
}
{% if args.packaging_options -%}
packagingOptions {
{%- for option in args.packaging_options %}
{{option}}
{%- endfor %}
}
{%- endif %}
buildTypes {
debug {
}
release {
{% if args.sign -%}
signingConfig signingConfigs.release
{%- endif %}
}
}
compileOptions {
{% if args.enable_androidx %}
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
{% else %}
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
{% endif %}
{%- for option in args.compile_options %}
{{option}}
{%- endfor %}
}
sourceSets {
main {
jniLibs.srcDir 'libs'
java {
{%- for adir, pattern in args.extra_source_dirs -%}
srcDir '{{adir}}'
{%- endfor -%}
}
}
}
aaptOptions {
noCompress "tflite"
}
}
dependencies {
{%- for aar in aars %}
compile(name: '{{ aar }}', ext: 'aar')
{%- endfor -%}
{%- for jar in jars %}
compile files('src/main/libs/{{ jar }}')
{%- endfor -%}
{%- if args.depends -%}
{%- for depend in args.depends %}
compile '{{ depend }}'
{%- endfor %}
{%- endif %}
{%- for aar in aars %}
implementation(name: '{{ aar }}', ext: 'aar')
{%- endfor -%}
{%- for jar in jars %}
implementation files('src/main/libs/{{ jar }}')
{%- endfor -%}
{%- if args.depends -%}
{%- for depend in args.depends %}
implementation '{{ depend }}'
{%- endfor %}
{%- endif %}
{% if args.presplash_lottie %}
implementation 'com.airbnb.android:lottie:3.4.0'
{%- endif %}
}

View file

@ -0,0 +1,4 @@
{% if args.enable_androidx %}
android.useAndroidX=true
android.enableJetifier=true
{% endif %}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerInside"
android:layout_weight="4"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/splashscreen"
/>
</LinearLayout>

View file

@ -0,0 +1,16 @@
from pythonforandroid.toolchain import Bootstrap
class EmptyBootstrap(Bootstrap):
name = 'empty'
recipe_depends = []
can_be_chosen_automatically = False
def assemble_distribution(self):
print('empty bootstrap has no distribute')
exit(1)
bootstrap = EmptyBootstrap()

View file

@ -0,0 +1 @@

View file

@ -13,7 +13,7 @@ EXCLUDE_EXTS = (".py", ".pyc", ".so.o", ".so.a", ".so.libs", ".pyx")
class LbryBootstrap(Bootstrap):
name = 'lbry'
recipe_depends = ['genericndkbuild', ('python2', 'python3crystax')]
recipe_depends = ['genericndkbuild', ('python3', 'python3crystax')]
def run_distribute(self):
info_main("# Creating Android project ({})".format(self.name))

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function
@ -574,7 +574,7 @@ tools directory of the Android SDK.
# Hardcoding python2.7 is okay for now, as python3 skips the
# compilation anyway
if not exists('crystax_python'):
python_executable = 'python2.7'
python_executable = 'python3'
try:
subprocess.call([python_executable, '--version'])
except (OSError, subprocess.CalledProcessError):

View file

@ -0,0 +1,52 @@
from pythonforandroid.toolchain import (
Bootstrap, shprint, current_directory, info, info_main)
from pythonforandroid.util import ensure_dir
from os.path import join
import sh
class SDL2GradleBootstrap(Bootstrap):
name = 'sdl2'
recipe_depends = list(
set(Bootstrap.recipe_depends).union({'sdl2'})
)
def assemble_distribution(self):
info_main("# Creating Android project ({})".format(self.name))
info("Copying SDL2/gradle build")
shprint(sh.rm, "-rf", self.dist_dir)
shprint(sh.cp, "-r", self.build_dir, self.dist_dir)
# either the build use environment variable (ANDROID_HOME)
# or the local.properties if exists
with current_directory(self.dist_dir):
with open('local.properties', 'w') as fileh:
fileh.write('sdk.dir={}'.format(self.ctx.sdk_dir))
with current_directory(self.dist_dir):
info("Copying Python distribution")
self.distribute_javaclasses(self.ctx.javaclass_dir,
dest_dir=join("src", "main", "java"))
for arch in self.ctx.archs:
python_bundle_dir = join(f'_python_bundle__{arch.arch}', '_python_bundle')
ensure_dir(python_bundle_dir)
self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)])
site_packages_dir = self.ctx.python_recipe.create_python_bundle(
join(self.dist_dir, python_bundle_dir), arch)
if not self.ctx.with_debug_symbols:
self.strip_libraries(arch)
self.fry_eggs(site_packages_dir)
if 'sqlite3' not in self.ctx.recipe_build_order:
with open('blacklist.txt', 'a') as fileh:
fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n')
super().assemble_distribution()
bootstrap = SDL2GradleBootstrap()

View file

@ -0,0 +1,14 @@
.gradle
/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties

View file

@ -0,0 +1,84 @@
# prevent user to include invalid extensions
*.apk
*.aab
*.apks
*.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/_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

View file

@ -0,0 +1,8 @@
# 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)
APP_PLATFORM := $(NDK_API)

View file

@ -0,0 +1,12 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := main
LOCAL_SRC_FILES := start.c
LOCAL_STATIC_LIBRARIES := SDL2_static
include $(BUILD_SHARED_LIBRARY)
$(call import-module,SDL)LOCAL_PATH := $(call my-dir)

View file

@ -0,0 +1,5 @@
#define BOOTSTRAP_NAME_SDL2
const char bootstrap_name[] = "SDL2"; // capitalized for historic reasons

View file

@ -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);
}
}

View file

@ -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);
};

View file

@ -0,0 +1,643 @@
package org.kivy.android;
import java.io.InputStream;
import java.io.FileWriter;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.PowerManager;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import android.content.res.Resources.NotFoundException;
import org.libsdl.app.SDLActivity;
import org.kivy.android.launcher.Project;
import org.renpy.android.ResourceManager;
public class PythonActivity extends SDLActivity {
private static final String TAG = "PythonActivity";
public static PythonActivity mActivity = null;
private ResourceManager resourceManager = null;
private Bundle mMetaData = null;
private PowerManager.WakeLock mWakeLock = null;
public String getAppRoot() {
String app_root = getFilesDir().getAbsolutePath() + "/app";
return app_root;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.v(TAG, "PythonActivity onCreate running");
resourceManager = new ResourceManager(this);
Log.v(TAG, "About to do super onCreate");
super.onCreate(savedInstanceState);
Log.v(TAG, "Did super onCreate");
this.mActivity = this;
this.showLoadingScreen(this.getLoadingScreen());
new UnpackFilesTask().execute(getAppRoot());
}
public void loadLibraries() {
String app_root = new String(getAppRoot());
File app_root_file = new File(app_root);
PythonUtil.loadLibraries(app_root_file,
new File(getApplicationInfo().nativeLibraryDir));
}
/**
* 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) {
}
}
}
private class UnpackFilesTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... params) {
File app_root_file = new File(params[0]);
Log.v(TAG, "Ready to unpack");
PythonUtil.unpackAsset(mActivity, "private", app_root_file, true);
PythonUtil.unpackPyBundle(mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false);
return null;
}
@Override
protected void onPostExecute(String result) {
// Figure out the directory where the game is. If the game was
// given to us via an intent, then we use the scheme-specific
// part of that intent to determine the file to launch. We
// also use the android.txt file to determine the orientation.
//
// Otherwise, we use the public data, if we have it, or the
// private data if we do not.
mActivity.finishLoad();
// finishLoad called setContentView with the SDL view, which
// removed the loading screen. However, we still need it to
// show until the app is ready to render, so pop it back up
// on top of the SDL view.
mActivity.showLoadingScreen(getLoadingScreen());
String app_root_dir = getAppRoot();
if (getIntent() != null && getIntent().getAction() != null &&
getIntent().getAction().equals("org.kivy.LAUNCH")) {
File path = new File(getIntent().getData().getSchemeSpecificPart());
Project p = Project.scanDirectory(path);
String entry_point = getEntryPoint(p.dir);
SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", p.dir + "/" + entry_point);
SDLActivity.nativeSetenv("ANDROID_ARGUMENT", p.dir);
SDLActivity.nativeSetenv("ANDROID_APP_PATH", p.dir);
if (p != null) {
if (p.landscape) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
// Let old apps know they started.
try {
FileWriter f = new FileWriter(new File(path, ".launch"));
f.write("started");
f.close();
} catch (IOException e) {
// pass
}
} else {
String entry_point = getEntryPoint(app_root_dir);
SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point);
SDLActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir);
SDLActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir);
}
String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath();
Log.v(TAG, "Setting env vars for start.c and Python to use");
SDLActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory);
SDLActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir);
SDLActivity.nativeSetenv("PYTHONHOME", app_root_dir);
SDLActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib");
SDLActivity.nativeSetenv("PYTHONOPTIMIZE", "2");
try {
Log.v(TAG, "Access to our meta-data...");
mActivity.mMetaData = mActivity.getPackageManager().getApplicationInfo(
mActivity.getPackageName(), PackageManager.GET_META_DATA).metaData;
PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
if ( mActivity.mMetaData.getInt("wakelock") == 1 ) {
mActivity.mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Screen On");
mActivity.mWakeLock.acquire();
}
if ( mActivity.mMetaData.getInt("surface.transparent") != 0 ) {
Log.v(TAG, "Surface will be transparent.");
getSurface().setZOrderOnTop(true);
getSurface().getHolder().setFormat(PixelFormat.TRANSPARENT);
} else {
Log.i(TAG, "Surface will NOT be transparent");
}
} catch (PackageManager.NameNotFoundException e) {
}
// Launch app if that hasn't been done yet:
if (mActivity.mHasFocus && (
// never went into proper resume state:
mActivity.mCurrentNativeState == NativeState.INIT ||
(
// resumed earlier but wasn't ready yet
mActivity.mCurrentNativeState == NativeState.RESUMED &&
mActivity.mSDLThread == null
))) {
// Because sometimes the app will get stuck here and never
// actually run, ensure that it gets launched if we're active:
mActivity.resumeNativeThread();
}
}
@Override
protected void onPreExecute() {
}
@Override
protected void onProgressUpdate(Void... values) {
}
}
public static ViewGroup getLayout() {
return mLayout;
}
public static SurfaceView getSurface() {
return mSurface;
}
//----------------------------------------------------------------------------
// Listener interface for onNewIntent
//
public interface NewIntentListener {
void onNewIntent(Intent intent);
}
private List<NewIntentListener> newIntentListeners = null;
public void registerNewIntentListener(NewIntentListener listener) {
if ( this.newIntentListeners == null )
this.newIntentListeners = Collections.synchronizedList(new ArrayList<NewIntentListener>());
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<NewIntentListener> 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<ActivityResultListener> activityResultListeners = null;
public void registerActivityResultListener(ActivityResultListener listener) {
if ( this.activityResultListeners == null )
this.activityResultListeners = Collections.synchronizedList(new ArrayList<ActivityResultListener>());
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<ActivityResultListener> iterator = this.activityResultListeners.iterator();
while ( iterator.hasNext() )
(iterator.next()).onActivityResult(requestCode, resultCode, intent);
}
}
public static void start_service(
String serviceTitle,
String serviceDescription,
String pythonServiceArgument
) {
_do_start_service(
serviceTitle, serviceDescription, pythonServiceArgument, true
);
}
public static void start_service_not_as_foreground(
String serviceTitle,
String serviceDescription,
String pythonServiceArgument
) {
_do_start_service(
serviceTitle, serviceDescription, pythonServiceArgument, false
);
}
public static void _do_start_service(
String serviceTitle,
String serviceDescription,
String pythonServiceArgument,
boolean showForegroundNotification
) {
Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath();
String app_root_dir = PythonActivity.mActivity.getAppRoot();
String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service");
serviceIntent.putExtra("androidPrivate", argument);
serviceIntent.putExtra("androidArgument", app_root_dir);
serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point);
serviceIntent.putExtra("pythonName", "python");
serviceIntent.putExtra("pythonHome", app_root_dir);
serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib");
serviceIntent.putExtra("serviceStartAsForeground",
(showForegroundNotification ? "true" : "false")
);
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);
}
/** Loading screen view **/
public static ImageView mImageView = null;
public static View mLottieView = null;
/** Whether main routine/actual app has started yet **/
protected boolean mAppConfirmedActive = false;
/** Timer for delayed loading screen removal. **/
protected Timer loadingScreenRemovalTimer = null;
// Overridden since it's called often, to check whether to remove the
// loading screen:
@Override
protected boolean sendCommand(int command, Object data) {
boolean result = super.sendCommand(command, data);
considerLoadingScreenRemoval();
return result;
}
/** Confirm that the app's main routine has been launched.
**/
@Override
public void appConfirmedActive() {
if (!mAppConfirmedActive) {
Log.v(TAG, "appConfirmedActive() -> preparing loading screen removal");
mAppConfirmedActive = true;
considerLoadingScreenRemoval();
}
}
/** This is called from various places to check whether the app's main
* routine has been launched already, and if it has, then the loading
* screen will be removed.
**/
public void considerLoadingScreenRemoval() {
if (loadingScreenRemovalTimer != null)
return;
runOnUiThread(new Runnable() {
public void run() {
if (((PythonActivity)PythonActivity.mSingleton).mAppConfirmedActive &&
loadingScreenRemovalTimer == null) {
// Remove loading screen but with a delay.
// (app can use p4a's android.loadingscreen module to
// do it quicker if it wants to)
// get a handler (call from main thread)
// this will run when timer elapses
TimerTask removalTask = new TimerTask() {
@Override
public void run() {
// post a runnable to the handler
runOnUiThread(new Runnable() {
@Override
public void run() {
PythonActivity activity =
((PythonActivity)PythonActivity.mSingleton);
if (activity != null)
activity.removeLoadingScreen();
}
});
}
};
loadingScreenRemovalTimer = new Timer();
loadingScreenRemovalTimer.schedule(removalTask, 5000);
}
}
});
}
public void removeLoadingScreen() {
runOnUiThread(new Runnable() {
public void run() {
View view = mLottieView != null ? mLottieView : mImageView;
if (view != null && view.getParent() != null) {
((ViewGroup)view.getParent()).removeView(view);
mLottieView = null;
mImageView = null;
}
}
});
}
public String getEntryPoint(String search_dir) {
/* Get the main file (.pyc|.py) depending on if we
* have a compiled version or not.
*/
List<String> entryPoints = new ArrayList<String>();
entryPoints.add("main.pyc"); // python 3 compiled files
for (String value : entryPoints) {
File mainFile = new File(search_dir + "/" + value);
if (mainFile.exists()) {
return value;
}
}
return "main.py";
}
protected void showLoadingScreen(View view) {
try {
if (mLayout == null) {
setContentView(view);
} else if (view.getParent() == null) {
mLayout.addView(view);
}
} catch (IllegalStateException e) {
// The loading screen can be attempted to be applied twice if app
// is tabbed in/out, quickly.
// (Gives error "The specified child already has a parent.
// You must call removeView() on the child's parent first.")
}
}
protected void setBackgroundColor(View view) {
/*
* Set the presplash loading screen background color
* https://developer.android.com/reference/android/graphics/Color.html
* Parse the color string, and return the corresponding color-int.
* If the string cannot be parsed, throws an IllegalArgumentException exception.
* Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
* 'red', 'blue', 'green', 'black', 'white', 'gray', 'cyan', 'magenta', 'yellow',
* 'lightgray', 'darkgray', 'grey', 'lightgrey', 'darkgrey', 'aqua', 'fuchsia',
* 'lime', 'maroon', 'navy', 'olive', 'purple', 'silver', 'teal'.
*/
String backgroundColor = resourceManager.getString("presplash_color");
if (backgroundColor != null) {
try {
view.setBackgroundColor(Color.parseColor(backgroundColor));
} catch (IllegalArgumentException e) {}
}
}
protected View getLoadingScreen() {
// If we have an mLottieView or mImageView already, then do
// nothing because it will have already been made the content
// view or added to the layout.
if (mLottieView != null || mImageView != null) {
// we already have a splash screen
return mLottieView != null ? mLottieView : mImageView;
}
// first try to load the lottie one
try {
mLottieView = getLayoutInflater().inflate(
this.resourceManager.getIdentifier("lottie", "layout"),
mLayout,
false
);
try {
if (mLayout == null) {
setContentView(mLottieView);
} else if (PythonActivity.mLottieView.getParent() == null) {
mLayout.addView(mLottieView);
}
} catch (IllegalStateException e) {
// The loading screen can be attempted to be applied twice if app
// is tabbed in/out, quickly.
// (Gives error "The specified child already has a parent.
// You must call removeView() on the child's parent first.")
}
setBackgroundColor(mLottieView);
return mLottieView;
}
catch (NotFoundException e) {
Log.v("SDL", "couldn't find lottie layout or animation, trying static splash");
}
// no lottie asset, try to load the static image then
int presplashId = this.resourceManager.getIdentifier("presplash", "drawable");
InputStream is = this.getResources().openRawResource(presplashId);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(is);
} finally {
try {
is.close();
} catch (IOException e) {};
}
mImageView = new ImageView(this);
mImageView.setImageBitmap(bitmap);
setBackgroundColor(mImageView);
mImageView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.FILL_PARENT));
mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
return mImageView;
}
@Override
protected void onPause() {
if (this.mWakeLock != null && mWakeLock.isHeld()) {
this.mWakeLock.release();
}
Log.v(TAG, "onPause()");
try {
super.onPause();
} catch (UnsatisfiedLinkError e) {
// Catch pause while still in loading screen failing to
// call native function (since it's not yet loaded)
}
}
@Override
protected void onResume() {
if (this.mWakeLock != null) {
this.mWakeLock.acquire();
}
Log.v(TAG, "onResume()");
try {
super.onResume();
} catch (UnsatisfiedLinkError e) {
// Catch resume while still in loading screen failing to
// call native function (since it's not yet loaded)
}
considerLoadingScreenRemoval();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
try {
super.onWindowFocusChanged(hasFocus);
} catch (UnsatisfiedLinkError e) {
// Catch window focus while still in loading screen failing to
// call native function (since it's not yet loaded)
}
considerLoadingScreenRemoval();
}
/**
* Used by android.permissions p4a module to register a call back after
* requesting runtime permissions
**/
public interface PermissionsCallback {
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
}
private PermissionsCallback permissionCallback;
private boolean havePermissionsCallback = false;
public void addPermissionsCallback(PermissionsCallback callback) {
permissionCallback = callback;
havePermissionsCallback = true;
Log.v(TAG, "addPermissionsCallback(): Added callback for onRequestPermissionsResult");
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
Log.v(TAG, "onRequestPermissionsResult()");
if (havePermissionsCallback) {
Log.v(TAG, "onRequestPermissionsResult passed to callback");
permissionCallback.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
/**
* Used by android.permissions p4a module to check a permission
**/
public boolean checkCurrentPermission(String permission) {
if (android.os.Build.VERSION.SDK_INT < 23)
return true;
try {
java.lang.reflect.Method methodCheckPermission =
Activity.class.getMethod("checkSelfPermission", String.class);
Object resultObj = methodCheckPermission.invoke(this, permission);
int result = Integer.parseInt(resultObj.toString());
if (result == PackageManager.PERMISSION_GRANTED)
return true;
} catch (IllegalAccessException | NoSuchMethodException |
InvocationTargetException e) {
}
return false;
}
/**
* Used by android.permissions p4a module to request runtime permissions
**/
public void requestPermissionsWithRequestCode(String[] permissions, int requestCode) {
if (android.os.Build.VERSION.SDK_INT < 23)
return;
try {
java.lang.reflect.Method methodRequestPermission =
Activity.class.getMethod("requestPermissions",
String[].class, int.class);
methodRequestPermission.invoke(this, permissions, requestCode);
} catch (IllegalAccessException | NoSuchMethodException |
InvocationTargetException e) {
}
}
public void requestPermissions(String[] permissions) {
requestPermissionsWithRequestCode(permissions, 1);
}
public static void changeKeyboard(int inputType) {
if (SDLActivity.keyboardInputType != inputType){
SDLActivity.keyboardInputType = inputType;
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.restartInput(mTextEdit);
}
}
}

View file

@ -0,0 +1,99 @@
package org.kivy.android.launcher;
import java.io.UnsupportedEncodingException;
import java.io.File;
import java.io.FileInputStream;
import java.util.Properties;
import android.util.Log;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
/**
* This represents a project we've scanned for.
*/
public class Project {
public String dir = null;
String title = null;
String author = null;
Bitmap icon = null;
public boolean landscape = false;
static String decode(String s) {
try {
return new String(s.getBytes("ISO-8859-1"), "UTF-8");
} catch (UnsupportedEncodingException e) {
return s;
}
}
/**
* Scans directory for a android.txt file. If it finds one,
* and it looks valid enough, then it creates a new Project,
* and returns that. Otherwise, returns null.
*/
public static Project scanDirectory(File dir) {
// We might have a link file.
if (dir.getAbsolutePath().endsWith(".link")) {
try {
// Scan the android.txt file.
File propfile = new File(dir, "android.txt");
FileInputStream in = new FileInputStream(propfile);
Properties p = new Properties();
p.load(in);
in.close();
String directory = p.getProperty("directory", null);
if (directory == null) {
return null;
}
dir = new File(directory);
} catch (Exception e) {
Log.i("Project", "Couldn't open link file " + dir, e);
}
}
// Make sure we're dealing with a directory.
if (! dir.isDirectory()) {
return null;
}
try {
// Scan the android.txt file.
File propfile = new File(dir, "android.txt");
FileInputStream in = new FileInputStream(propfile);
Properties p = new Properties();
p.load(in);
in.close();
// Get the various properties.
String title = decode(p.getProperty("title", "Untitled"));
String author = decode(p.getProperty("author", ""));
boolean landscape = p.getProperty("orientation", "portrait").equals("landscape");
// Create the project object.
Project rv = new Project();
rv.title = title;
rv.author = author;
rv.icon = BitmapFactory.decodeFile(new File(dir, "icon.png").getAbsolutePath());
rv.landscape = landscape;
rv.dir = dir.getAbsolutePath();
return rv;
} catch (Exception e) {
Log.i("Project", "Couldn't open android.txt", e);
}
return null;
}
}

View file

@ -0,0 +1,35 @@
package org.kivy.android.launcher;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.ImageView;
import org.renpy.android.ResourceManager;
public class ProjectAdapter extends ArrayAdapter<Project> {
private ResourceManager resourceManager;
public ProjectAdapter(Activity context) {
super(context, 0);
resourceManager = new ResourceManager(context);
}
public View getView(int position, View convertView, ViewGroup parent) {
Project p = getItem(position);
View v = resourceManager.inflateView("chooser_item");
TextView title = (TextView) resourceManager.getViewById(v, "title");
TextView author = (TextView) resourceManager.getViewById(v, "author");
ImageView icon = (ImageView) resourceManager.getViewById(v, "icon");
title.setText(p.title);
author.setText(p.author);
icon.setImageBitmap(p.icon);
return v;
}
}

View file

@ -0,0 +1,90 @@
package org.kivy.android.launcher;
import android.app.Activity;
import android.content.Intent;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AdapterView;
import android.os.Environment;
import java.io.File;
import java.util.Arrays;
import android.net.Uri;
import org.renpy.android.ResourceManager;
public class ProjectChooser extends Activity implements AdapterView.OnItemClickListener {
ResourceManager resourceManager;
String urlScheme;
@Override
public void onStart()
{
super.onStart();
resourceManager = new ResourceManager(this);
urlScheme = resourceManager.getString("urlScheme");
// Set the window title.
setTitle(resourceManager.getString("appName"));
// Scan the sdcard for files, and sort them.
File dir = new File(Environment.getExternalStorageDirectory(), urlScheme);
File entries[] = dir.listFiles();
if (entries == null) {
entries = new File[0];
}
Arrays.sort(entries);
// Create a ProjectAdapter and fill it with projects.
ProjectAdapter projectAdapter = new ProjectAdapter(this);
// Populate it with the properties files.
for (File d : entries) {
Project p = Project.scanDirectory(d);
if (p != null) {
projectAdapter.add(p);
}
}
if (projectAdapter.getCount() != 0) {
View v = resourceManager.inflateView("project_chooser");
ListView l = (ListView) resourceManager.getViewById(v, "projectList");
l.setAdapter(projectAdapter);
l.setOnItemClickListener(this);
setContentView(v);
} else {
View v = resourceManager.inflateView("project_empty");
TextView emptyText = (TextView) resourceManager.getViewById(v, "emptyText");
emptyText.setText("No projects are available to launch. Please place a project into " + dir + " and restart this application. Press the back button to exit.");
setContentView(v);
}
}
public void onItemClick(AdapterView parent, View view, int position, long id) {
Project p = (Project) parent.getItemAtPosition(position);
Intent intent = new Intent(
"org.kivy.LAUNCH",
Uri.fromParts(urlScheme, p.dir, ""));
intent.setClassName(getPackageName(), "org.kivy.android.PythonActivity");
this.startActivity(intent);
this.finish();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:gravity="center"
>
<ImageView
android:id="@+id/icon"
android:layout_width="64sp"
android:layout_height="64sp"
android:scaleType="fitCenter"
android:padding="2sp"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/title"
android:textSize="18sp"
android:textColor="#fff"
android:singleLine="true"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:id="@+id/author"
/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, SDLActivity"
/>
</LinearLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
>
<TextView
android:text="Please choose a project:"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="4sp"
/>
<ListView
android:id="@+id/projectList"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
>
<TextView
android:id="@+id/emptyText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="4sp"
/>
</LinearLayout>

View file

@ -0,0 +1,75 @@
--- a/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/src/main/java/org/libsdl/app/SDLActivity.java
@@ -222,6 +222,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
// This is what SDL runs in. It invokes SDL_main(), eventually
protected static Thread mSDLThread;
+ public static int keyboardInputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
+
protected static SDLGenericMotionListener_API12 getMotionListener() {
if (mMotionListener == null) {
if (Build.VERSION.SDK_INT >= 26) {
@@ -324,6 +326,15 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
Log.v(TAG, "onCreate()");
super.onCreate(savedInstanceState);
+ SDLActivity.initialize();
+ // So we can call stuff from static callbacks
+ mSingleton = this;
+ }
+
+ // We don't do this in onCreate because we unpack and load the app data on a thread
+ // and we can't run setup tasks until that thread completes.
+ protected void finishLoad() {
+
try {
Thread.currentThread().setName("SDLActivity");
} catch (Exception e) {
@@ -835,7 +846,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
Handler commandHandler = new SDLCommandHandler();
// Send a message from the SDLMain thread
- boolean sendCommand(int command, Object data) {
+ protected boolean sendCommand(int command, Object data) {
Message msg = commandHandler.obtainMessage();
msg.arg1 = command;
msg.obj = data;
@@ -1384,6 +1395,20 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
return SDLActivity.mSurface.getNativeSurface();
}
+ /**
+ * Calls turnActive() on singleton to keep loading screen active
+ */
+ public static void triggerAppConfirmedActive() {
+ mSingleton.appConfirmedActive();
+ }
+
+ /**
+ * Trick needed for loading screen, overridden by PythonActivity
+ * to keep loading screen active
+ */
+ public void appConfirmedActive() {
+ }
+
// Input
/**
@@ -1878,6 +1903,7 @@ class SDLMain implements Runnable {
Log.v("SDL", "Running main function " + function + " from library " + library);
+ SDLActivity.mSingleton.appConfirmedActive();
SDLActivity.nativeRunMain(library, function, arguments);
Log.v("SDL", "Finished main function");
@@ -1935,8 +1961,7 @@ class DummyEdit extends View implements View.OnKeyListener {
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
ic = new SDLInputConnection(this, true);
- outAttrs.inputType = InputType.TYPE_CLASS_TEXT |
- InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ outAttrs.inputType = SDLActivity.keyboardInputType | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;

View file

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Replace org.libsdl.app with the identifier of your game below, e.g.
com.gamemaker.game
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{ args.package }}"
android:versionCode="{{ args.numeric_version }}"
android:versionName="{{ args.version }}"
android:installLocation="auto">
<supports-screens
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true"
{% if args.min_sdk_version >= 9 %}
android:xlargeScreens="true"
{% endif %}
/>
<!-- Android 2.3.3 -->
<uses-sdk android:minSdkVersion="{{ args.min_sdk_version }}" android:targetSdkVersion="{{ android_api }}" />
<!-- OpenGL ES 2.0 -->
<uses-feature android:glEsVersion="0x00020000" />
<!-- Allow writing to external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
{% for perm in args.permissions %}
{% if '.' in perm %}
<uses-permission android:name="{{ perm }}" />
{% else %}
<uses-permission android:name="android.permission.{{ perm }}" />
{% endif %}
{% endfor %}
{% if args.wakelock %}
<uses-permission android:name="android.permission.WAKE_LOCK" />
{% endif %}
{% if args.billing_pubkey %}
<uses-permission android:name="com.android.vending.BILLING" />
{% endif %}
{{ args.extra_manifest_xml }}
<!-- Create a Java class extending SDLActivity and place it in a
directory under src matching the package, e.g.
src/com/gamemaker/game/MyGame.java
then replace "SDLActivity" with the name of your class (e.g. "MyGame")
in the XML below.
An example Java class can be found in README-android.txt
-->
<application android:label="@string/app_name"
{% if debug %}android:debuggable="true"{% endif %}
android:icon="@mipmap/icon"
android:allowBackup="{{ args.allow_backup }}"
{% if args.backup_rules %}android:fullBackupContent="@xml/{{ args.backup_rules }}"{% endif %}
{{ args.extra_manifest_application_arguments }}
android:theme="{{args.android_apptheme}}{% if not args.window %}.Fullscreen{% endif %}"
android:hardwareAccelerated="true"
android:extractNativeLibs="true" >
{% for l in args.android_used_libs %}
<uses-library android:name="{{ l }}" />
{% endfor %}
{% for m in args.meta_data %}
<meta-data android:name="{{ m.split('=', 1)[0] }}" android:value="{{ m.split('=', 1)[-1] }}"/>{% endfor %}
<meta-data android:name="wakelock" android:value="{% if args.wakelock %}1{% else %}0{% endif %}"/>
<activity android:name="{{args.android_entrypoint}}"
android:label="@string/app_name"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|fontScale|uiMode{% if args.min_sdk_version >= 8 %}|uiMode{% endif %}{% if args.min_sdk_version >= 13 %}|screenSize|smallestScreenSize{% endif %}{% if args.min_sdk_version >= 17 %}|layoutDirection{% endif %}{% if args.min_sdk_version >= 24 %}|density{% endif %}"
android:screenOrientation="{{ args.orientation }}"
android:exported="true"
{% if args.activity_launch_mode %}
android:launchMode="{{ args.activity_launch_mode }}"
{% endif %}
>
{% if args.launcher %}
<intent-filter>
<action android:name="org.kivy.LAUNCH" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="{{ url_scheme }}" />
</intent-filter>
{% else %}
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
{% endif %}
{%- if args.intent_filters -%}
{{- args.intent_filters -}}
{%- endif -%}
</activity>
{% if args.launcher %}
<activity android:name="org.kivy.android.launcher.ProjectChooser"
android:icon="@mipmap/icon"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
{% endif %}
{% if service or args.launcher %}
<service android:name="{{ args.service_class_name }}"
android:process=":pythonservice" />
{% endif %}
{% for name in service_names %}
<service android:name="{{ args.package }}.Service{{ name|capitalize }}"
android:process=":service_{{ name }}" />
{% endfor %}
{% for name in native_services %}
<service android:name="{{ name }}" />
{% endfor %}
{% if args.billing_pubkey %}
<service android:name="org.kivy.android.billing.BillingReceiver"
android:process=":pythonbilling" />
<receiver android:name="org.kivy.android.billing.BillingReceiver"
android:process=":pythonbillingreceiver"
android:exported="false">
<intent-filter>
<action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
<action android:name="com.android.vending.billing.RESPONSE_CODE" />
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
</intent-filter>
</receiver>
{% endif %}
{% for a in args.add_activity %}
<activity android:name="{{ a }}"></activity>
{% endfor %}
</application>
</manifest>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">{{ args.name }}</string>
<string name="private_version">{{ private_version }}</string>
<string name="presplash_color">{{ args.presplash_color }}</string>
<string name="urlScheme">{{ url_scheme }}</string>
</resources>

View file

@ -0,0 +1,9 @@
from pythonforandroid.bootstraps.service_only import ServiceOnlyBootstrap
class ServiceLibraryBootstrap(ServiceOnlyBootstrap):
name = 'service_library'
bootstrap = ServiceLibraryBootstrap()

View file

@ -0,0 +1,6 @@
#define BOOTSTRAP_NAME_LIBRARY
#define BOOTSTRAP_USES_NO_SDL_HEADERS
const char bootstrap_name[] = "service_library";

View file

@ -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);
}
}

View file

@ -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);
};

View file

@ -0,0 +1,9 @@
package org.kivy.android;
import android.app.Activity;
// Required by PythonService class
public class PythonActivity extends Activity {
public static PythonActivity mActivity = null;
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{ args.package }}"
android:versionCode="{{ args.numeric_version }}"
android:versionName="{{ args.version }}">
<!-- Android 2.3.3 -->
<uses-sdk android:minSdkVersion="{{ args.min_sdk_version }}" android:targetSdkVersion="{{ android_api }}" />
<application {% if debug %}android:debuggable="true"{% endif %} >
{% for name in service_names %}
<service android:name="{{ args.package }}.Service{{ name|capitalize }}"
android:process=":service_{{ name }}"
android:exported="true" />
{% endfor %}
</application>
</manifest>

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