Compare commits
11 commits
master
...
test-112-d
Author | SHA1 | Date | |
---|---|---|---|
|
16b6b7619a | ||
|
69f2c58f30 | ||
|
eda66969b1 | ||
|
4a0b6742f9 | ||
|
943fea3545 | ||
|
f3600c40be | ||
|
258a79b272 | ||
|
5f7861b7be | ||
|
6d519ea619 | ||
|
718044ee02 | ||
|
156b43c72f |
981 changed files with 58286 additions and 3900 deletions
4
.github/workflows/deploy.yml
vendored
4
.github/workflows/deploy.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -3,6 +3,7 @@
|
|||
.gradle
|
||||
app/node_modules/
|
||||
bin
|
||||
venv
|
||||
buildozer.spec
|
||||
build.log
|
||||
recipes/**/*.pyc
|
||||
|
|
7
BUILD.md
7
BUILD.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
1857
p4a/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
115
p4a/Dockerfile
Normal file
115
p4a/Dockerfile
Normal 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}
|
|
@ -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
129
p4a/Makefile
Normal 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)
|
150
p4a/README.md
150
p4a/README.md
|
@ -1,19 +1,25 @@
|
|||
# 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.
|
||||
- Multiple architecture targets, for APKs optimised on any given
|
||||
device.
|
||||
- AAB: Android App Bundle support.
|
||||
|
||||
For documentation and support, see:
|
||||
|
||||
|
@ -21,76 +27,118 @@ For documentation and support, see:
|
|||
- 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
45
p4a/ci/constants.py
Normal 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
114
p4a/ci/makefiles/android.mk
Normal 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
13
p4a/ci/makefiles/osx.mk
Normal 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
13
p4a/ci/osx_ci.sh
Normal 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
111
p4a/ci/rebuild_updated_recipes.py
Executable 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()
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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::
|
||||
|
||||
|
|
|
@ -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,7 @@ 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``,
|
||||
|
@ -103,6 +72,8 @@ options (this list may not be exhaustive):
|
|||
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
|
||||
|
@ -118,14 +89,16 @@ options (this list may not be exhaustive):
|
|||
- ``--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.
|
||||
- ``--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,python2``.
|
||||
``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
|
||||
|
@ -145,7 +118,7 @@ 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``.
|
||||
- ``--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``,
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
68
p4a/doc/source/docker.rst
Normal 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
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~
|
||||
|
|
|
@ -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'
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
226
p4a/doc/source/testing_pull_requests.rst
Normal file
226
p4a/doc/source/testing_pull_requests.rst
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
__version__ = '0.5'
|
||||
__version__ = '2022.09.04'
|
||||
|
|
83
p4a/pythonforandroid/androidndk.py
Normal file
83
p4a/pythonforandroid/androidndk.py
Normal 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")
|
|
@ -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]])
|
||||
|
||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
||||
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))
|
||||
|
||||
env['CFLAGS'] = ' '.join(cflags)
|
||||
|
||||
# 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
|
||||
]) + ' '
|
||||
|
||||
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])
|
||||
# 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
|
||||
)
|
||||
|
||||
env['LDFLAGS'] += '--sysroot={} '.format(self.ctx.ndk_platform)
|
||||
@property
|
||||
def clang_exe(self):
|
||||
"""Full path of the clang compiler depending on the android's ndk
|
||||
version used."""
|
||||
return self.get_clang_exe()
|
||||
|
||||
env["CXXFLAGS"] = env["CFLAGS"]
|
||||
@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)
|
||||
|
||||
env["LDFLAGS"] += " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)])
|
||||
def get_clang_exe(self, with_target=False, plus_plus=False):
|
||||
"""Returns the full path of the clang/clang++ compiler, supports two
|
||||
kwargs:
|
||||
|
||||
if self.ctx.ndk == 'crystax':
|
||||
env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch)
|
||||
- `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)
|
||||
|
||||
toolchain_prefix = self.ctx.toolchain_prefix
|
||||
toolchain_version = self.ctx.toolchain_version
|
||||
command_prefix = self.command_prefix
|
||||
def get_env(self, with_flags_in_cc=True):
|
||||
env = {}
|
||||
|
||||
env['TOOLCHAIN_PREFIX'] = toolchain_prefix
|
||||
env['TOOLCHAIN_VERSION'] = toolchain_version
|
||||
# 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']
|
||||
|
||||
# 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']
|
||||
|
||||
# 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'"
|
||||
+ 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)
|
||||
)
|
||||
)
|
||||
|
||||
# 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
|
||||
|
|
|
@ -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,6 +127,24 @@ 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
|
||||
|
@ -144,5 +158,8 @@ def _set_user_options():
|
|||
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()
|
||||
|
|
255
p4a/pythonforandroid/bootstrap.py
Normal file → Executable file
255
p4a/pythonforandroid/bootstrap.py
Normal file → Executable 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):
|
||||
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 '
|
||||
raise BuildInterruptingException(
|
||||
'Internal error: tried to access {}.dist_dir, but {}.distribution '
|
||||
'is None'.format(self, self))
|
||||
exit(1)
|
||||
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
|
||||
|
|
|
@ -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,17 +235,13 @@ 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:
|
||||
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")
|
||||
|
@ -304,31 +249,141 @@ main.py that loads it.''')
|
|||
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
|
||||
|
||||
# Package up the private data (public not supported).
|
||||
tar_dirs = [env_vars_tarpath]
|
||||
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:
|
||||
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 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":
|
||||
tar_dirs.append('webview_includes')
|
||||
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(assets_dir, 'private.mp3'), tar_dirs, args.ignore_path,
|
||||
optimize_python=args.optimize_python)
|
||||
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":
|
||||
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')
|
||||
|
@ -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))
|
||||
try:
|
||||
subprocess.check_output([
|
||||
# -N: insist this is FORWARd patch, don't reverse apply
|
||||
|
||||
# -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
|
||||
])
|
||||
patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path]
|
||||
|
||||
try:
|
||||
# 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,10 +921,12 @@ 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'])
|
||||
|
@ -763,10 +935,6 @@ tools directory of the Android SDK.
|
|||
else:
|
||||
PYTHON = python_executable
|
||||
|
||||
if args.no_compile_pyo:
|
||||
PYTHON = None
|
||||
BLACKLIST_PATTERNS.remove('*.py')
|
||||
|
||||
if args.blacklist:
|
||||
with open(args.blacklist) as fd:
|
||||
patterns = [x.strip() for x in fd.read().splitlines()
|
||||
|
@ -791,4 +959,4 @@ tools directory of the Android SDK.
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parse_args()
|
||||
parse_args_and_make_package()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,7 +62,12 @@ 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;
|
||||
|
@ -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,8 +162,11 @@ public class PythonService extends Service implements Runnable {
|
|||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
//sticky servcie runtime/restart is managed by the OS. leave it running when app is closed
|
||||
if (startType() != START_STICKY) {
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(){
|
||||
|
|
|
@ -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 {
|
||||
|
@ -74,4 +86,173 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,13 +13,20 @@ allprojects {
|
|||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
{%- 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 }}
|
||||
|
@ -29,8 +36,23 @@ android {
|
|||
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,6 +62,15 @@ android {
|
|||
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
|
||||
}
|
||||
}
|
||||
|
||||
{%- endif %}
|
||||
|
||||
{% if args.packaging_options -%}
|
||||
packagingOptions {
|
||||
{%- for option in args.packaging_options %}
|
||||
{{option}}
|
||||
{%- endfor %}
|
||||
}
|
||||
{%- endif %}
|
||||
|
||||
buildTypes {
|
||||
|
@ -53,28 +84,51 @@ android {
|
|||
}
|
||||
|
||||
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')
|
||||
implementation(name: '{{ aar }}', ext: 'aar')
|
||||
{%- endfor -%}
|
||||
{%- for jar in jars %}
|
||||
compile files('src/main/libs/{{ jar }}')
|
||||
implementation files('src/main/libs/{{ jar }}')
|
||||
{%- endfor -%}
|
||||
{%- if args.depends -%}
|
||||
{%- for depend in args.depends %}
|
||||
compile '{{ depend }}'
|
||||
implementation '{{ depend }}'
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{% if args.presplash_lottie %}
|
||||
implementation 'com.airbnb.android:lottie:3.4.0'
|
||||
{%- endif %}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{% if args.enable_androidx %}
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
{% endif %}
|
|
@ -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>
|
||||
|
16
p4a/pythonforandroid/bootstraps/empty/__init__.py
Normal file
16
p4a/pythonforandroid/bootstraps/empty/__init__.py
Normal 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()
|
1
p4a/pythonforandroid/bootstraps/empty/build/.gitkeep
Normal file
1
p4a/pythonforandroid/bootstraps/empty/build/.gitkeep
Normal file
|
@ -0,0 +1 @@
|
|||
|
|
@ -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))
|
||||
|
|
|
@ -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):
|
||||
|
|
52
p4a/pythonforandroid/bootstraps/sdl2/__init__.py
Normal file
52
p4a/pythonforandroid/bootstraps/sdl2/__init__.py
Normal 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()
|
14
p4a/pythonforandroid/bootstraps/sdl2/build/.gitignore
vendored
Normal file
14
p4a/pythonforandroid/bootstraps/sdl2/build/.gitignore
vendored
Normal 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
|
84
p4a/pythonforandroid/bootstraps/sdl2/build/blacklist.txt
Normal file
84
p4a/pythonforandroid/bootstraps/sdl2/build/blacklist.txt
Normal 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
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
#define BOOTSTRAP_NAME_SDL2
|
||||
|
||||
const char bootstrap_name[] = "SDL2"; // capitalized for historic reasons
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 */;
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
from pythonforandroid.bootstraps.service_only import ServiceOnlyBootstrap
|
||||
|
||||
|
||||
class ServiceLibraryBootstrap(ServiceOnlyBootstrap):
|
||||
|
||||
name = 'service_library'
|
||||
|
||||
|
||||
bootstrap = ServiceLibraryBootstrap()
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
#define BOOTSTRAP_NAME_LIBRARY
|
||||
#define BOOTSTRAP_USES_NO_SDL_HEADERS
|
||||
|
||||
const char bootstrap_name[] = "service_library";
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue