Test 112 deps #17
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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master, test-112-deps]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_arm64_aar:
|
build_arm64_aar:
|
||||||
runs-on: ubuntu-latest
|
runs-on: self-hosted
|
||||||
container: lbry/android-base:python39
|
container: lbry/android-base:python39
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@
|
||||||
.gradle
|
.gradle
|
||||||
app/node_modules/
|
app/node_modules/
|
||||||
bin
|
bin
|
||||||
|
venv
|
||||||
buildozer.spec
|
buildozer.spec
|
||||||
build.log
|
build.log
|
||||||
recipes/**/*.pyc
|
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
|
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
|
#### Create buildozer.spec
|
||||||
Assuming `lbry-android` as the current working folder:
|
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.
|
* 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
|
# (list) Application requirements
|
||||||
# comma seperated e.g. requirements = sqlite3,kivy
|
# 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
|
# (str) Custom source folders for requirements
|
||||||
# Sets custom source for any requirements with recipes
|
# Sets custom source for any requirements with recipes
|
||||||
|
|
|
@ -39,7 +39,7 @@ version.filename = %(source.dir)s/main.py
|
||||||
|
|
||||||
# (list) Application requirements
|
# (list) Application requirements
|
||||||
# comma seperated e.g. requirements = sqlite3,kivy
|
# 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
|
# (str) Custom source folders for requirements
|
||||||
# Sets custom source for any requirements with recipes
|
# Sets custom source for any requirements with recipes
|
||||||
|
|
|
@ -39,7 +39,7 @@ version.filename = %(source.dir)s/main.py
|
||||||
|
|
||||||
# (list) Application requirements
|
# (list) Application requirements
|
||||||
# comma seperated e.g. requirements = sqlite3,kivy
|
# 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
|
# (str) Custom source folders for requirements
|
||||||
# Sets custom source for any requirements with recipes
|
# Sets custom source for any requirements with recipes
|
||||||
|
|
|
@ -39,7 +39,7 @@ version.filename = %(source.dir)s/main.py
|
||||||
|
|
||||||
# (list) Application requirements
|
# (list) Application requirements
|
||||||
# comma seperated e.g. requirements = sqlite3,kivy
|
# 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
|
# (str) Custom source folders for requirements
|
||||||
# Sets custom source for any requirements with recipes
|
# 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:
|
# Then, invoke the command line with the "demo" profile:
|
||||||
#
|
#
|
||||||
#buildozer --profile demo android debug
|
#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 LICENSE README.md
|
||||||
|
include *.toml
|
||||||
|
|
||||||
recursive-include doc *
|
recursive-include doc *
|
||||||
prune doc/build
|
prune doc/build
|
||||||
|
|
||||||
recursive-include pythonforandroid *.py *.tmpl biglink liblink
|
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 .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)
|
166
p4a/README.md
166
p4a/README.md
|
@ -1,96 +1,144 @@
|
||||||
# python-for-android
|
python-for-android
|
||||||
|
==================
|
||||||
|
|
||||||
python-for-android is a packager for Python apps on Android. You can
|
[![Unit tests & build apps](https://github.com/kivy/python-for-android/workflows/Unit%20tests%20&%20build%20apps/badge.svg?branch=develop)](https://github.com/kivy/python-for-android/actions?query=workflow%3A%22Unit+tests+%26+build+apps%22)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/github/kivy/python-for-android/badge.svg?branch=develop&kill_cache=1)](https://coveralls.io/github/kivy/python-for-android?branch=develop)
|
||||||
|
[![Backers on Open Collective](https://opencollective.com/kivy/backers/badge.svg)](#backers)
|
||||||
|
[![Sponsors on Open Collective](https://opencollective.com/kivy/sponsors/badge.svg)](#sponsors)
|
||||||
|
|
||||||
|
python-for-android is a packaging tool for Python apps on Android. You can
|
||||||
create your own Python distribution including the modules and
|
create your own Python distribution including the modules and
|
||||||
dependencies you want and bundle it in an APK along with your own
|
dependencies you want, and bundle it in an APK or AAB along with your own code.
|
||||||
code.
|
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
|
|
||||||
- Support for building with both Python 2 and Python 3.
|
- Different app backends including Kivy, PySDL2, and a WebView with
|
||||||
- Different app backends including Kivy, PySDL2, and a WebView with
|
Python webserver.
|
||||||
Python webserver.
|
- Automatic support for most pure Python modules, and built in support
|
||||||
- Automatic support for most pure Python modules, and built in support
|
for many others, including popular dependencies such as numpy and
|
||||||
for many others, including popular dependencies such as numpy and
|
sqlalchemy.
|
||||||
sqlalchemy.
|
- Multiple architecture targets, for APKs optimised on any given
|
||||||
- Multiple architecture targets, for APKs optimized on any given device.
|
device.
|
||||||
|
- AAB: Android App Bundle support.
|
||||||
|
|
||||||
For documentation and support, see:
|
For documentation and support, see:
|
||||||
|
|
||||||
- Website: http://python-for-android.readthedocs.io
|
- Website: http://python-for-android.readthedocs.io
|
||||||
- Mailing list: https://groups.google.com/forum/#!forum/kivy-users or
|
- Mailing list: https://groups.google.com/forum/#!forum/kivy-users or
|
||||||
https://groups.google.com/forum/#!forum/python-android.
|
https://groups.google.com/forum/#!forum/python-android.
|
||||||
|
|
||||||
In 2015, these tools were rewritten to provide a newer, easier to use and
|
## Documentation
|
||||||
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
|
Follow the [quickstart
|
||||||
|
instructions](<https://python-for-android.readthedocs.org/en/latest/quickstart/>)
|
||||||
|
to install and begin creating APKs and AABs.
|
||||||
|
|
||||||
Follow the
|
**Quick instructions**: install python-for-android with:
|
||||||
[quickstart instructions](https://python-for-android.readthedocs.org/en/latest/quickstart/)
|
|
||||||
to install and begin creating APKs.
|
|
||||||
|
|
||||||
Quick instructions to start would be::
|
|
||||||
|
|
||||||
pip install python-for-android
|
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
|
p4a --version
|
||||||
equivalent). To test that the installation worked, try
|
|
||||||
|
|
||||||
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
|
p4a apk --requirements=kivy --private /home/username/devel/planewave_frozen/ --package=net.inclem.planewavessdl2 --name="planewavessdl2" --version=0.5 --bootstrap=sdl2
|
||||||
as described in the documentation linked above.
|
|
||||||
|
|
||||||
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:
|
If you need assistance, you can ask for help on our mailing list:
|
||||||
|
|
||||||
* User Group: https://groups.google.com/group/kivy-users
|
- User Group: https://groups.google.com/group/kivy-users
|
||||||
* Email: kivy-users@googlegroups.com
|
- 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
|
## Contributing
|
||||||
* Port: 6667, 6697 (SSL only)
|
|
||||||
* Channel: #kivy
|
|
||||||
|
|
||||||
# Contributing
|
We love pull requests and discussing novel ideas. Check out the Kivy
|
||||||
|
project [contribution guide](https://kivy.org/doc/stable/contribute.html) and
|
||||||
We love pull requests and discussing novel ideas. Check out our
|
|
||||||
[contribution guide](http://kivy.org/docs/contribute.html) and
|
|
||||||
feel free to improve python-for-android.
|
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
|
The following mailing list and IRC channel are used exclusively for
|
||||||
discussions about developing the Kivy framework and its sister projects:
|
discussions about developing the Kivy framework and its sister projects:
|
||||||
|
|
||||||
* Dev Group: https://groups.google.com/group/kivy-dev
|
- Dev Group: https://groups.google.com/group/kivy-dev
|
||||||
* Email: kivy-dev@googlegroups.com
|
- Email: kivy-dev@googlegroups.com
|
||||||
|
|
||||||
IRC channel:
|
We also have [#dev Discord channel](https://chat.kivy.org/).
|
||||||
|
|
||||||
* Server: irc.freenode.net
|
## License
|
||||||
* Port: 6667, 6697 (SSL only)
|
|
||||||
* Channel: #kivy or #kivy-dev
|
|
||||||
|
|
||||||
# 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
|
## History
|
||||||
LICENSE file.
|
|
||||||
|
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
|
This page gives details on accessing Android APIs and managing other
|
||||||
interactions on Android.
|
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
|
Instead, the `android` module which you can add to your `--requirements`
|
||||||
Android Java APIs, in order to control your application's appearance
|
allows you to query the most commonly required paths::
|
||||||
(fullscreen, orientation etc.), interact with other apps or use
|
|
||||||
hardware like vibration and sensors.
|
|
||||||
|
|
||||||
You can access these with `Pyjnius
|
from android.storage import app_storage_path
|
||||||
<http://pyjnius.readthedocs.org/en/latest/>`_, a Python library for
|
settings_path = app_storage_path()
|
||||||
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.
|
|
||||||
|
|
||||||
Pyjnius and Plyer are independent projects whose documentation is
|
from android.storage import primary_external_storage_path
|
||||||
linked above. See below for some simple introductory examples, and
|
primary_ext_storage = primary_external_storage_path()
|
||||||
explanation of how to include these modules in your APKs.
|
|
||||||
|
|
||||||
This page also documents the ``android`` module which you can include
|
from android.storage import secondary_external_storage_path
|
||||||
with p4a, but this is mostly replaced by Pyjnius and is not
|
secondary_ext_storage = secondary_external_storage_path()
|
||||||
recommended for use in new applications.
|
|
||||||
|
`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
|
Other common tasks
|
||||||
~~~~~~~~~~~~~
|
------------------
|
||||||
|
|
||||||
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
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
Dismissing the splash screen
|
Dismissing the splash screen
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
With the SDL2 bootstrap, the app's splash screen may not be dismissed
|
With the SDL2 bootstrap, the app's splash screen may be visible
|
||||||
immediately when your app has finished loading, due to a limitation
|
longer than necessary (with your app already being loaded) due to a
|
||||||
with the way we check if the app has properly started. In this case,
|
limitation with the way we check if the app has properly started.
|
||||||
the splash screen overlaps the app gui for a short time.
|
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
|
To dismiss the loading screen explicitly in your code, use the `android`
|
||||||
app build method (or use ``kivy.clock.Clock.schedule_once`` to run it
|
module::
|
||||||
in the following frame)::
|
|
||||||
|
|
||||||
from jnius import autoclass
|
from android import loadingscreen
|
||||||
activity = autoclass('org.kivy.android.PythonActivity').mActivity
|
loadingscreen.hide_loading_screen()
|
||||||
activity.removeLoadingScreen()
|
|
||||||
|
|
||||||
This problem does not affect the Pygame bootstrap, as it uses a
|
You can call it e.g. using ``kivy.clock.Clock.schedule_once`` to run it
|
||||||
different splash screen method.
|
in the first active frame of your app, or use the app build method.
|
||||||
|
|
||||||
|
|
||||||
Handling the back button
|
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.
|
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.).
|
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
|
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>`.
|
:ref:`build options <bootstrap_build_options>`.
|
||||||
|
|
||||||
python-for-android (p4a) supports multiple *bootstraps*. These fulfill a
|
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
|
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
|
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
|
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::
|
For instance, the SDL2 bootstrap looks like the following::
|
||||||
|
|
||||||
|
|
|
@ -8,44 +8,13 @@ This page contains instructions for using different build options.
|
||||||
Python versions
|
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
|
Python-for-android no longer supports building for Python 3 using the CrystaX
|
||||||
architecture. There are no special requirements, all the building is
|
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>`__
|
||||||
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>`__.
|
|
||||||
|
|
||||||
.. _bootstrap_build_options:
|
.. _bootstrap_build_options:
|
||||||
|
|
||||||
|
@ -65,7 +34,7 @@ sdl2
|
||||||
~~~~
|
~~~~
|
||||||
|
|
||||||
Use this with ``--bootstrap=sdl2``, or just include the
|
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
|
SDL2 is a popular cross-platform depelopment library, particularly for
|
||||||
games. It has its own Android project support, which
|
games. It has its own Android project support, which
|
||||||
|
@ -85,7 +54,71 @@ The sdl2 bootstrap supports the following additional command line
|
||||||
options (this list may not be exhaustive):
|
options (this list may not be exhaustive):
|
||||||
|
|
||||||
- ``--private``: The directory containing your project files.
|
- ``--private``: The directory containing your project files.
|
||||||
- ``--package``: The Java package name for your project. Choose e.g. ``org.example.yourapp``.
|
- ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``.
|
||||||
|
- ``--name``: The app name.
|
||||||
|
- ``--version``: The version number.
|
||||||
|
- ``--orientation``: Usually one of ``portait``, ``landscape``,
|
||||||
|
``sensor`` to automatically rotate according to the device
|
||||||
|
orientation, or ``user`` to do the same but obeying the user's
|
||||||
|
settings. The full list of valid options is given under
|
||||||
|
``android:screenOrientation`` in the `Android documentation
|
||||||
|
<https://developer.android.com/guide/topics/manifest/activity-element.html>`__.
|
||||||
|
- ``--icon``: A path to the png file to use as the application icon.
|
||||||
|
- ``--permission``: A permission name for the app,
|
||||||
|
e.g. ``--permission VIBRATE``. For multiple permissions, add
|
||||||
|
multiple ``--permission`` arguments.
|
||||||
|
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
|
||||||
|
- ``--presplash``: A path to the image file to use as a screen while
|
||||||
|
the application is loading.
|
||||||
|
- ``--presplash-color``: The presplash screen background color, of the
|
||||||
|
form ``#RRGGBB`` or a color name ``red``, ``green``, ``blue`` etc.
|
||||||
|
- ``--presplash-lottie``: use a lottie (json) file as a presplash animation. If
|
||||||
|
used, this will replace the static presplash image.
|
||||||
|
- ``--wakelock``: If the argument is included, the application will
|
||||||
|
prevent the device from sleeping.
|
||||||
|
- ``--window``: If the argument is included, the application will not
|
||||||
|
cover the Android status bar.
|
||||||
|
- ``--blacklist``: The path to a file containing blacklisted patterns
|
||||||
|
that will be excluded from the final APK. Defaults to ``./blacklist.txt``.
|
||||||
|
- ``--whitelist``: The path to a file containing whitelisted patterns
|
||||||
|
that will be included in the APK even if also blacklisted.
|
||||||
|
- ``--add-jar``: The path to a .jar file to include in the APK. To
|
||||||
|
include multiple jar files, pass this argument multiple times.
|
||||||
|
- ``--intent-filters``: A file path containing intent filter xml to be
|
||||||
|
included in AndroidManifest.xml.
|
||||||
|
- ``--service``: A service name and the Python script it should
|
||||||
|
run. See :ref:`arbitrary_scripts_services`.
|
||||||
|
- ``--add-source``: Add a source directory to the app's Java code.
|
||||||
|
- ``--no-byte-compile-python``: Skip byte compile for .py files.
|
||||||
|
- ``--enable-androidx``: Enable AndroidX support library.
|
||||||
|
- ``--add-resource``: Put this file or directory in the apk res directory.
|
||||||
|
|
||||||
|
|
||||||
|
webview
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
You can use this with ``--bootstrap=webview``, or include the
|
||||||
|
``webviewjni`` recipe, e.g. ``--requirements=webviewjni,python3``.
|
||||||
|
|
||||||
|
The webview bootstrap gui is, per the name, a WebView displaying a
|
||||||
|
webpage, but this page is hosted on the device via a Python
|
||||||
|
webserver. For instance, your Python code can start a Flask
|
||||||
|
application, and your app will display and allow the user to navigate
|
||||||
|
this website.
|
||||||
|
|
||||||
|
.. note:: Your Flask script must start the webserver *without*
|
||||||
|
:code:``debug=True``. Debug mode doesn't seem to work on
|
||||||
|
Android due to use of a subprocess.
|
||||||
|
|
||||||
|
This bootstrap will automatically try to load a website on port 5000
|
||||||
|
(the default for Flask), or you can specify a different option with
|
||||||
|
the `--port` command line option. If the webserver is not immediately
|
||||||
|
present (e.g. during the short Python loading time when first
|
||||||
|
started), it will instead display a loading screen until the server is
|
||||||
|
ready.
|
||||||
|
|
||||||
|
- ``--private``: The directory containing your project files.
|
||||||
|
- ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``.
|
||||||
- ``--name``: The app name.
|
- ``--name``: The app name.
|
||||||
- ``--version``: The version number.
|
- ``--version``: The version number.
|
||||||
- ``--orientation``: Usually one of ``portait``, ``landscape``,
|
- ``--orientation``: Usually one of ``portait``, ``landscape``,
|
||||||
|
@ -99,66 +132,6 @@ options (this list may not be exhaustive):
|
||||||
e.g. ``--permission VIBRATE``. For multiple permissions, add
|
e.g. ``--permission VIBRATE``. For multiple permissions, add
|
||||||
multiple ``--permission`` arguments.
|
multiple ``--permission`` arguments.
|
||||||
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
|
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
|
||||||
- ``--presplash``: A path to the image file to use as a screen while
|
|
||||||
the application is loading.
|
|
||||||
- ``--presplash-color``: The presplash screen background color, of the
|
|
||||||
form ``#RRGGBB`` or a color name ``red``, ``green``, ``blue`` etc.
|
|
||||||
- ``--wakelock``: If the argument is included, the application will
|
|
||||||
prevent the device from sleeping.
|
|
||||||
- ``--window``: If the argument is included, the application will not
|
|
||||||
cover the Android status bar.
|
|
||||||
- ``--blacklist``: The path to a file containing blacklisted patterns
|
|
||||||
that will be excluded from the final APK. Defaults to ``./blacklist.txt``.
|
|
||||||
- ``--whitelist``: The path to a file containing whitelisted patterns
|
|
||||||
that will be included in the APK even if also blacklisted.
|
|
||||||
- ``--add-jar``: The path to a .jar file to include in the APK. To
|
|
||||||
include multiple jar files, pass this argument multiple times.
|
|
||||||
- ``--intent-filters``: A file path containing intent filter xml to be
|
|
||||||
included in AndroidManifest.xml.
|
|
||||||
- ``--service``: A service name and the Python script it should
|
|
||||||
run. See :ref:`arbitrary_scripts_services`.
|
|
||||||
- ``--add-source``: Add a source directory to the app's Java code.
|
|
||||||
- ``--no-compile-pyo``: Do not optimise .py files to .pyo.
|
|
||||||
|
|
||||||
|
|
||||||
webview
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
You can use this with ``--bootstrap=webview``, or include the
|
|
||||||
``webviewjni`` recipe, e.g. ``--requirements=webviewjni,python2``.
|
|
||||||
|
|
||||||
The webview bootstrap gui is, per the name, a WebView displaying a
|
|
||||||
webpage, but this page is hosted on the device via a Python
|
|
||||||
webserver. For instance, your Python code can start a Flask
|
|
||||||
application, and your app will display and allow the user to navigate
|
|
||||||
this website.
|
|
||||||
|
|
||||||
.. note:: Your Flask script must start the webserver *without*
|
|
||||||
:code:``debug=True``. Debug mode doesn't seem to work on
|
|
||||||
Android due to use of a subprocess.
|
|
||||||
|
|
||||||
This bootstrap will automatically try to load a website on port 5000
|
|
||||||
(the default for Flask), or you can specify a different option with
|
|
||||||
the `--port` command line option. If the webserver is not immediately
|
|
||||||
present (e.g. during the short Python loading time when first
|
|
||||||
started), it will instead display a loading screen until the server is
|
|
||||||
ready.
|
|
||||||
|
|
||||||
- ``--private``: The directory containing your project files.
|
|
||||||
- ``--package``: The Java package name for your project. Choose e.g. ``org.example.yourapp``.
|
|
||||||
- ``--name``: The app name.
|
|
||||||
- ``--version``: The version number.
|
|
||||||
- ``--orientation``: Usually one of ``portait``, ``landscape``,
|
|
||||||
``sensor`` to automatically rotate according to the device
|
|
||||||
orientation, or ``user`` to do the same but obeying the user's
|
|
||||||
settings. The full list of valid options is given under
|
|
||||||
``android:screenOrientation`` in the `Android documentation
|
|
||||||
<https://developer.android.com/guide/topics/manifest/activity-element.html>`__.
|
|
||||||
- ``--icon``: A path to the png file to use as the application icon.
|
|
||||||
- ``-- permission``: A permission name for the app,
|
|
||||||
e.g. ``--permission VIBRATE``. For multiple permissions, add
|
|
||||||
multiple ``--permission`` arguments.
|
|
||||||
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
|
|
||||||
- ``--presplash``: A path to the image file to use as a screen while
|
- ``--presplash``: A path to the image file to use as a screen while
|
||||||
the application is loading.
|
the application is loading.
|
||||||
- ``--presplash-color``: The presplash screen background color, of the
|
- ``--presplash-color``: The presplash screen background color, of the
|
||||||
|
@ -182,58 +155,48 @@ ready.
|
||||||
access. Defaults to 5000.
|
access. Defaults to 5000.
|
||||||
|
|
||||||
|
|
||||||
pygame
|
service_library
|
||||||
~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can use this with ``--bootstrap=pygame``, or simply include the
|
You can use this with ``--bootstrap=service_library`` option.
|
||||||
``pygame`` recipe in your ``--requirements``.
|
|
||||||
|
|
||||||
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
|
This bootstrap can be used together with ``aar`` output target to generate
|
||||||
not before the sdl2 bootstrap includes all the features that would be
|
a library, containing Python services that can be used with other build
|
||||||
lost.
|
systems and frameworks.
|
||||||
|
|
||||||
Build options
|
|
||||||
%%%%%%%%%%%%%
|
|
||||||
|
|
||||||
The pygame bootstrap supports the following additional command line
|
|
||||||
options (this list may not be exhaustive):
|
|
||||||
|
|
||||||
- ``--private``: The directory containing your project files.
|
- ``--private``: The directory containing your project files.
|
||||||
- ``--dir``: The directory containing your project files if you want
|
- ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``.
|
||||||
them to be unpacked to the external storage directory rather than
|
- ``--name``: The library name.
|
||||||
the app private directory.
|
|
||||||
- ``--package``: The Java package name for your project. Choose e.g. ``org.example.yourapp``.
|
|
||||||
- ``--name``: The app name.
|
|
||||||
- ``--version``: The version number.
|
- ``--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
|
- ``--service``: A service name and the Python script it should
|
||||||
run. See :ref:`arbitrary_scripts_services`.
|
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.
|
- ``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
|
distribution must contain, as a comma separated list. These must be
|
||||||
names of recipes or the pypi names of Python modules.
|
names of recipes or the pypi names of Python modules.
|
||||||
|
|
||||||
``--force_build BOOL``
|
``--force-build BOOL``
|
||||||
Whether the distribution must be compiled from scratch.
|
Whether the distribution must be compiled from scratch.
|
||||||
|
|
||||||
``--arch``
|
``--arch``
|
||||||
The architecture to build for. Currently only one architecture can be
|
The architecture to build for. You can specify multiple architectures to build for
|
||||||
targeted at a time, and a given distribution can only include one architecture.
|
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``
|
``--bootstrap BOOTSTRAP``
|
||||||
The Java bootstrap to use for your application. You mostly don't
|
The Java bootstrap to use for your application. You mostly don't
|
||||||
need to worry about this or set it manually, as an appropriate
|
need to worry about this or set it manually, as an appropriate
|
||||||
bootstrap will be chosen from your ``--requirements``. Current
|
bootstrap will be chosen from your ``--requirements``. Current
|
||||||
choices are ``sdl2`` or ``pygame``; ``sdl2`` is experimental but
|
choices are ``sdl2`` (used with Kivy and most other apps) or ``webview``.
|
||||||
preferable where possible.
|
|
||||||
|
|
||||||
|
|
||||||
.. note:: These options are preliminary. Others will include toggles
|
.. 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
|
The development of python-for-android is managed by the Kivy team `via
|
||||||
Github <https://github.com/kivy/python-for-android>`_.
|
Github <https://github.com/kivy/python-for-android>`_.
|
||||||
|
|
||||||
Issues and pull requests are welcome via the integrated `issue tracker
|
Issues and pull requests are welcome via the integrated `issue tracker
|
||||||
<https://github.com/kivy/python-for-android/issues>`_.
|
<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
|
distutils/setuptools integration
|
||||||
================================
|
================================
|
||||||
|
|
||||||
Instead of running p4a via the command line, you can integrate with
|
Have `p4a apk` run setup.py (replaces ``--requirements``)
|
||||||
distutils and setup.py.
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
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::
|
The base command is::
|
||||||
|
|
||||||
|
@ -35,7 +95,7 @@ The Android package name uses ``org.test.lowercaseappname``
|
||||||
if not set explicitly.
|
if not set explicitly.
|
||||||
|
|
||||||
The ``--private`` argument is set automatically using the
|
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``.
|
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
|
python setup.py apk --name="Testapp Setup" --version=2.5
|
||||||
|
|
||||||
Adding p4a arguments in setup.py
|
Adding p4a arguments in setup.py
|
||||||
--------------------------------
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Instead of providing extra arguments on the command line, you can
|
Instead of providing extra arguments on the command line, you can
|
||||||
store them in setup.py by passing the ``options`` parameter to
|
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
|
from setuptools import find_packages
|
||||||
|
|
||||||
options = {'apk': {'debug': None, # use None for arguments that don't pass a value
|
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,
|
'android-api': 19,
|
||||||
'ndk-dir': '/path/to/ndk',
|
'ndk-dir': '/path/to/ndk',
|
||||||
'dist-name': 'bdisttest',
|
'dist-name': 'bdisttest',
|
||||||
|
@ -79,7 +139,7 @@ setup.py apk``. Any options passed on the command line will override
|
||||||
these values.
|
these values.
|
||||||
|
|
||||||
Adding p4a arguments in setup.cfg
|
Adding p4a arguments in setup.cfg
|
||||||
---------------------------------
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can also provide p4a arguments in the setup.cfg file, as normal
|
You can also provide p4a arguments in the setup.cfg file, as normal
|
||||||
for distutils. The syntax is::
|
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,
|
"private": true,
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"grunt": "~0.4.1",
|
"grunt": ">=1.3.0",
|
||||||
"grunt-contrib-sass": "~0.7.2",
|
"grunt-contrib-sass": "~0.7.2",
|
||||||
"grunt-contrib-watch": "~0.4.3",
|
"grunt-contrib-watch": "~0.4.3",
|
||||||
"grunt-contrib-connect": "0.5.0",
|
"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-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
|
installed, or uploaded to marketplaces such as the Play Store just
|
||||||
like any other Android app. This tool was originally developed for the
|
like any other Android app. This tool was originally developed for the
|
||||||
`Kivy cross-platform graphical framework <http://kivy.org/#home>`_,
|
`Kivy cross-platform graphical framework <http://kivy.org/#home>`_,
|
||||||
but now supports multiple bootstraps and can be easily extended to
|
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
|
python-for-android supports two major operations; first, it can
|
||||||
compile the Python interpreter, its dependencies, backend libraries
|
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
|
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
|
a standalone Android project which can be used to generate any number
|
||||||
of different APKs, even with different names, icons, Python code etc.
|
of different APKs, even with different names, icons, Python code etc.
|
||||||
|
@ -29,15 +29,16 @@ Contents
|
||||||
quickstart
|
quickstart
|
||||||
buildoptions
|
buildoptions
|
||||||
commands
|
commands
|
||||||
|
apis
|
||||||
|
launcher
|
||||||
distutils
|
distutils
|
||||||
recipes
|
recipes
|
||||||
bootstraps
|
bootstraps
|
||||||
services
|
services
|
||||||
apis
|
|
||||||
troubleshooting
|
troubleshooting
|
||||||
launcher
|
docker
|
||||||
contribute
|
contribute
|
||||||
old_toolchain/index.rst
|
testing_pull_requests
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|
|
@ -4,7 +4,7 @@ Launcher
|
||||||
========
|
========
|
||||||
|
|
||||||
The Kivy Launcher is an Android application that can run any Kivy app
|
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
|
version for your android device from the
|
||||||
`Play Store <https://play.google.com/store/apps/details?id=org.kivy.pygame>`_.
|
`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
|
aren't always enough for an application to run or even launch if you
|
||||||
work with other dependencies that are not packaged.
|
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
|
anything more advanced we recommend building your own APK with
|
||||||
python-for-android.
|
python-for-android.
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ Building
|
||||||
|
|
||||||
The Kivy Launcher is built using python-for-android. To get the most recent
|
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
|
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
|
.. highlight:: none
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ grab an old (cached) package instead of fresh one.
|
||||||
--name="App name" \
|
--name="App name" \
|
||||||
--version=x.y.z \
|
--version=x.y.z \
|
||||||
--android_api XY \
|
--android_api XY \
|
||||||
--bootstrap=pygame or sdl2 \
|
--bootstrap=sdl2 \
|
||||||
--launcher \
|
--launcher \
|
||||||
--minsdk 13
|
--minsdk 13
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ grab an old (cached) package instead of fresh one.
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
Do not use any of `--private`, `--public`, `--dir` or other arguments for
|
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.
|
above them and tells the p4a to build the launcher version of the APK.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
|
@ -78,8 +78,8 @@ to change other settings.
|
||||||
After you set your `android.txt` file, you can now run the launcher
|
After you set your `android.txt` file, you can now run the launcher
|
||||||
and start any available app from the list.
|
and start any available app from the list.
|
||||||
|
|
||||||
To differentiate between apps in ``/sdcard/kivy`` you can include an icon
|
To differentiate between apps in ``/sdcard/kivy``, you can include an icon
|
||||||
named ``icon.png`` to the folder. The icon should be a square.
|
named ``icon.png`` in the folder. The icon should be a square.
|
||||||
|
|
||||||
Release on the market
|
Release on the market
|
||||||
---------------------
|
---------------------
|
||||||
|
@ -91,12 +91,8 @@ it changes quickly and needs testing.
|
||||||
Source code
|
Source code
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
.. |renpy| replace:: pygame org.renpy.android
|
|
||||||
.. |kivy| replace:: sdl2 org.kivy.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:
|
.. _sdl2:
|
||||||
https://github.com/kivy/python-for-android/tree/master/\
|
https://github.com/kivy/python-for-android/tree/master/\
|
||||||
pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android
|
pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android
|
||||||
|
|
|
@ -9,27 +9,37 @@ for android as p4a in this documentation.
|
||||||
Concepts
|
Concepts
|
||||||
--------
|
--------
|
||||||
|
|
||||||
- requirements: For p4a, your applications dependencies are
|
*Basic:*
|
||||||
requirements similar to the standard `requirements.txt`, but with
|
|
||||||
one difference: p4a will search for a recipe first instead of
|
|
||||||
installing requirements with pip.
|
|
||||||
|
|
||||||
- recipe: A recipe is a file that defines how to compile a
|
- **requirements:** For p4a, all your app's dependencies must be specified
|
||||||
requirement. Any libraries that have a Python extension *must* have
|
via ``--requirements`` similar to the standard `requirements.txt`.
|
||||||
a recipe in p4a, or compilation will fail. If there is no recipe for
|
(Unless you specify them via a `setup.py`/`install_requires`)
|
||||||
a requirement, it will be downloaded using pip.
|
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
|
- **build:** A build refers to a compiled recipe or distribution.
|
||||||
compiled requirements, as an Android project that can be turned
|
|
||||||
directly into an APK. p4a can contain multiple distributions with
|
|
||||||
different sets of requirements.
|
|
||||||
|
|
||||||
- bootstrap: A bootstrap is the app backend that will start your
|
- **bootstrap:** A bootstrap is the app backend that will start your
|
||||||
application. Your application could use SDL2 as a base, or Pygame,
|
application. The default for graphical applications is SDL2.
|
||||||
or a web backend like Flask with a WebView bootstrap. Different
|
You can also use e.g. the webview for web apps, or service_only/service_library for
|
||||||
bootstraps can have different build options.
|
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
|
Installation
|
||||||
|
@ -38,7 +48,7 @@ Installation
|
||||||
Installing p4a
|
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
|
pip install python-for-android
|
||||||
|
|
||||||
|
@ -51,37 +61,54 @@ Installing Dependencies
|
||||||
|
|
||||||
p4a has several dependencies that must be installed:
|
p4a has several dependencies that must be installed:
|
||||||
|
|
||||||
- git
|
|
||||||
- ant
|
- 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)
|
- cython (can be installed via pip)
|
||||||
- a Java JDK (e.g. openjdk-7)
|
- gcc
|
||||||
- zlib (including 32 bit)
|
- git
|
||||||
- libncurses (including 32 bit)
|
- libncurses (including 32 bit)
|
||||||
|
- libtool (for libffi and recipes)
|
||||||
|
- libssl-dev (for TLS/SSL support on hostpython3 and recipe)
|
||||||
|
- openjdk-8
|
||||||
|
- patch
|
||||||
|
- python3
|
||||||
- unzip
|
- unzip
|
||||||
- virtualenv (can be installed via pip)
|
- virtualenv (can be installed via pip)
|
||||||
- ccache (optional)
|
- zlib (including 32 bit)
|
||||||
- autoconf (for ffpyplayer_codecs recipe)
|
- zip
|
||||||
- libtool (for ffpyplayer_codecs recipe)
|
|
||||||
|
|
||||||
On recent versions of Ubuntu and its derivatives you may be able to
|
On recent versions of Ubuntu and its derivatives you may be able to
|
||||||
install most of these with::
|
install most of these with::
|
||||||
|
|
||||||
sudo dpkg --add-architecture i386
|
sudo dpkg --add-architecture i386
|
||||||
sudo apt-get update
|
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
|
install most of the dependencies (note: this list may not be
|
||||||
complete). gcc-multilib will conflict with (and replace) gcc if not
|
complete)::
|
||||||
already installed. If your installation is already 32-bit, install the
|
|
||||||
same packages but without ``lib32-`` or ``-multilib``::
|
|
||||||
|
|
||||||
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
|
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/):
|
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>`_
|
- `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.
|
the SDK packages needed.
|
||||||
|
|
||||||
For Android NDK, note that modern releases will only work on a 64-bit
|
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),
|
operating system. **The minimal, and recommended, NDK version to use is r25b:**
|
||||||
the latest useable NDK version is r10e, which can be downloaded here:
|
|
||||||
|
|
||||||
- `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
|
Second, install the build-tools. You can use
|
||||||
``$SDK_DIR/tools/bin/sdkmanager --list`` to see all the
|
``$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!
|
# Adjust the paths!
|
||||||
export ANDROIDSDK="$HOME/Documents/android-sdk-21"
|
export ANDROIDSDK="$HOME/Documents/android-sdk-27"
|
||||||
export ANDROIDNDK="$HOME/Documents/android-ndk-r10e"
|
export ANDROIDNDK="$HOME/Documents/android-ndk-r23b"
|
||||||
export ANDROIDAPI="19" # Minimum API version your application require
|
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
|
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:
|
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:`--sdk-dir PATH` as an equivalent of `$ANDROIDSDK`
|
||||||
- :code:`--ndk_dir PATH` as an equivalent of `$ANDROIDNDK`
|
- :code:`--ndk-dir PATH` as an equivalent of `$ANDROIDNDK`
|
||||||
- :code:`--android_api VERSION` as an equivalent of `$ANDROIDAPI`
|
- :code:`--android-api VERSION` as an equivalent of `$ANDROIDAPI`
|
||||||
- :code:`--ndk_version PATH` as an equivalent of `$ANDROIDNDKVER`
|
- :code:`--ndk-api VERSION` as an equivalent of `$NDKAPI`
|
||||||
|
- :code:`--ndk-version VERSION` as an equivalent of `$ANDROIDNDKVER`
|
||||||
|
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|
||||||
Build a Kivy application
|
Build a Kivy or SDL2 application
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
To build your application, you need to have a name, version, a package
|
To build your application, you need to specify name, version, a package
|
||||||
identifier, and explicitly write the bootstrap you want to use, as
|
identifier, the bootstrap you want to use (`sdl2` for kivy or sdl2 apps)
|
||||||
well as the requirements::
|
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
|
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
|
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.
|
You can also replace flask with another web framework.
|
||||||
|
|
||||||
Replace ``--port=5000`` with the port on which your app will serve a
|
Replace ``--port=5000`` with the port on which your app will serve a
|
||||||
website. The default for Flask is 5000.
|
website. The default for Flask is 5000.
|
||||||
|
|
||||||
Build an SDL2 based application
|
|
||||||
|
Build a Service library archive
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
This includes e.g. `PySDL2
|
To build an android archive (.aar), containing an android service , you need a name, version, package identifier, explicitly use the
|
||||||
<https://pysdl2.readthedocs.io/en/latest/>`__.
|
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
|
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
|
||||||
identifier, and explicitly write the sdl2 bootstrap, as well as the
|
|
||||||
requirements::
|
|
||||||
|
|
||||||
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``,
|
You can then call the generated Java entrypoint(s) for your Python service(s) in other apk build frameworks.
|
||||||
e.g. ``--requirements=pysdl2`` or ``--requirements=vispy``.
|
|
||||||
|
|
||||||
|
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
|
Other options
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
@ -177,7 +251,7 @@ You can pass other command line arguments to control app behaviours
|
||||||
such as orientation, wakelock and app permissions. See
|
such as orientation, wakelock and app permissions. See
|
||||||
:ref:`bootstrap_build_options`.
|
:ref:`bootstrap_build_options`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Rebuild everything
|
Rebuild everything
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -185,18 +259,18 @@ Rebuild everything
|
||||||
If anything goes wrong and you want to clean the downloads and builds to retry everything, run::
|
If anything goes wrong and you want to clean the downloads and builds to retry everything, run::
|
||||||
|
|
||||||
p4a clean_all
|
p4a clean_all
|
||||||
|
|
||||||
If you just want to clean the builds to avoid redownloading dependencies, run::
|
If you just want to clean the builds to avoid redownloading dependencies, run::
|
||||||
|
|
||||||
p4a clean_builds && p4a clean_dists
|
p4a clean_builds && p4a clean_dists
|
||||||
|
|
||||||
Getting help
|
Getting help
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
If something goes wrong and you don't know how to fix it, add the
|
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
|
``--debug`` option and post the output log to the `kivy-users Google
|
||||||
group <https://groups.google.com/forum/#!forum/kivy-users>`__ or irc
|
group <https://groups.google.com/forum/#!forum/kivy-users>`__ or the
|
||||||
channel #kivy at irc.freenode.net .
|
kivy `#support Discord channel <https://chat.kivy.org/>`_.
|
||||||
|
|
||||||
See :doc:`troubleshooting` for more information.
|
See :doc:`troubleshooting` for more information.
|
||||||
|
|
||||||
|
@ -248,7 +322,7 @@ You can list the available distributions::
|
||||||
And clean all of them::
|
And clean all of them::
|
||||||
|
|
||||||
p4a clean_dists
|
p4a clean_dists
|
||||||
|
|
||||||
Configuration file
|
Configuration file
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -258,9 +332,28 @@ command line. For example, you can add the options you would always
|
||||||
include such as::
|
include such as::
|
||||||
|
|
||||||
--dist_name my_example
|
--dist_name my_example
|
||||||
--android_api 19
|
--android_api 27
|
||||||
--requirements kivy,openssl
|
--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
|
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.
|
be compiled for Android with the correct architecture.
|
||||||
|
|
||||||
python-for-android comes with many recipes for popular modules. No
|
python-for-android comes with many recipes for popular modules. No
|
||||||
recipe is necessary to use of Python modules with no
|
recipe is necessary for Python modules which have no
|
||||||
compiled components; these are installed automaticaly via pip.
|
compiled components; these are installed automatically via pip.
|
||||||
|
|
||||||
If you are new to building recipes, it is recommended that you first
|
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
|
read all of this page, at least up to the Recipe reference
|
||||||
documentation. The different recipe sections include a number of
|
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
|
patches = ['some_fix.patch'] # Paths relative to the recipe dir
|
||||||
|
|
||||||
depends = ['kivy', 'sdl2'] # These are just examples
|
depends = ['kivy', 'sdl2'] # These are just examples
|
||||||
conflicts = ['pygame']
|
conflicts = ['generickndkbuild']
|
||||||
|
|
||||||
recipe = YourRecipe()
|
recipe = YourRecipe()
|
||||||
|
|
||||||
|
@ -62,21 +61,21 @@ when the recipe is imported.
|
||||||
The actual build process takes place via three core methods::
|
The actual build process takes place via three core methods::
|
||||||
|
|
||||||
def prebuild_arch(self, arch):
|
def prebuild_arch(self, arch):
|
||||||
super(YourRecipe, self).prebuild_arch(arch)
|
super().prebuild_arch(arch)
|
||||||
# Do any pre-initialisation
|
# Do any pre-initialisation
|
||||||
|
|
||||||
def build_arch(self, arch):
|
def build_arch(self, arch):
|
||||||
super(YourRecipe, self).build_arch(arch)
|
super().build_arch(arch)
|
||||||
# Do the main recipe build
|
# Do the main recipe build
|
||||||
|
|
||||||
def postbuild_arch(self, arch):
|
def postbuild_arch(self, arch):
|
||||||
super(YourRecipe, self).build_arch(arch)
|
super().build_arch(arch)
|
||||||
# Do any clearing up
|
# Do any clearing up
|
||||||
|
|
||||||
These methods are always run in the listed order; prebuild, then
|
These methods are always run in the listed order; prebuild, then
|
||||||
build, then postbuild.
|
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.
|
download it, this is handled automatically.
|
||||||
|
|
||||||
The recipe will automatically be built in a special isolated build
|
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
|
from pythonforandroid.toolchain import current_directory
|
||||||
def build_arch(self, arch):
|
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 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')
|
fileh.write('This is written to a file within the build dir')
|
||||||
|
|
||||||
The argument to each method, ``arch``, is an object relating to the
|
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::
|
this using the ``sh`` module as follows::
|
||||||
|
|
||||||
def build_arch(self, arch):
|
def build_arch(self, arch):
|
||||||
super(YourRecipe, self).build_arch(arch)
|
super().build_arch(arch)
|
||||||
env = self.get_recipe_env(arch)
|
env = self.get_recipe_env(arch)
|
||||||
sh.echo('$PATH', _env=env) # Will print the PATH entry from the
|
sh.echo('$PATH', _env=env) # Will print the PATH entry from the
|
||||||
# env dict
|
# env dict
|
||||||
|
@ -192,12 +191,12 @@ its current status::
|
||||||
shprint(sh.echo, '$PATH', _env=env)
|
shprint(sh.echo, '$PATH', _env=env)
|
||||||
|
|
||||||
You can also override the ``get_recipe_env`` method to add new 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
|
the following when compiling for SDL2, in order to tell Kivy what
|
||||||
backend to use::
|
backend to use::
|
||||||
|
|
||||||
def get_recipe_env(self, arch):
|
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['USE_SDL2'] = '1'
|
||||||
|
|
||||||
env['KIVY_SDL2_PATH'] = ':'.join([
|
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 instance, the following is all that's necessary to create a recipe
|
||||||
for the Vispy module::
|
for the Vispy module::
|
||||||
|
|
||||||
from pythonforandroid.toolchain import PythonRecipe
|
from pythonforandroid.recipe import PythonRecipe
|
||||||
class VispyRecipe(PythonRecipe):
|
class VispyRecipe(PythonRecipe):
|
||||||
version = 'master'
|
version = 'master'
|
||||||
url = 'https://github.com/vispy/vispy/archive/{version}.zip'
|
url = 'https://github.com/vispy/vispy/archive/{version}.zip'
|
||||||
|
|
||||||
depends = ['python2', 'numpy']
|
depends = ['python3', 'numpy']
|
||||||
|
|
||||||
site_packages_name = 'vispy'
|
site_packages_name = 'vispy'
|
||||||
|
|
||||||
|
@ -273,7 +272,7 @@ Python installation.
|
||||||
For reference, the code that accomplishes this is the following::
|
For reference, the code that accomplishes this is the following::
|
||||||
|
|
||||||
def build_arch(self, arch):
|
def build_arch(self, arch):
|
||||||
super(PythonRecipe, self).build_arch(arch)
|
super().build_arch(arch)
|
||||||
self.install_python_package()
|
self.install_python_package()
|
||||||
|
|
||||||
def install_python_package(self):
|
def install_python_package(self):
|
||||||
|
@ -307,14 +306,14 @@ the cython components and to install the Python module just like a
|
||||||
normal PythonRecipe.
|
normal PythonRecipe.
|
||||||
|
|
||||||
For instance, the following is all that's necessary to make a recipe
|
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):
|
class KivyRecipe(CythonRecipe):
|
||||||
version = 'stable'
|
version = 'stable'
|
||||||
url = 'https://github.com/kivy/kivy/archive/{version}.zip'
|
url = 'https://github.com/kivy/kivy/archive/{version}.zip'
|
||||||
name = 'kivy'
|
name = 'kivy'
|
||||||
|
|
||||||
depends = ['pygame', 'pyjnius', 'android']
|
depends = ['sdl2', 'pyjnius']
|
||||||
|
|
||||||
recipe = KivyRecipe()
|
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',
|
shprint(sh.find, build_lib[0], '-name', '*.o', '-exec',
|
||||||
env['STRIP'], '{}', ';', _env=env)
|
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
|
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
|
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
|
this is probably not usually an issue. If this happens to you, try
|
||||||
patching to remove this import or make it fail quietly.
|
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
|
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
|
has an ``Application.mk`` and ``Android.mk`` that the Android build
|
||||||
system can use), you can use an NDKRecipe to automatically set it
|
system can use), you can use an NDKRecipe to automatically set it
|
||||||
up. The NDKRecipe overrides the normal ``get_build_dir`` method to
|
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'
|
url = 'http://example.com/example-{version}.tar.gz'
|
||||||
# {version} will be replaced with self.version when downloading
|
# {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
|
# that must be built before this
|
||||||
# one
|
# one
|
||||||
|
|
||||||
|
@ -435,29 +434,29 @@ overrides if you do not use them::
|
||||||
# alongside this one
|
# alongside this one
|
||||||
|
|
||||||
def get_recipe_env(self, arch):
|
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
|
# Manipulate the env here if you want
|
||||||
return env
|
return env
|
||||||
|
|
||||||
def should_build(self):
|
def should_build(self, arch):
|
||||||
# Add a check for whether the recipe is already built if you
|
# Add a check for whether the recipe is already built if you
|
||||||
# want, and return False if it is.
|
# want, and return False if it is.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def prebuild_arch(self, arch):
|
def prebuild_arch(self, arch):
|
||||||
super(YourRecipe, self).prebuild_arch(self)
|
super().prebuild_arch(self)
|
||||||
# Do any extra prebuilding you want, e.g.:
|
# Do any extra prebuilding you want, e.g.:
|
||||||
self.apply_patch('path/to/patch.patch')
|
self.apply_patch('path/to/patch.patch')
|
||||||
|
|
||||||
def build_arch(self, arch):
|
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.
|
# Build the code. Make sure to use the right build dir, e.g.
|
||||||
with current_directory(self.get_build_dir(arch.arch)):
|
with current_directory(self.get_build_dir(arch.arch)):
|
||||||
sh.ls('-lathr') # Or run some commands that actually do
|
sh.ls('-lathr') # Or run some commands that actually do
|
||||||
# something
|
# something
|
||||||
|
|
||||||
def postbuild_arch(self, arch):
|
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
|
# Do anything you want after the build, e.g. deleting
|
||||||
# unnecessary files such as documentation
|
# 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.
|
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
|
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
|
will have its own main.py file with the Python script to be run.
|
||||||
can communicate with the service process from your app using e.g. `osc
|
Please note that python-for-android explicitly runs services as separated
|
||||||
<https://pypi.python.org/pypi/python-osc>`__ or (a heavier option)
|
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/>`__.
|
`twisted <https://twistedmatrix.com/trac/>`__.
|
||||||
|
|
||||||
Service creation
|
Service creation
|
||||||
|
@ -21,18 +26,16 @@ There are two ways to have services included in your APK.
|
||||||
Service folder
|
Service folder
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
This basic method works with both the new SDL2 and old Pygame
|
This is the older method of handling services. It is
|
||||||
bootstraps. It is recommended to use the second method (below) where
|
recommended to use the second method (below) where possible.
|
||||||
possible.
|
|
||||||
|
|
||||||
Create a folder named ``service`` in your app directory, and add a
|
Create a folder named ``service`` in your app directory, and add a
|
||||||
file ``service/main.py``. This file should contain the Python code
|
file ``service/main.py``. This file should contain the Python code
|
||||||
that you want the service to run.
|
that you want the service to run.
|
||||||
|
|
||||||
To start the service, use the :code:`start_service` function from the
|
To start the service, use the :code:`start_service` function from the
|
||||||
:code:`android` module (included automatically with the Pygame
|
:code:`android` module (you may need to add ``android`` to your app
|
||||||
bootstrap, you must add it to the requirements manually with SDL2 if
|
requirements)::
|
||||||
you wish to use this method)::
|
|
||||||
|
|
||||||
import android
|
import android
|
||||||
android.start_service(title='service name',
|
android.start_service(title='service name',
|
||||||
|
@ -44,38 +47,67 @@ you wish to use this method)::
|
||||||
Arbitrary service scripts
|
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
|
This method is recommended for non-trivial use of services as it is
|
||||||
more flexible, supporting multiple services and a wider range of
|
more flexible, supporting multiple services and a wider range of
|
||||||
options.
|
options.
|
||||||
|
|
||||||
To create the service, create a python script with your service code
|
To create the service, create a python script with your service code
|
||||||
and add a :code:`--service=myservice:/path/to/myservice.py` argument
|
and add a :code:`--service=myservice:PATH_TO_SERVICE_PY` argument
|
||||||
when calling python-for-android. The ``myservice`` name before the
|
when calling python-for-android, or in buildozer.spec, a
|
||||||
colon is the name of the service class, via which you will interact
|
:code:`services = myservice:PATH_TO_SERVICE_PY` [app] setting.
|
||||||
with it later. You can add multiple
|
|
||||||
:code:`--service` arguments to include multiple services, which you
|
The ``myservice`` name before the colon is the name of the service
|
||||||
will later be able to stop and start from your app.
|
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
|
To run the services (i.e. starting them from within your main app
|
||||||
code), you must use PyJNIus to interact with the java class
|
code), you must use PyJNIus to interact with the java class
|
||||||
python-for-android creates for each one, as follows::
|
python-for-android creates for each one, as follows::
|
||||||
|
|
||||||
from jnius import autoclass
|
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
|
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
|
||||||
argument = ''
|
argument = ''
|
||||||
service.start(mActivity, argument)
|
service.start(mActivity, argument)
|
||||||
|
|
||||||
Here, ``your.package.name`` refers to the package identifier of your
|
Here, ``your.package.domain.package.name`` refers to the package identifier
|
||||||
APK as set by the ``--package`` argument to python-for-android, and
|
of your APK.
|
||||||
the name of the service is ``ServiceYourservicename``, in which
|
|
||||||
``Yourservicename`` is the identifier passed to the ``--service``
|
If you are using buildozer, the identifier is set by the ``package.name``
|
||||||
argument with the first letter upper case. You must also pass the
|
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
|
``argument`` parameter even if (as here) it is an empty string. If you
|
||||||
do pass it, the service can make use of this argument.
|
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
|
Services support a range of options and interactions not yet
|
||||||
documented here but all accessible via calling other methods of the
|
documented here but all accessible via calling other methods of the
|
||||||
``service`` reference.
|
``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``
|
your service folder you must use e.g. ``import service.module``
|
||||||
instead of ``import module``, if the service file is in the
|
instead of ``import module``, if the service file is in the
|
||||||
``service/`` folder.
|
``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
|
full debug output including the output of all the external tools used
|
||||||
in the compilation and packaging steps.
|
in the compilation and packaging steps.
|
||||||
|
|
||||||
If reporting a problem by email or irc, it is usually helpful to
|
If reporting a problem by email or Discord, it is usually helpful to
|
||||||
include this full log, via e.g. a `pastebin
|
include this full log, e.g. via a `pastebin
|
||||||
<http://paste.ubuntu.com/>`_ or `Github gist
|
<http://paste.ubuntu.com/>`_ or `Github gist
|
||||||
<https://gist.github.com/>`_.
|
<https://gist.github.com/>`_.
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ get help with any problems using the same channels as Kivy itself:
|
||||||
|
|
||||||
- by email to the `kivy-users Google group
|
- by email to the `kivy-users Google group
|
||||||
<https://groups.google.com/forum/#!forum/kivy-users>`_
|
<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
|
If you find a bug, you can also post an issue on the
|
||||||
`python-for-android Github page
|
`python-for-android Github page
|
||||||
<https://github.com/kivy/python-for-android>`_.
|
<https://github.com/kivy/python-for-android>`_.
|
||||||
|
@ -58,7 +58,7 @@ grepping this).
|
||||||
When your app crashes, you'll see the normal Python traceback here, as
|
When your app crashes, you'll see the normal Python traceback here, as
|
||||||
well as the output of any print statements etc. that your app
|
well as the output of any print statements etc. that your app
|
||||||
runs. Use these to diagnose the problem just as normal.
|
runs. Use these to diagnose the problem just as normal.
|
||||||
|
|
||||||
The adb command passes its arguments straight to adb itself, so you
|
The adb command passes its arguments straight to adb itself, so you
|
||||||
can also do other debugging tasks such as ``python-for-android adb
|
can also do other debugging tasks such as ``python-for-android adb
|
||||||
devices`` to get the list of connected devices.
|
devices`` to get the list of connected devices.
|
||||||
|
@ -72,7 +72,7 @@ particular.
|
||||||
Unpacking an APK
|
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.
|
especially when debugging python-for-android itself.
|
||||||
|
|
||||||
APKs are just zip files, so you can extract the contents easily::
|
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
|
AndroidManifest.xml classes.dex META-INF res
|
||||||
assets lib YourApk.apk resources.arsc
|
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
|
$ cd assets
|
||||||
$ ls
|
$ ls
|
||||||
private.mp3
|
private.tar
|
||||||
|
|
||||||
``private.mp3`` is actually a tarball containing all your packaged
|
``private.tar`` is a tarball containing all your packaged
|
||||||
data, and the Python distribution. Extract it::
|
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
|
$ ls
|
||||||
android_runnable.pyo include interpreter_subprocess main.kv pipinterface.kv settings.pyo
|
README.txt android.txt icon.png main.pyc p4a_env_vars.txt particle.png
|
||||||
assets __init__.pyo interpreterwrapper.pyo main.pyo pipinterface.pyo utils.pyo
|
private.tar touchtracer.kv
|
||||||
editor.kv interpreter.kv lib menu.kv private.mp3 widgets.pyo
|
|
||||||
editor.pyo interpreter.pyo libpymodules.so menu.pyo settings.kv
|
|
||||||
|
|
||||||
Most of these files have been included by the user (in this case, they
|
Due to how We're required to ship ABI-specific things in Android App Bundle,
|
||||||
come from one of my own apps), the rest relate to the python
|
the Python installation is packaged separately, as (most of it) is ABI-specific.
|
||||||
distribution.
|
|
||||||
|
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
|
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
|
This is a known bug in some releases. To work around it, add your
|
||||||
python requirement explicitly,
|
python requirement explicitly,
|
||||||
e.g. :code:`--requirements=python2,kivy`. This also applies when using
|
e.g. :code:`--requirements=python3,kivy`. This also applies when using
|
||||||
buildozer, in which case add python2 to your buildozer.spec requirements.
|
buildozer, in which case add python3 to your buildozer.spec requirements.
|
||||||
|
|
||||||
linkname too long
|
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
|
directory should fix the problem, and is desirable anyway since you
|
||||||
don't want it in the APK.
|
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
|
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
|
If using buildozer this should be done automatically, but as a
|
||||||
workaround you can run these from
|
workaround you can run these from
|
||||||
``~/.buildozer/android/platform/android-sdk-20/tools/android``.
|
``~/.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__ = '2022.09.04'
|
||||||
__version__ = '0.5'
|
|
||||||
|
|
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 distutils.spawn import find_executable
|
||||||
from os import environ
|
from os import environ
|
||||||
from os.path import (exists, join, dirname, split)
|
from os.path import join
|
||||||
from glob import glob
|
from multiprocessing import cpu_count
|
||||||
|
|
||||||
from pythonforandroid.recipe import Recipe
|
from pythonforandroid.recipe import Recipe
|
||||||
from pythonforandroid.util import BuildInterruptingException, build_platform
|
from pythonforandroid.util import BuildInterruptingException, build_platform
|
||||||
|
|
||||||
|
|
||||||
class Arch(object):
|
class Arch:
|
||||||
|
|
||||||
toolchain_prefix = None
|
|
||||||
'''The prefix for the toolchain dir in the NDK.'''
|
|
||||||
|
|
||||||
command_prefix = None
|
command_prefix = None
|
||||||
'''The prefix for NDK commands such as gcc.'''
|
'''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):
|
def __init__(self, ctx):
|
||||||
super(Arch, self).__init__()
|
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
|
|
||||||
# Allows injecting additional linker paths used by any recipe.
|
# Allows injecting additional linker paths used by any recipe.
|
||||||
|
@ -28,6 +52,14 @@ class Arch(object):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.arch
|
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
|
@property
|
||||||
def include_dirs(self):
|
def include_dirs(self):
|
||||||
return [
|
return [
|
||||||
|
@ -38,216 +70,235 @@ class Arch(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self):
|
def target(self):
|
||||||
target_data = self.command_prefix.split('-')
|
# As of NDK r19, the toolchains installed by default with the
|
||||||
return '-'.join(
|
# NDK may be used in-place. The make_standalone_toolchain.py script
|
||||||
[target_data[0], 'none', target_data[1], target_data[2]])
|
# is no longer needed for interfacing with arbitrary build systems.
|
||||||
|
# See: https://developer.android.com/ndk/guides/other_build_systems
|
||||||
|
return '{triplet}{ndk_api}'.format(
|
||||||
|
triplet=self.command_prefix, ndk_api=self.ctx.ndk_api
|
||||||
|
)
|
||||||
|
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
@property
|
||||||
|
def clang_exe(self):
|
||||||
|
"""Full path of the clang compiler depending on the android's ndk
|
||||||
|
version used."""
|
||||||
|
return self.get_clang_exe()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def clang_exe_cxx(self):
|
||||||
|
"""Full path of the clang++ compiler depending on the android's ndk
|
||||||
|
version used."""
|
||||||
|
return self.get_clang_exe(plus_plus=True)
|
||||||
|
|
||||||
|
def get_clang_exe(self, with_target=False, plus_plus=False):
|
||||||
|
"""Returns the full path of the clang/clang++ compiler, supports two
|
||||||
|
kwargs:
|
||||||
|
|
||||||
|
- `with_target`: prepend `target` to clang
|
||||||
|
- `plus_plus`: will return the clang++ compiler (defaults to `False`)
|
||||||
|
"""
|
||||||
|
compiler = 'clang'
|
||||||
|
if with_target:
|
||||||
|
compiler = '{target}-{compiler}'.format(
|
||||||
|
target=self.target, compiler=compiler
|
||||||
|
)
|
||||||
|
if plus_plus:
|
||||||
|
compiler += '++'
|
||||||
|
return join(self.ctx.ndk.llvm_bin_dir, compiler)
|
||||||
|
|
||||||
|
def get_env(self, with_flags_in_cc=True):
|
||||||
env = {}
|
env = {}
|
||||||
|
|
||||||
cflags = [
|
# HOME: User's home directory
|
||||||
'-DANDROID',
|
#
|
||||||
'-fomit-frame-pointer',
|
# Many tools including p4a store outputs in the user's home
|
||||||
'-D__ANDROID_API__={}'.format(self.ctx.ndk_api)]
|
# directory. This is found from the HOME environment variable
|
||||||
if not clang:
|
# and falls back to the system account database. Setting HOME
|
||||||
cflags.append('-mandroid')
|
# can be used to globally divert these tools to use a different
|
||||||
else:
|
# path. Furthermore, in containerized environments the user may
|
||||||
cflags.append('-target ' + self.target)
|
# not exist in the account database, so if HOME isn't set than
|
||||||
toolchain = '{android_host}-{toolchain_version}'.format(
|
# these tools will fail.
|
||||||
android_host=self.ctx.toolchain_prefix,
|
if 'HOME' in environ:
|
||||||
toolchain_version=self.ctx.toolchain_version)
|
env['HOME'] = environ['HOME']
|
||||||
toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain,
|
|
||||||
'prebuilt', build_platform)
|
|
||||||
cflags.append('-gcc-toolchain {}'.format(toolchain))
|
|
||||||
|
|
||||||
env['CFLAGS'] = ' '.join(cflags)
|
# CFLAGS/CXXFLAGS: the processor flags
|
||||||
|
env['CFLAGS'] = ' '.join(self.common_cflags).format(target=self.target)
|
||||||
|
if self.arch_cflags:
|
||||||
|
# each architecture may have has his own CFLAGS
|
||||||
|
env['CFLAGS'] += ' ' + ' '.join(self.arch_cflags)
|
||||||
|
env['CXXFLAGS'] = env['CFLAGS']
|
||||||
|
|
||||||
# Link the extra global link paths first before anything else
|
# CPPFLAGS (for macros and includes)
|
||||||
|
env['CPPFLAGS'] = ' '.join(self.common_cppflags).format(
|
||||||
|
ctx=self.ctx,
|
||||||
|
command_prefix=self.command_prefix,
|
||||||
|
python_includes=join(
|
||||||
|
self.ctx.get_python_install_dir(self.arch),
|
||||||
|
'include/python{}'.format(self.ctx.python_recipe.version[0:3]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# LDFLAGS: Link the extra global link paths first before anything else
|
||||||
# (such that overriding system libraries with them is possible)
|
# (such that overriding system libraries with them is possible)
|
||||||
env['LDFLAGS'] = ' ' + " ".join([
|
env['LDFLAGS'] = (
|
||||||
"-L'" + l.replace("'", "'\"'\"'") + "'" # no shlex.quote in py2
|
' '
|
||||||
for l in self.extra_global_link_paths
|
+ " ".join(
|
||||||
]) + ' '
|
[
|
||||||
|
"-L'"
|
||||||
|
+ link_path.replace("'", "'\"'\"'")
|
||||||
|
+ "'" # no shlex.quote in py2
|
||||||
|
for link_path in self.extra_global_link_paths
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ ' ' + ' '.join(self.common_ldflags).format(
|
||||||
|
ctx_libs_dir=self.ctx.get_libs_dir(self.arch)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
sysroot = join(self.ctx._ndk_dir, 'sysroot')
|
# LDLIBS: Library flags or names given to compilers when they are
|
||||||
if exists(sysroot):
|
# supposed to invoke the linker.
|
||||||
# post-15 NDK per
|
env['LDLIBS'] = ' '.join(self.common_ldlibs)
|
||||||
# https://android.googlesource.com/platform/ndk/+/ndk-r15-release/docs/UnifiedHeaders.md
|
|
||||||
env['CFLAGS'] += ' -isystem {}/sysroot/usr/include/{}'.format(
|
|
||||||
self.ctx.ndk_dir, self.ctx.toolchain_prefix)
|
|
||||||
env['CFLAGS'] += ' -I{}/sysroot/usr/include/{}'.format(
|
|
||||||
self.ctx.ndk_dir, self.command_prefix)
|
|
||||||
else:
|
|
||||||
sysroot = self.ctx.ndk_platform
|
|
||||||
env['CFLAGS'] += ' -I{}'.format(self.ctx.ndk_platform)
|
|
||||||
env['CFLAGS'] += ' -isysroot {} '.format(sysroot)
|
|
||||||
env['CFLAGS'] += '-I' + join(self.ctx.get_python_install_dir(),
|
|
||||||
'include/python{}'.format(
|
|
||||||
self.ctx.python_recipe.version[0:3])
|
|
||||||
)
|
|
||||||
|
|
||||||
env['LDFLAGS'] += '--sysroot={} '.format(self.ctx.ndk_platform)
|
|
||||||
|
|
||||||
env["CXXFLAGS"] = env["CFLAGS"]
|
|
||||||
|
|
||||||
env["LDFLAGS"] += " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)])
|
|
||||||
|
|
||||||
if self.ctx.ndk == 'crystax':
|
|
||||||
env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch)
|
|
||||||
|
|
||||||
toolchain_prefix = self.ctx.toolchain_prefix
|
|
||||||
toolchain_version = self.ctx.toolchain_version
|
|
||||||
command_prefix = self.command_prefix
|
|
||||||
|
|
||||||
env['TOOLCHAIN_PREFIX'] = toolchain_prefix
|
|
||||||
env['TOOLCHAIN_VERSION'] = toolchain_version
|
|
||||||
|
|
||||||
|
# CCACHE
|
||||||
ccache = ''
|
ccache = ''
|
||||||
if self.ctx.ccache and bool(int(environ.get('USE_CCACHE', '1'))):
|
if self.ctx.ccache and bool(int(environ.get('USE_CCACHE', '1'))):
|
||||||
# print('ccache found, will optimize builds')
|
# print('ccache found, will optimize builds')
|
||||||
ccache = self.ctx.ccache + ' '
|
ccache = self.ctx.ccache + ' '
|
||||||
env['USE_CCACHE'] = '1'
|
env['USE_CCACHE'] = '1'
|
||||||
env['NDK_CCACHE'] = self.ctx.ccache
|
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:
|
# Compiler: `CC` and `CXX` (and make sure that the compiler exists)
|
||||||
llvm_dirname = split(
|
env['PATH'] = self.ctx.env['PATH']
|
||||||
glob(join(self.ctx.ndk_dir, 'toolchains', 'llvm*'))[-1])[-1]
|
cc = find_executable(self.clang_exe, path=env['PATH'])
|
||||||
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'])
|
|
||||||
if cc is None:
|
if cc is None:
|
||||||
print('Searching path are: {!r}'.format(environ['PATH']))
|
print('Searching path are: {!r}'.format(env['PATH']))
|
||||||
raise BuildInterruptingException(
|
raise BuildInterruptingException(
|
||||||
'Couldn\'t find executable for CC. This indicates a '
|
'Couldn\'t find executable for CC. This indicates a '
|
||||||
'problem locating the {} executable in the Android '
|
'problem locating the {} executable in the Android '
|
||||||
'NDK, not that you don\'t have a normal compiler '
|
'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:
|
if with_flags_in_cc:
|
||||||
env['CC'] = '{ccache}{exe} {cflags}'.format(
|
env['CC'] = '{ccache}{exe} {cflags}'.format(
|
||||||
exe=exe,
|
exe=self.clang_exe,
|
||||||
ccache=ccache,
|
ccache=ccache,
|
||||||
cflags=env['CFLAGS'])
|
cflags=env['CFLAGS'])
|
||||||
env['CXX'] = '{ccache}{execxx} {cxxflags}'.format(
|
env['CXX'] = '{ccache}{execxx} {cxxflags}'.format(
|
||||||
execxx=execxx,
|
execxx=self.clang_exe_cxx,
|
||||||
ccache=ccache,
|
ccache=ccache,
|
||||||
cxxflags=env['CXXFLAGS'])
|
cxxflags=env['CXXFLAGS'])
|
||||||
else:
|
else:
|
||||||
env['CC'] = '{ccache}{exe}'.format(
|
env['CC'] = '{ccache}{exe}'.format(
|
||||||
exe=exe,
|
exe=self.clang_exe,
|
||||||
ccache=ccache)
|
ccache=ccache)
|
||||||
env['CXX'] = '{ccache}{execxx}'.format(
|
env['CXX'] = '{ccache}{execxx}'.format(
|
||||||
execxx=execxx,
|
execxx=self.clang_exe_cxx,
|
||||||
ccache=ccache)
|
ccache=ccache)
|
||||||
|
|
||||||
env['AR'] = '{}-ar'.format(command_prefix)
|
# Android's LLVM binutils
|
||||||
env['RANLIB'] = '{}-ranlib'.format(command_prefix)
|
env['AR'] = self.ctx.ndk.llvm_ar
|
||||||
env['LD'] = '{}-ld'.format(command_prefix)
|
env['RANLIB'] = self.ctx.ndk.llvm_ranlib
|
||||||
env['LDSHARED'] = env["CC"] + " -pthread -shared " +\
|
env['STRIP'] = f'{self.ctx.ndk.llvm_strip} --strip-unneeded'
|
||||||
"-Wl,-O1 -Wl,-Bsymbolic-functions "
|
env['READELF'] = self.ctx.ndk.llvm_readelf
|
||||||
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
|
env['OBJCOPY'] = self.ctx.ndk.llvm_objcopy
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
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(
|
hostpython_recipe = Recipe.get_recipe(
|
||||||
'host' + self.ctx.python_recipe.name, self.ctx)
|
'host' + self.ctx.python_recipe.name, self.ctx)
|
||||||
env['BUILDLIB_PATH'] = join(
|
env['BUILDLIB_PATH'] = join(
|
||||||
hostpython_recipe.get_build_dir(self.arch),
|
hostpython_recipe.get_build_dir(self.arch),
|
||||||
'build', 'lib.{}-{}'.format(
|
'native-build',
|
||||||
build_platform, self.ctx.python_recipe.major_minor_version_string)
|
'build',
|
||||||
|
'lib.{}-{}'.format(
|
||||||
|
build_platform,
|
||||||
|
self.ctx.python_recipe.major_minor_version_string,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
env['PATH'] = environ['PATH']
|
# for reproducible builds
|
||||||
|
if 'SOURCE_DATE_EPOCH' in environ:
|
||||||
env['ARCH'] = self.arch
|
for k in 'LC_ALL TZ SOURCE_DATE_EPOCH PYTHONHASHSEED BUILD_DATE BUILD_TIME'.split():
|
||||||
env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api))
|
if k in environ:
|
||||||
|
env[k] = environ[k]
|
||||||
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
|
|
||||||
env['CRYSTAX_PYTHON_VERSION'] = self.ctx.python_recipe.version
|
|
||||||
|
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
class ArchARM(Arch):
|
class ArchARM(Arch):
|
||||||
arch = "armeabi"
|
arch = "armeabi"
|
||||||
toolchain_prefix = 'arm-linux-androideabi'
|
|
||||||
command_prefix = 'arm-linux-androideabi'
|
command_prefix = 'arm-linux-androideabi'
|
||||||
platform_dir = 'arch-arm'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self):
|
def target(self):
|
||||||
target_data = self.command_prefix.split('-')
|
target_data = self.command_prefix.split('-')
|
||||||
return '-'.join(
|
return '{triplet}{ndk_api}'.format(
|
||||||
['armv7a', 'none', target_data[1], target_data[2]])
|
triplet='-'.join(['armv7a', target_data[1], target_data[2]]),
|
||||||
|
ndk_api=self.ctx.ndk_api,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArchARMv7_a(ArchARM):
|
class ArchARMv7_a(ArchARM):
|
||||||
arch = 'armeabi-v7a'
|
arch = 'armeabi-v7a'
|
||||||
|
arch_cflags = [
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
'-march=armv7-a',
|
||||||
env = super(ArchARMv7_a, self).get_env(with_flags_in_cc, clang=clang)
|
'-mfloat-abi=softfp',
|
||||||
env['CFLAGS'] = (env['CFLAGS'] +
|
'-mfpu=vfp',
|
||||||
(' -march=armv7-a -mfloat-abi=softfp '
|
'-mthumb',
|
||||||
'-mfpu=vfp -mthumb'))
|
'-fPIC',
|
||||||
env['CXXFLAGS'] = env['CFLAGS']
|
]
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
class Archx86(Arch):
|
class Archx86(Arch):
|
||||||
arch = 'x86'
|
arch = 'x86'
|
||||||
toolchain_prefix = 'x86'
|
|
||||||
command_prefix = 'i686-linux-android'
|
command_prefix = 'i686-linux-android'
|
||||||
platform_dir = 'arch-x86'
|
arch_cflags = [
|
||||||
|
'-march=i686',
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
'-mssse3',
|
||||||
env = super(Archx86, self).get_env(with_flags_in_cc, clang=clang)
|
'-mfpmath=sse',
|
||||||
env['CFLAGS'] = (env['CFLAGS'] +
|
'-m32',
|
||||||
' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32')
|
'-fPIC',
|
||||||
env['CXXFLAGS'] = env['CFLAGS']
|
]
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
class Archx86_64(Arch):
|
class Archx86_64(Arch):
|
||||||
arch = 'x86_64'
|
arch = 'x86_64'
|
||||||
toolchain_prefix = 'x86_64'
|
|
||||||
command_prefix = 'x86_64-linux-android'
|
command_prefix = 'x86_64-linux-android'
|
||||||
platform_dir = 'arch-x86_64'
|
arch_cflags = [
|
||||||
|
'-march=x86-64',
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
'-msse4.2',
|
||||||
env = super(Archx86_64, self).get_env(with_flags_in_cc, clang=clang)
|
'-mpopcnt',
|
||||||
env['CFLAGS'] = (env['CFLAGS'] +
|
'-m64',
|
||||||
' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel')
|
'-fPIC',
|
||||||
env['CXXFLAGS'] = env['CFLAGS']
|
]
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
class ArchAarch_64(Arch):
|
class ArchAarch_64(Arch):
|
||||||
arch = 'arm64-v8a'
|
arch = 'arm64-v8a'
|
||||||
toolchain_prefix = 'aarch64-linux-android'
|
|
||||||
command_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):
|
# Note: This `EXTRA_CFLAGS` below should target the commented `include`
|
||||||
env = super(ArchAarch_64, self).get_env(with_flags_in_cc, clang=clang)
|
# above in `arch_cflags`. The original lines were added during the Sdl2's
|
||||||
incpath = ' -I' + join(dirname(__file__), 'includes', 'arm64-v8a')
|
# bootstrap creation, and modified/commented during the migration to the
|
||||||
env['EXTRA_CFLAGS'] = incpath
|
# NDK r19 build system, because it seems that we don't need it anymore,
|
||||||
env['CFLAGS'] += incpath
|
# do we need them?
|
||||||
env['CXXFLAGS'] += incpath
|
# def get_env(self, with_flags_in_cc=True):
|
||||||
if with_flags_in_cc:
|
# env = super().get_env(with_flags_in_cc)
|
||||||
env['CC'] += incpath
|
# env['EXTRA_CFLAGS'] = self.arch_cflags[-1]
|
||||||
env['CXX'] += incpath
|
# return env
|
||||||
return env
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
from __future__ import print_function
|
|
||||||
from setuptools import Command
|
from setuptools import Command
|
||||||
from pythonforandroid import toolchain
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from os.path import realpath, join, exists, dirname, curdir, basename, split
|
from os.path import realpath, join, exists, dirname, curdir, basename, split
|
||||||
|
@ -16,16 +14,16 @@ def argv_contains(t):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BdistAPK(Command):
|
class Bdist(Command):
|
||||||
description = 'Create an APK with python-for-android'
|
|
||||||
|
|
||||||
user_options = []
|
user_options = []
|
||||||
|
package_type = None
|
||||||
|
|
||||||
def initialize_options(self):
|
def initialize_options(self):
|
||||||
for option in self.user_options:
|
for option in self.user_options:
|
||||||
setattr(self, option[0].strip('=').replace('-', '_'), None)
|
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
|
# This is a hack, we probably aren't supposed to loop through
|
||||||
# the option_dict so early because distutils does exactly the
|
# 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():
|
for (option, (source, value)) in option_dict.items():
|
||||||
setattr(self, option, str(value))
|
setattr(self, option, str(value))
|
||||||
|
|
||||||
|
|
||||||
def finalize_options(self):
|
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():
|
for (option, (source, value)) in setup_options.items():
|
||||||
if source == 'command line':
|
if source == 'command line':
|
||||||
continue
|
continue
|
||||||
|
@ -70,16 +67,15 @@ class BdistAPK(Command):
|
||||||
sys.argv.append('--version={}'.format(version))
|
sys.argv.append('--version={}'.format(version))
|
||||||
|
|
||||||
if not argv_contains('--arch'):
|
if not argv_contains('--arch'):
|
||||||
arch = 'arm64-v8a'
|
arch = 'armeabi-v7a'
|
||||||
self.arch = arch
|
self.arch = arch
|
||||||
sys.argv.append('--arch={}'.format(arch))
|
sys.argv.append('--arch={}'.format(arch))
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|
||||||
self.prepare_build_dir()
|
self.prepare_build_dir()
|
||||||
|
|
||||||
from pythonforandroid.toolchain import main
|
from pythonforandroid.entrypoints import main
|
||||||
sys.argv[1] = 'apk'
|
sys.argv[1] = self.package_type
|
||||||
main()
|
main()
|
||||||
|
|
||||||
def prepare_build_dir(self):
|
def prepare_build_dir(self):
|
||||||
|
@ -112,7 +108,7 @@ class BdistAPK(Command):
|
||||||
makedirs(new_dir)
|
makedirs(new_dir)
|
||||||
print('Including {}'.format(filen))
|
print('Including {}'.format(filen))
|
||||||
copyfile(filen, join(bdist_dir, 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)
|
main_py_dirs.append(filen)
|
||||||
|
|
||||||
# This feels ridiculous, but how else to define the main.py dir?
|
# This feels ridiculous, but how else to define the main.py dir?
|
||||||
|
@ -123,7 +119,7 @@ class BdistAPK(Command):
|
||||||
exit(1)
|
exit(1)
|
||||||
if len(main_py_dirs) > 1:
|
if len(main_py_dirs) > 1:
|
||||||
print('WARNING: Multiple main.py dirs found, using the shortest path')
|
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'):
|
if not argv_contains('--launcher'):
|
||||||
sys.argv.append('--private={}'.format(
|
sys.argv.append('--private={}'.format(
|
||||||
|
@ -131,18 +127,39 @@ class BdistAPK(Command):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BdistAPK(Bdist):
|
||||||
|
"""distutil command handler for 'apk'."""
|
||||||
|
description = 'Create an APK with python-for-android'
|
||||||
|
package_type = 'apk'
|
||||||
|
|
||||||
|
|
||||||
|
class BdistAAR(Bdist):
|
||||||
|
"""distutil command handler for 'aar'."""
|
||||||
|
description = 'Create an AAR with python-for-android'
|
||||||
|
package_type = 'aar'
|
||||||
|
|
||||||
|
|
||||||
|
class BdistAAB(Bdist):
|
||||||
|
"""distutil command handler for 'aab'."""
|
||||||
|
description = 'Create an AAB with python-for-android'
|
||||||
|
package_type = 'aab'
|
||||||
|
|
||||||
|
|
||||||
def _set_user_options():
|
def _set_user_options():
|
||||||
# This seems like a silly way to do things, but not sure if there's a
|
# 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
|
# better way to pass arbitrary options onwards to p4a
|
||||||
user_options = [('requirements=', None, None),]
|
user_options = [('requirements=', None, None), ]
|
||||||
for i, arg in enumerate(sys.argv):
|
for i, arg in enumerate(sys.argv):
|
||||||
if arg.startswith('--'):
|
if arg.startswith('--'):
|
||||||
if ('=' in arg or
|
if ('=' in arg or
|
||||||
(i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))):
|
(i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))):
|
||||||
user_options.append((arg[2:].split('=')[0] + '=', None, None))
|
user_options.append((arg[2:].split('=')[0] + '=', None, None))
|
||||||
else:
|
else:
|
||||||
user_options.append((arg[2:], None, None))
|
user_options.append((arg[2:], None, None))
|
||||||
|
|
||||||
BdistAPK.user_options = user_options
|
BdistAPK.user_options = user_options
|
||||||
|
BdistAAB.user_options = user_options
|
||||||
|
BdistAAR.user_options = user_options
|
||||||
|
|
||||||
|
|
||||||
_set_user_options()
|
_set_user_options()
|
||||||
|
|
259
p4a/pythonforandroid/bootstrap.py
Normal file → Executable file
259
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.path import (join, dirname, isdir, normpath, splitext, basename)
|
||||||
from os import listdir, walk, sep
|
from os import listdir, walk, sep
|
||||||
import sh
|
import sh
|
||||||
import shlex
|
import shlex
|
||||||
import glob
|
|
||||||
import importlib
|
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pythonforandroid.logger import (warning, shprint, info, logger,
|
from pythonforandroid.logger import (shprint, info, logger, debug)
|
||||||
debug)
|
from pythonforandroid.util import (
|
||||||
from pythonforandroid.util import (current_directory, ensure_dir,
|
current_directory, ensure_dir, temp_directory, BuildInterruptingException)
|
||||||
temp_directory)
|
|
||||||
from pythonforandroid.recipe import Recipe
|
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 root, dirnames, filenames in walk(src_root):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
subdir = normpath(root.replace(src_root, ""))
|
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):
|
if override and os.path.exists(dest_file):
|
||||||
os.unlink(dest_file)
|
os.unlink(dest_file)
|
||||||
if not os.path.exists(dest_file):
|
if not os.path.exists(dest_file):
|
||||||
shutil.copy(src_file, dest_file)
|
if symlink:
|
||||||
|
os.symlink(src_file, dest_file)
|
||||||
|
else:
|
||||||
|
shutil.copy(src_file, dest_file)
|
||||||
else:
|
else:
|
||||||
os.makedirs(dest_file)
|
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
|
'''An Android project template, containing recipe stuff for
|
||||||
compilation and templated fields for APK info.
|
compilation and templated fields for APK info.
|
||||||
'''
|
'''
|
||||||
|
@ -45,15 +77,11 @@ class Bootstrap(object):
|
||||||
bootstrap_dir = None
|
bootstrap_dir = None
|
||||||
|
|
||||||
build_dir = None
|
build_dir = None
|
||||||
dist_dir = None
|
|
||||||
dist_name = None
|
dist_name = None
|
||||||
distribution = None
|
distribution = None
|
||||||
|
|
||||||
# All bootstraps should include Python in some way:
|
# All bootstraps should include Python in some way:
|
||||||
recipe_depends = [
|
recipe_depends = ['python3', 'android']
|
||||||
("python2", "python2legacy", "python3", "python3crystax"),
|
|
||||||
'android',
|
|
||||||
]
|
|
||||||
|
|
||||||
can_be_chosen_automatically = True
|
can_be_chosen_automatically = True
|
||||||
'''Determines whether the bootstrap can be chosen as one that
|
'''Determines whether the bootstrap can be chosen as one that
|
||||||
|
@ -70,9 +98,9 @@ class Bootstrap(object):
|
||||||
def dist_dir(self):
|
def dist_dir(self):
|
||||||
'''The dist dir at which to place the finished distribution.'''
|
'''The dist dir at which to place the finished distribution.'''
|
||||||
if self.distribution is None:
|
if self.distribution is None:
|
||||||
warning('Tried to access {}.dist_dir, but {}.distribution '
|
raise BuildInterruptingException(
|
||||||
'is None'.format(self, self))
|
'Internal error: tried to access {}.dist_dir, but {}.distribution '
|
||||||
exit(1)
|
'is None'.format(self, self))
|
||||||
return self.distribution.dist_dir
|
return self.distribution.dist_dir
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -84,7 +112,7 @@ class Bootstrap(object):
|
||||||
and optional dependencies are being used,
|
and optional dependencies are being used,
|
||||||
and returns a list of these.'''
|
and returns a list of these.'''
|
||||||
recipes = []
|
recipes = []
|
||||||
built_recipes = self.ctx.recipe_build_order
|
built_recipes = self.ctx.recipe_build_order or []
|
||||||
for recipe in self.recipe_depends:
|
for recipe in self.recipe_depends:
|
||||||
if isinstance(recipe, (tuple, list)):
|
if isinstance(recipe, (tuple, list)):
|
||||||
for alternative in recipe:
|
for alternative in recipe:
|
||||||
|
@ -104,70 +132,102 @@ class Bootstrap(object):
|
||||||
def get_dist_dir(self, name):
|
def get_dist_dir(self, name):
|
||||||
return join(self.ctx.dist_dir, name)
|
return join(self.ctx.dist_dir, name)
|
||||||
|
|
||||||
def get_common_dir(self):
|
|
||||||
return os.path.abspath(join(self.bootstrap_dir, "..", 'common'))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
modname = self.__class__.__module__
|
modname = self.__class__.__module__
|
||||||
return modname.split(".", 2)[-1]
|
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):
|
def prepare_build_dir(self):
|
||||||
'''Ensure that a build dir exists for the recipe. This same single
|
"""Ensure that a build dir exists for the recipe. This same single
|
||||||
dir will be used for building all different archs.'''
|
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.build_dir = self.get_build_dir()
|
||||||
self.common_dir = self.get_common_dir()
|
for bootstrap_dir in bootstrap_dirs:
|
||||||
copy_files(join(self.bootstrap_dir, 'build'), self.build_dir)
|
copy_files(join(bootstrap_dir, 'build'), self.build_dir, symlink=self.ctx.symlink_bootstrap_files)
|
||||||
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'))
|
|
||||||
with current_directory(self.build_dir):
|
with current_directory(self.build_dir):
|
||||||
with open('project.properties', 'w') as fileh:
|
with open('project.properties', 'w') as fileh:
|
||||||
fileh.write('target=android-{}'.format(self.ctx.android_api))
|
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)
|
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)
|
self.distribution.save_info(self.dist_dir)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list_bootstraps(cls):
|
def all_bootstraps(cls):
|
||||||
'''Find all the available bootstraps and return them.'''
|
'''Find all the available bootstraps and return them.'''
|
||||||
forbidden_dirs = ('__pycache__', 'common')
|
forbidden_dirs = ('__pycache__', 'common')
|
||||||
bootstraps_dir = join(dirname(__file__), 'bootstraps')
|
bootstraps_dir = join(dirname(__file__), 'bootstraps')
|
||||||
|
result = set()
|
||||||
for name in listdir(bootstraps_dir):
|
for name in listdir(bootstraps_dir):
|
||||||
if name in forbidden_dirs:
|
if name in forbidden_dirs:
|
||||||
continue
|
continue
|
||||||
filen = join(bootstraps_dir, name)
|
filen = join(bootstraps_dir, name)
|
||||||
if isdir(filen):
|
if isdir(filen):
|
||||||
yield name
|
result.add(name)
|
||||||
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_bootstrap_from_recipes(cls, recipes, ctx):
|
def get_usable_bootstraps_for_recipes(cls, recipes, ctx):
|
||||||
'''Returns a bootstrap whose recipe requirements do not conflict with
|
'''Returns all bootstrap whose recipe requirements do not conflict
|
||||||
the given recipes.'''
|
with the given recipes, in no particular order.'''
|
||||||
info('Trying to find a bootstrap that matches the given recipes.')
|
info('Trying to find a bootstrap that matches the given recipes.')
|
||||||
bootstraps = [cls.get_bootstrap(name, ctx)
|
bootstraps = [cls.get_bootstrap(name, ctx)
|
||||||
for name in cls.list_bootstraps()]
|
for name in cls.all_bootstraps()]
|
||||||
acceptable_bootstraps = []
|
acceptable_bootstraps = set()
|
||||||
|
|
||||||
|
# Find out which bootstraps are acceptable:
|
||||||
for bs in bootstraps:
|
for bs in bootstraps:
|
||||||
if not bs.can_be_chosen_automatically:
|
if not bs.can_be_chosen_automatically:
|
||||||
continue
|
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:
|
for possible_dependencies in possible_dependency_lists:
|
||||||
ok = True
|
ok = True
|
||||||
|
# Check if the bootstap's dependencies have an internal conflict:
|
||||||
for recipe in possible_dependencies:
|
for recipe in possible_dependencies:
|
||||||
recipe = Recipe.get_recipe(recipe, ctx)
|
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
|
ok = False
|
||||||
break
|
break
|
||||||
|
# Check if bootstrap's dependencies conflict with chosen
|
||||||
|
# packages:
|
||||||
for recipe in recipes:
|
for recipe in recipes:
|
||||||
try:
|
try:
|
||||||
recipe = Recipe.get_recipe(recipe, ctx)
|
recipe = Recipe.get_recipe(recipe, ctx)
|
||||||
|
@ -175,19 +235,63 @@ class Bootstrap(object):
|
||||||
conflicts = []
|
conflicts = []
|
||||||
else:
|
else:
|
||||||
conflicts = recipe.conflicts
|
conflicts = recipe.conflicts
|
||||||
if any([conflict in possible_dependencies
|
if any(conflict in possible_dependencies
|
||||||
for conflict in conflicts]):
|
for conflict in conflicts):
|
||||||
ok = False
|
ok = False
|
||||||
break
|
break
|
||||||
if ok and bs not in acceptable_bootstraps:
|
if ok and bs not in acceptable_bootstraps:
|
||||||
acceptable_bootstraps.append(bs)
|
acceptable_bootstraps.add(bs)
|
||||||
|
|
||||||
info('Found {} acceptable bootstraps: {}'.format(
|
info('Found {} acceptable bootstraps: {}'.format(
|
||||||
len(acceptable_bootstraps),
|
len(acceptable_bootstraps),
|
||||||
[bs.name for bs in acceptable_bootstraps]))
|
[bs.name for bs in acceptable_bootstraps]))
|
||||||
if acceptable_bootstraps:
|
return acceptable_bootstraps
|
||||||
info('Using the first of these: {}'
|
|
||||||
.format(acceptable_bootstraps[0].name))
|
@classmethod
|
||||||
return acceptable_bootstraps[0]
|
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
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -218,15 +322,16 @@ class Bootstrap(object):
|
||||||
tgt_dir = join(dest_dir, arch.arch)
|
tgt_dir = join(dest_dir, arch.arch)
|
||||||
ensure_dir(tgt_dir)
|
ensure_dir(tgt_dir)
|
||||||
for src_dir in src_dirs:
|
for src_dir in src_dirs:
|
||||||
for lib in glob.glob(join(src_dir, wildcard)):
|
libs = glob.glob(join(src_dir, wildcard))
|
||||||
shprint(sh.cp, '-a', lib, tgt_dir)
|
if libs:
|
||||||
|
shprint(sh.cp, '-a', *libs, tgt_dir)
|
||||||
|
|
||||||
def distribute_javaclasses(self, javaclass_dir, dest_dir="src"):
|
def distribute_javaclasses(self, javaclass_dir, dest_dir="src"):
|
||||||
'''Copy existing javaclasses from build dir to current dist dir.'''
|
'''Copy existing javaclasses from build dir to current dist dir.'''
|
||||||
info('Copying java files')
|
info('Copying java files')
|
||||||
ensure_dir(dest_dir)
|
ensure_dir(dest_dir)
|
||||||
for filename in glob.glob(javaclass_dir):
|
filenames = glob.glob(javaclass_dir)
|
||||||
shprint(sh.cp, '-a', filename, dest_dir)
|
shprint(sh.cp, '-a', *filenames, dest_dir)
|
||||||
|
|
||||||
def distribute_aars(self, arch):
|
def distribute_aars(self, arch):
|
||||||
'''Process existing .aar bundles and copy to current dist dir.'''
|
'''Process existing .aar bundles and copy to current dist dir.'''
|
||||||
|
@ -259,24 +364,18 @@ class Bootstrap(object):
|
||||||
debug(" to {}".format(so_tgt_dir))
|
debug(" to {}".format(so_tgt_dir))
|
||||||
ensure_dir(so_tgt_dir)
|
ensure_dir(so_tgt_dir)
|
||||||
so_files = glob.glob(join(so_src_dir, '*.so'))
|
so_files = glob.glob(join(so_src_dir, '*.so'))
|
||||||
for f in so_files:
|
shprint(sh.cp, '-a', *so_files, so_tgt_dir)
|
||||||
shprint(sh.cp, '-a', f, so_tgt_dir)
|
|
||||||
|
|
||||||
def strip_libraries(self, arch):
|
def strip_libraries(self, arch):
|
||||||
info('Stripping libraries')
|
info('Stripping libraries')
|
||||||
if self.ctx.python_recipe.from_crystax:
|
|
||||||
info('Python was loaded from CrystaX, skipping strip')
|
|
||||||
return
|
|
||||||
env = arch.get_env()
|
env = arch.get_env()
|
||||||
tokens = shlex.split(env['STRIP'])
|
tokens = shlex.split(env['STRIP'])
|
||||||
strip = sh.Command(tokens[0])
|
strip = sh.Command(tokens[0])
|
||||||
if len(tokens) > 1:
|
if len(tokens) > 1:
|
||||||
strip = strip.bake(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')
|
'_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'),
|
filens = shprint(sh.find, libs_dir, join(self.dist_dir, 'libs'),
|
||||||
'-iname', '*.so', _env=env).stdout.decode('utf-8')
|
'-iname', '*.so', _env=env).stdout.decode('utf-8')
|
||||||
|
|
||||||
|
@ -301,9 +400,31 @@ class Bootstrap(object):
|
||||||
shprint(sh.rm, '-rf', d)
|
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 = [[]]
|
recipe_lists = [[]]
|
||||||
for recipe in recipes:
|
for recipe in recipes_with_deps:
|
||||||
if isinstance(recipe, (tuple, list)):
|
if isinstance(recipe, (tuple, list)):
|
||||||
new_recipe_lists = []
|
new_recipe_lists = []
|
||||||
for alternative in recipe:
|
for alternative in recipe:
|
||||||
|
@ -313,6 +434,6 @@ def expand_dependencies(recipes):
|
||||||
new_recipe_lists.append(new_list)
|
new_recipe_lists.append(new_list)
|
||||||
recipe_lists = new_recipe_lists
|
recipe_lists = new_recipe_lists
|
||||||
else:
|
else:
|
||||||
for old_list in recipe_lists:
|
for existing_list in recipe_lists:
|
||||||
old_list.append(recipe)
|
existing_list.append(recipe)
|
||||||
return recipe_lists
|
return recipe_lists
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
#!/usr/bin/env python2.7
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
|
from gzip import GzipFile
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from os.path import (
|
from os.path import (
|
||||||
dirname, join, isfile, realpath,
|
dirname, join, isfile, realpath,
|
||||||
relpath, split, exists, basename
|
relpath, split, exists, basename
|
||||||
)
|
)
|
||||||
from os import listdir, makedirs, remove
|
from os import environ, listdir, makedirs, remove
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -16,19 +16,20 @@ import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from zipfile import ZipFile
|
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
|
|
||||||
def get_dist_info_for(key):
|
def get_dist_info_for(key, error_if_missing=True):
|
||||||
try:
|
try:
|
||||||
with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
|
with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
|
||||||
info = json.load(fileh)
|
info = json.load(fileh)
|
||||||
value = str(info[key])
|
value = info[key]
|
||||||
except (OSError, KeyError) as e:
|
except (OSError, KeyError) as e:
|
||||||
|
if not error_if_missing:
|
||||||
|
return None
|
||||||
print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
|
print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
|
||||||
"from dist_info.json: " + str(e))
|
"from dist_info.json: " + str(e))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -39,10 +40,6 @@ def get_hostpython():
|
||||||
return get_dist_info_for('hostpython')
|
return get_dist_info_for('hostpython')
|
||||||
|
|
||||||
|
|
||||||
def get_python_version():
|
|
||||||
return get_dist_info_for('python_version')
|
|
||||||
|
|
||||||
|
|
||||||
def get_bootstrap_name():
|
def get_bootstrap_name():
|
||||||
return get_dist_info_for('bootstrap')
|
return get_dist_info_for('bootstrap')
|
||||||
|
|
||||||
|
@ -57,7 +54,6 @@ else:
|
||||||
curdir = dirname(__file__)
|
curdir = dirname(__file__)
|
||||||
|
|
||||||
PYTHON = get_hostpython()
|
PYTHON = get_hostpython()
|
||||||
PYTHON_VERSION = get_python_version()
|
|
||||||
if PYTHON is not None and not exists(PYTHON):
|
if PYTHON is not None and not exists(PYTHON):
|
||||||
PYTHON = None
|
PYTHON = None
|
||||||
|
|
||||||
|
@ -72,29 +68,23 @@ BLACKLIST_PATTERNS = [
|
||||||
'~',
|
'~',
|
||||||
'*.bak',
|
'*.bak',
|
||||||
'*.swp',
|
'*.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 = []
|
WHITELIST_PATTERNS = []
|
||||||
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
|
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
|
||||||
WHITELIST_PATTERNS.append('pyconfig.h')
|
WHITELIST_PATTERNS.append('pyconfig.h')
|
||||||
|
|
||||||
python_files = []
|
|
||||||
|
|
||||||
|
|
||||||
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
|
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
|
||||||
join(curdir, 'templates')))
|
join(curdir, 'templates')))
|
||||||
|
|
||||||
|
|
||||||
def try_unlink(fn):
|
DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity'
|
||||||
if exists(fn):
|
DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService'
|
||||||
os.unlink(fn)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_dir(path):
|
def ensure_dir(path):
|
||||||
|
@ -154,75 +144,33 @@ def listfiles(d):
|
||||||
yield fn
|
yield fn
|
||||||
|
|
||||||
|
|
||||||
def make_python_zip():
|
def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True):
|
||||||
'''
|
|
||||||
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):
|
|
||||||
'''
|
'''
|
||||||
Make a zip file `fn` from the contents of source_dis.
|
Make a zip file `fn` from the contents of source_dis.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# selector function
|
def clean(tinfo):
|
||||||
def select(fn):
|
"""cleaning function (for reproducible builds)"""
|
||||||
rfn = realpath(fn)
|
tinfo.uid = tinfo.gid = 0
|
||||||
for p in ignore_path:
|
tinfo.uname = tinfo.gname = ''
|
||||||
if p.endswith('/'):
|
tinfo.mtime = 0
|
||||||
p = p[:-1]
|
return tinfo
|
||||||
if rfn.startswith(p):
|
|
||||||
return False
|
|
||||||
if rfn in python_files:
|
|
||||||
return False
|
|
||||||
return not is_blacklist(fn)
|
|
||||||
|
|
||||||
# get the files and relpath file of all the directory we asked for
|
# get the files and relpath file of all the directory we asked for
|
||||||
files = []
|
files = []
|
||||||
for sd in source_dirs:
|
for sd in source_dirs:
|
||||||
sd = realpath(sd)
|
sd = realpath(sd)
|
||||||
compile_dir(sd, optimize_python=optimize_python)
|
for fn in listfiles(sd):
|
||||||
files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd)
|
if is_blacklist(fn):
|
||||||
if select(x)]
|
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
|
# 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 = []
|
dirs = []
|
||||||
for fn, afn in files:
|
for fn, afn in files:
|
||||||
dn = dirname(afn)
|
dn = dirname(afn)
|
||||||
|
@ -238,25 +186,24 @@ def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True):
|
||||||
dirs.append(d)
|
dirs.append(d)
|
||||||
tinfo = tarfile.TarInfo(d)
|
tinfo = tarfile.TarInfo(d)
|
||||||
tinfo.type = tarfile.DIRTYPE
|
tinfo.type = tarfile.DIRTYPE
|
||||||
|
clean(tinfo)
|
||||||
tf.addfile(tinfo)
|
tf.addfile(tinfo)
|
||||||
|
|
||||||
# put the file
|
# put the file
|
||||||
tf.add(fn, afn)
|
tf.add(fn, afn, filter=clean)
|
||||||
tf.close()
|
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:
|
if PYTHON is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if int(PYTHON_VERSION[0]) >= 3:
|
args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file]
|
||||||
args = [PYTHON, '-m', 'compileall', '-b', '-f', dfn]
|
|
||||||
else:
|
|
||||||
args = [PYTHON, '-m', 'compileall', '-f', dfn]
|
|
||||||
if optimize_python:
|
if optimize_python:
|
||||||
# -OO = strip docstrings
|
# -OO = strip docstrings
|
||||||
args.insert(1, '-OO')
|
args.insert(1, '-OO')
|
||||||
|
@ -268,16 +215,18 @@ def compile_dir(dfn, optimize_python=True):
|
||||||
'error, see logs above')
|
'error, see logs above')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
return ".".join([os.path.splitext(python_file)[0], "pyc"])
|
||||||
|
|
||||||
|
|
||||||
def make_package(args):
|
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 \
|
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)
|
# (webview doesn't need an entrypoint, apparently)
|
||||||
if args.private is None or (
|
if args.private is None or (
|
||||||
not exists(join(realpath(args.private), 'main.py')) and
|
not exists(join(realpath(args.private), 'main.py')) and
|
||||||
not exists(join(realpath(args.private), 'main.pyo'))):
|
not exists(join(realpath(args.private), 'main.pyc'))):
|
||||||
print('''BUILD FAILURE: No main.py(o) found in your app directory. This
|
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
|
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
|
started by a file with a different name, rename it to main.py or add a
|
||||||
main.py that loads it.''')
|
main.py that loads it.''')
|
||||||
|
@ -286,53 +235,159 @@ main.py that loads it.''')
|
||||||
assets_dir = "src/main/assets"
|
assets_dir = "src/main/assets"
|
||||||
|
|
||||||
# Delete the old assets.
|
# Delete the old assets.
|
||||||
try_unlink(join(assets_dir, 'public.mp3'))
|
shutil.rmtree(assets_dir, ignore_errors=True)
|
||||||
try_unlink(join(assets_dir, 'private.mp3'))
|
|
||||||
ensure_dir(assets_dir)
|
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:
|
# Add extra environment variable file into tar-able directory:
|
||||||
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
|
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
|
||||||
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
|
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
|
||||||
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
|
if hasattr(args, "window"):
|
||||||
|
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
|
||||||
if hasattr(args, "orientation"):
|
if hasattr(args, "orientation"):
|
||||||
f.write("P4A_ORIENTATION=" + str(args.orientation) + "\n")
|
f.write("P4A_ORIENTATION=" + str(args.orientation) + "\n")
|
||||||
f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
|
f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
|
||||||
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
|
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
|
||||||
|
|
||||||
# Package up the private data (public not supported).
|
# Package up the private data (public not supported).
|
||||||
tar_dirs = [env_vars_tarpath]
|
use_setup_py = get_dist_info_for("use_setup_py",
|
||||||
if args.private:
|
error_if_missing=False) is True
|
||||||
tar_dirs.append(args.private)
|
private_tar_dirs = [env_vars_tarpath]
|
||||||
for python_bundle_dir in ('private', 'crystax_python', '_python_bundle'):
|
_temp_dirs_to_clean = []
|
||||||
if exists(python_bundle_dir):
|
try:
|
||||||
tar_dirs.append(python_bundle_dir)
|
if args.private:
|
||||||
if get_bootstrap_name() == "webview":
|
if not use_setup_py or (
|
||||||
tar_dirs.append('webview_includes')
|
not exists(join(args.private, "setup.py")) and
|
||||||
if args.private or args.launcher:
|
not exists(join(args.private, "pyproject.toml"))
|
||||||
make_tar(
|
):
|
||||||
join(assets_dir, 'private.mp3'), tar_dirs, args.ignore_path,
|
print('No setup.py/pyproject.toml used, copying '
|
||||||
optimize_python=args.optimize_python)
|
'full private data into .apk.')
|
||||||
|
private_tar_dirs.append(args.private)
|
||||||
|
else:
|
||||||
|
print("Copying main.py's ONLY, since other app data is "
|
||||||
|
"expected in site-packages.")
|
||||||
|
main_py_only_dir = tempfile.mkdtemp()
|
||||||
|
_temp_dirs_to_clean.append(main_py_only_dir)
|
||||||
|
|
||||||
|
# Check all main.py files we need to copy:
|
||||||
|
copy_paths = ["main.py", join("service", "main.py")]
|
||||||
|
for copy_path in copy_paths:
|
||||||
|
variants = [
|
||||||
|
copy_path,
|
||||||
|
copy_path.partition(".")[0] + ".pyc",
|
||||||
|
]
|
||||||
|
# Check in all variants with all possible endings:
|
||||||
|
for variant in variants:
|
||||||
|
if exists(join(args.private, variant)):
|
||||||
|
# Make sure surrounding directly exists:
|
||||||
|
dir_path = os.path.dirname(variant)
|
||||||
|
if (len(dir_path) > 0 and
|
||||||
|
not exists(
|
||||||
|
join(main_py_only_dir, dir_path)
|
||||||
|
)):
|
||||||
|
os.mkdir(join(main_py_only_dir, dir_path))
|
||||||
|
# Copy actual file:
|
||||||
|
shutil.copyfile(
|
||||||
|
join(args.private, variant),
|
||||||
|
join(main_py_only_dir, variant),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Append directory with all main.py's to result apk paths:
|
||||||
|
private_tar_dirs.append(main_py_only_dir)
|
||||||
|
if get_bootstrap_name() == "webview":
|
||||||
|
for asset in listdir('webview_includes'):
|
||||||
|
shutil.copy(join('webview_includes', asset), join(assets_dir, asset))
|
||||||
|
|
||||||
|
for asset in args.assets:
|
||||||
|
asset_src, asset_dest = asset.split(":")
|
||||||
|
if isfile(realpath(asset_src)):
|
||||||
|
ensure_dir(dirname(join(assets_dir, asset_dest)))
|
||||||
|
shutil.copy(realpath(asset_src), join(assets_dir, asset_dest))
|
||||||
|
else:
|
||||||
|
shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest))
|
||||||
|
|
||||||
|
if args.private or args.launcher:
|
||||||
|
for arch in get_dist_info_for("archs"):
|
||||||
|
libs_dir = f"libs/{arch}"
|
||||||
|
make_tar(
|
||||||
|
join(libs_dir, "libpybundle.so"),
|
||||||
|
[f"_python_bundle__{arch}"],
|
||||||
|
byte_compile_python=args.byte_compile_python,
|
||||||
|
optimize_python=args.optimize_python,
|
||||||
|
)
|
||||||
|
make_tar(
|
||||||
|
join(assets_dir, "private.tar"),
|
||||||
|
private_tar_dirs,
|
||||||
|
byte_compile_python=args.byte_compile_python,
|
||||||
|
optimize_python=args.optimize_python,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
for directory in _temp_dirs_to_clean:
|
||||||
|
shutil.rmtree(directory)
|
||||||
|
|
||||||
# Remove extra env vars tar-able directory:
|
# Remove extra env vars tar-able directory:
|
||||||
shutil.rmtree(env_vars_tarpath)
|
shutil.rmtree(env_vars_tarpath)
|
||||||
|
|
||||||
# Prepare some variables for templating process
|
# Prepare some variables for templating process
|
||||||
res_dir = "src/main/res"
|
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_icon = 'templates/kivy-icon.png'
|
||||||
default_presplash = 'templates/kivy-presplash.jpg'
|
default_presplash = 'templates/kivy-presplash.jpg'
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
args.icon or default_icon,
|
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":
|
if get_bootstrap_name() != "service_only":
|
||||||
shutil.copy(
|
lottie_splashscreen = join(res_dir, 'raw/splashscreen.json')
|
||||||
args.presplash or default_presplash,
|
if args.presplash_lottie:
|
||||||
join(res_dir, 'drawable/presplash.jpg')
|
shutil.copy(
|
||||||
)
|
'templates/lottie.xml',
|
||||||
|
join(res_dir, 'layout/lottie.xml')
|
||||||
|
)
|
||||||
|
ensure_dir(join(res_dir, 'raw'))
|
||||||
|
shutil.copy(
|
||||||
|
args.presplash_lottie,
|
||||||
|
join(res_dir, 'raw/splashscreen.json')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if exists(lottie_splashscreen):
|
||||||
|
remove(lottie_splashscreen)
|
||||||
|
remove(join(res_dir, 'layout/lottie.xml'))
|
||||||
|
|
||||||
|
shutil.copy(
|
||||||
|
args.presplash or default_presplash,
|
||||||
|
join(res_dir, 'drawable/presplash.jpg')
|
||||||
|
)
|
||||||
|
|
||||||
# If extra Java jars were requested, copy them into the libs directory
|
# If extra Java jars were requested, copy them into the libs directory
|
||||||
jars = []
|
jars = []
|
||||||
|
@ -360,17 +415,17 @@ main.py that loads it.''')
|
||||||
|
|
||||||
version_code = 0
|
version_code = 0
|
||||||
if not args.numeric_version:
|
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:
|
Set version code in format (10 + minsdk + app_version)
|
||||||
dist_data = json.load(dist_info)
|
Historically versioning was (arch + minsdk + app_version),
|
||||||
arch = dist_data["archs"][0]
|
with arch expressed with a single digit from 6 to 9.
|
||||||
arch_dict = {"x86_64": "9", "arm64-v8a": "8", "armeabi-v7a": "7", "x86": "6"}
|
Since the multi-arch support, has been changed to 10.
|
||||||
arch_code = arch_dict.get(arch, '1')
|
"""
|
||||||
min_sdk = args.min_sdk_version
|
min_sdk = args.min_sdk_version
|
||||||
for i in args.version.split('.'):
|
for i in args.version.split('.'):
|
||||||
version_code *= 100
|
version_code *= 100
|
||||||
version_code += int(i)
|
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:
|
if args.intent_filters:
|
||||||
with open(args.intent_filters) as fd:
|
with open(args.intent_filters) as fd:
|
||||||
|
@ -387,6 +442,9 @@ main.py that loads it.''')
|
||||||
for spec in args.extra_source_dirs:
|
for spec in args.extra_source_dirs:
|
||||||
if ':' in spec:
|
if ':' in spec:
|
||||||
specdir, specincludes = spec.split(':')
|
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:
|
else:
|
||||||
specdir = spec
|
specdir = spec
|
||||||
specincludes = '**'
|
specincludes = '**'
|
||||||
|
@ -402,6 +460,7 @@ main.py that loads it.''')
|
||||||
service = True
|
service = True
|
||||||
|
|
||||||
service_names = []
|
service_names = []
|
||||||
|
base_service_class = args.service_class_name.split('.')[-1]
|
||||||
for sid, spec in enumerate(args.services):
|
for sid, spec in enumerate(args.services):
|
||||||
spec = spec.split(':')
|
spec = spec.split(':')
|
||||||
name = spec[0]
|
name = spec[0]
|
||||||
|
@ -426,6 +485,7 @@ main.py that loads it.''')
|
||||||
foreground=foreground,
|
foreground=foreground,
|
||||||
sticky=sticky,
|
sticky=sticky,
|
||||||
service_id=sid + 1,
|
service_id=sid + 1,
|
||||||
|
base_service_class=base_service_class,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find the SDK directory and target API
|
# 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
|
# Try to build with the newest available build tools
|
||||||
ignored = {".DS_Store", ".ds_store"}
|
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 = [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]
|
build_tools_version = build_tools_versions[-1]
|
||||||
|
|
||||||
# Folder name for launcher (used by SDL2 bootstrap)
|
# Folder name for launcher (used by SDL2 bootstrap)
|
||||||
url_scheme = 'kivy'
|
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:
|
# Render out android manifest:
|
||||||
manifest_path = "src/main/AndroidManifest.xml"
|
manifest_path = "src/main/AndroidManifest.xml"
|
||||||
render_args = {
|
render_args = {
|
||||||
"args": args,
|
"args": args,
|
||||||
"service": service,
|
"service": service,
|
||||||
"service_names": service_names,
|
"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":
|
if get_bootstrap_name() == "sdl2":
|
||||||
render_args["url_scheme"] = url_scheme
|
render_args["url_scheme"] = url_scheme
|
||||||
|
@ -482,9 +560,17 @@ main.py that loads it.''')
|
||||||
aars=aars,
|
aars=aars,
|
||||||
jars=jars,
|
jars=jars,
|
||||||
android_api=android_api,
|
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
|
# ant build templates
|
||||||
render(
|
render(
|
||||||
'build.tmpl.xml',
|
'build.tmpl.xml',
|
||||||
|
@ -493,9 +579,18 @@ main.py that loads it.''')
|
||||||
versioned_name=versioned_name)
|
versioned_name=versioned_name)
|
||||||
|
|
||||||
# String resources:
|
# 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 = {
|
render_args = {
|
||||||
"args": args,
|
"args": args,
|
||||||
"private_version": str(time.time())
|
"private_version": hashlib.sha1(private_version.encode()).hexdigest()
|
||||||
}
|
}
|
||||||
if get_bootstrap_name() == "sdl2":
|
if get_bootstrap_name() == "sdl2":
|
||||||
render_args["url_scheme"] = url_scheme
|
render_args["url_scheme"] = url_scheme
|
||||||
|
@ -527,27 +622,31 @@ main.py that loads it.''')
|
||||||
for patch_name in os.listdir(join('src', 'patches')):
|
for patch_name in os.listdir(join('src', 'patches')):
|
||||||
patch_path = join('src', 'patches', patch_name)
|
patch_path = join('src', 'patches', patch_name)
|
||||||
print("Applying patch: " + str(patch_path))
|
print("Applying patch: " + str(patch_path))
|
||||||
|
|
||||||
|
# -N: insist this is FORWARD patch, don't reverse apply
|
||||||
|
# -p1: strip first path component
|
||||||
|
# -t: batch mode, don't ask questions
|
||||||
|
patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_output([
|
# Use a dry run to establish whether the patch is already applied.
|
||||||
# -N: insist this is FORWARd patch, don't reverse apply
|
# If we don't check this, the patch may be partially applied (which is bad!)
|
||||||
# -p1: strip first path component
|
subprocess.check_output(patch_command + ["--dry-run"])
|
||||||
# -t: batch mode, don't ask questions
|
|
||||||
"patch", "-N", "-p1", "-t", "-i", patch_path
|
|
||||||
])
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if e.returncode == 1:
|
if e.returncode == 1:
|
||||||
# Return code 1 means it didn't apply, this will
|
# Return code 1 means not all hunks could be applied, this usually
|
||||||
# usually mean it is already applied.
|
# means the patch is already applied.
|
||||||
print("Warning: failed to apply patch (" +
|
print("Warning: failed to apply patch (exit code 1), "
|
||||||
"exit code 1), " +
|
"assuming it is already applied: ",
|
||||||
"assuming it is already applied: " +
|
str(patch_path))
|
||||||
str(patch_path)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise e
|
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
|
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
|
||||||
|
|
||||||
# Get the default minsdk, equal to the NDK API that this dist is built against
|
# 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')
|
help='Custom key=value to add in application metadata')
|
||||||
ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
|
ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
|
||||||
help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
|
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',
|
ap.add_argument('--icon', dest='icon',
|
||||||
help=('A png file to use as the icon for '
|
help=('A png file to use as the icon for '
|
||||||
'the application.'))
|
'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=[],
|
ap.add_argument('--service', dest='services', action='append', default=[],
|
||||||
help='Declare a new service entrypoint: '
|
help='Declare a new service entrypoint: '
|
||||||
'NAME:PATH_TO_PY[:foreground]')
|
'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":
|
if get_bootstrap_name() != "service_only":
|
||||||
ap.add_argument('--presplash', dest='presplash',
|
ap.add_argument('--presplash', dest='presplash',
|
||||||
help=('A jpeg file to use as a screen while the '
|
help=('A jpeg file to use as a screen while the '
|
||||||
'application is loading.'))
|
'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',
|
ap.add_argument('--presplash-color',
|
||||||
dest='presplash_color',
|
dest='presplash_color',
|
||||||
default='#000000',
|
default='#000000',
|
||||||
|
@ -636,6 +755,28 @@ tools directory of the Android SDK.
|
||||||
'https://developer.android.com/guide/'
|
'https://developer.android.com/guide/'
|
||||||
'topics/manifest/'
|
'topics/manifest/'
|
||||||
'activity-element.html'))
|
'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',
|
ap.add_argument('--wakelock', dest='wakelock', action='store_true',
|
||||||
help=('Indicate if the application needs the device '
|
help=('Indicate if the application needs the device '
|
||||||
'to stay on'))
|
'to stay on'))
|
||||||
|
@ -647,6 +788,13 @@ tools directory of the Android SDK.
|
||||||
default=join(curdir, 'whitelist.txt'),
|
default=join(curdir, 'whitelist.txt'),
|
||||||
help=('Use a whitelist file to prevent blacklisting of '
|
help=('Use a whitelist file to prevent blacklisting of '
|
||||||
'file in the final APK'))
|
'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',
|
ap.add_argument('--add-jar', dest='add_jar', action='append',
|
||||||
help=('Add a Java .jar to the libs, so you can access its '
|
help=('Add a Java .jar to the libs, so you can access its '
|
||||||
'classes with pyjnius. You can specify this '
|
'classes with pyjnius. You can specify this '
|
||||||
|
@ -674,6 +822,8 @@ tools directory of the Android SDK.
|
||||||
'filename containing xml. The filename should be '
|
'filename containing xml. The filename should be '
|
||||||
'located relative to the python-for-android '
|
'located relative to the python-for-android '
|
||||||
'directory'))
|
'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',
|
ap.add_argument('--with-billing', dest='billing_pubkey',
|
||||||
help='If set, the billing service will be added (not implemented)')
|
help='If set, the billing service will be added (not implemented)')
|
||||||
ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
|
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',
|
ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Use the system python during compileall if possible.')
|
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',
|
ap.add_argument('--sign', action='store_true',
|
||||||
help=('Try to sign the APK with your credentials. You must set '
|
help=('Try to sign the APK with your credentials. You must set '
|
||||||
'the appropriate environment variables.'))
|
'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.')
|
help='Set the launch mode of the main activity in the manifest.')
|
||||||
ap.add_argument('--allow-backup', dest='allow_backup', default='true',
|
ap.add_argument('--allow-backup', dest='allow_backup', default='true',
|
||||||
help="if set to 'false', then android won't backup the application.")
|
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',
|
ap.add_argument('--no-optimize-python', dest='optimize_python',
|
||||||
action='store_false', default=True,
|
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)'))
|
'(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:
|
# Put together arguments, and add those from .p4a config file:
|
||||||
if args is None:
|
if args is None:
|
||||||
|
@ -721,7 +892,6 @@ tools directory of the Android SDK.
|
||||||
_read_configuration()
|
_read_configuration()
|
||||||
|
|
||||||
args = ap.parse_args(args)
|
args = ap.parse_args(args)
|
||||||
args.ignore_path = []
|
|
||||||
|
|
||||||
if args.name and args.name[0] == '"' and args.name[-1] == '"':
|
if args.name and args.name[0] == '"' and args.name[-1] == '"':
|
||||||
args.name = args.name[1:-1]
|
args.name = args.name[1:-1]
|
||||||
|
@ -751,21 +921,19 @@ tools directory of the Android SDK.
|
||||||
if args.permissions and isinstance(args.permissions[0], list):
|
if args.permissions and isinstance(args.permissions[0], list):
|
||||||
args.permissions = [p for perm in args.permissions for p in perm]
|
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:
|
if args.try_system_python_compile:
|
||||||
# Hardcoding python2.7 is okay for now, as python3 skips the
|
# Hardcoding python2.7 is okay for now, as python3 skips the
|
||||||
# compilation anyway
|
# compilation anyway
|
||||||
if not exists('crystax_python'):
|
python_executable = 'python2.7'
|
||||||
python_executable = 'python2.7'
|
try:
|
||||||
try:
|
subprocess.call([python_executable, '--version'])
|
||||||
subprocess.call([python_executable, '--version'])
|
except (OSError, subprocess.CalledProcessError):
|
||||||
except (OSError, subprocess.CalledProcessError):
|
pass
|
||||||
pass
|
else:
|
||||||
else:
|
PYTHON = python_executable
|
||||||
PYTHON = python_executable
|
|
||||||
|
|
||||||
if args.no_compile_pyo:
|
|
||||||
PYTHON = None
|
|
||||||
BLACKLIST_PATTERNS.remove('*.py')
|
|
||||||
|
|
||||||
if args.blacklist:
|
if args.blacklist:
|
||||||
with open(args.blacklist) as fd:
|
with open(args.blacklist) as fd:
|
||||||
|
@ -791,4 +959,4 @@ tools directory of the Android SDK.
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parse_args()
|
parse_args_and_make_package()
|
||||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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)
|
LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS)
|
||||||
|
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
|
|
||||||
ifdef CRYSTAX_PYTHON_VERSION
|
|
||||||
$(call import-module,python/$(CRYSTAX_PYTHON_VERSION))
|
|
||||||
endif
|
|
||||||
|
|
|
@ -15,15 +15,11 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
#include "bootstrap_name.h"
|
#include "bootstrap_name.h"
|
||||||
|
|
||||||
#ifndef BOOTSTRAP_USES_NO_SDL_HEADERS
|
#ifndef BOOTSTRAP_USES_NO_SDL_HEADERS
|
||||||
#include "SDL.h"
|
#include "SDL.h"
|
||||||
#ifndef BOOTSTRAP_NAME_PYGAME
|
|
||||||
#include "SDL_opengles2.h"
|
#include "SDL_opengles2.h"
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
#ifdef BOOTSTRAP_NAME_PYGAME
|
|
||||||
#include "jniwrapperstuff.h"
|
|
||||||
#endif
|
|
||||||
#include "android/log.h"
|
#include "android/log.h"
|
||||||
|
|
||||||
#define ENTRYPOINT_MAXLEN 128
|
#define ENTRYPOINT_MAXLEN 128
|
||||||
|
@ -169,26 +165,14 @@ int main(int argc, char *argv[]) {
|
||||||
// Set up the python path
|
// Set up the python path
|
||||||
char paths[256];
|
char paths[256];
|
||||||
|
|
||||||
char crystax_python_dir[256];
|
|
||||||
snprintf(crystax_python_dir, 256,
|
|
||||||
"%s/crystax_python", getenv("ANDROID_UNPACK"));
|
|
||||||
char python_bundle_dir[256];
|
char python_bundle_dir[256];
|
||||||
snprintf(python_bundle_dir, 256,
|
snprintf(python_bundle_dir, 256,
|
||||||
"%s/_python_bundle", getenv("ANDROID_UNPACK"));
|
"%s/_python_bundle", getenv("ANDROID_UNPACK"));
|
||||||
if (dir_exists(crystax_python_dir) || dir_exists(python_bundle_dir)) {
|
if (dir_exists(python_bundle_dir)) {
|
||||||
if (dir_exists(crystax_python_dir)) {
|
LOGP("_python_bundle dir exists");
|
||||||
LOGP("crystax_python exists");
|
snprintf(paths, 256,
|
||||||
snprintf(paths, 256,
|
"%s/stdlib.zip:%s/modules",
|
||||||
"%s/stdlib.zip:%s/modules",
|
python_bundle_dir, python_bundle_dir);
|
||||||
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("calculated paths to be...");
|
||||||
LOGP(paths);
|
LOGP(paths);
|
||||||
|
@ -200,24 +184,11 @@ int main(int argc, char *argv[]) {
|
||||||
|
|
||||||
LOGP("set wchar paths...");
|
LOGP("set wchar paths...");
|
||||||
} else {
|
} else {
|
||||||
// We do not expect to see crystax_python any more, so no point
|
LOGP("_python_bundle does not exist...this not looks good, all python"
|
||||||
// reminding the user about it. If it does exist, we'll have
|
" recipes should have this folder, should we expect a crash soon?");
|
||||||
// logged it earlier.
|
|
||||||
LOGP("_python_bundle does not exist");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Py_Initialize();
|
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");
|
LOGP("Initialized python");
|
||||||
|
|
||||||
/* ensure threads will work.
|
/* ensure threads will work.
|
||||||
|
@ -236,34 +207,8 @@ int main(int argc, char *argv[]) {
|
||||||
* replace sys.path with our path
|
* replace sys.path with our path
|
||||||
*/
|
*/
|
||||||
PyRun_SimpleString("import sys, posix\n");
|
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];
|
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)) {
|
if (dir_exists(python_bundle_dir)) {
|
||||||
snprintf(add_site_packages_dir, 256,
|
snprintf(add_site_packages_dir, 256,
|
||||||
|
@ -281,13 +226,13 @@ int main(int argc, char *argv[]) {
|
||||||
PyRun_SimpleString(
|
PyRun_SimpleString(
|
||||||
"class LogFile(object):\n"
|
"class LogFile(object):\n"
|
||||||
" def __init__(self):\n"
|
" def __init__(self):\n"
|
||||||
" self.buffer = ''\n"
|
" self.__buffer = ''\n"
|
||||||
" def write(self, s):\n"
|
" def write(self, s):\n"
|
||||||
" s = self.buffer + s\n"
|
" s = self.__buffer + s\n"
|
||||||
" lines = s.split(\"\\n\")\n"
|
" lines = s.split('\\n')\n"
|
||||||
" for l in lines[:-1]:\n"
|
" for l in lines[:-1]:\n"
|
||||||
" androidembed.log(l)\n"
|
" androidembed.log(l.replace('\\x00', ''))\n"
|
||||||
" self.buffer = lines[-1]\n"
|
" self.__buffer = lines[-1]\n"
|
||||||
" def flush(self):\n"
|
" def flush(self):\n"
|
||||||
" return\n"
|
" return\n"
|
||||||
"sys.stdout = sys.stderr = LogFile()\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");
|
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, '.');
|
char *dot = strrchr(env_entrypoint, '.');
|
||||||
#if PY_MAJOR_VERSION > 2
|
|
||||||
char *ext = ".pyc";
|
char *ext = ".pyc";
|
||||||
#else
|
|
||||||
char *ext = ".pyo";
|
|
||||||
#endif
|
|
||||||
if (dot <= 0) {
|
if (dot <= 0) {
|
||||||
LOGP("Invalid entrypoint, abort.");
|
LOGP("Invalid entrypoint, abort.");
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -329,21 +270,17 @@ int main(int argc, char *argv[]) {
|
||||||
entrypoint[strlen(env_entrypoint) - 1] = '\0';
|
entrypoint[strlen(env_entrypoint) - 1] = '\0';
|
||||||
LOGP(entrypoint);
|
LOGP(entrypoint);
|
||||||
if (!file_exists(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;
|
return -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
strcpy(entrypoint, env_entrypoint);
|
strcpy(entrypoint, env_entrypoint);
|
||||||
}
|
}
|
||||||
} else if (!strcmp(dot, ".py")) {
|
} 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);
|
strcpy(entrypoint, env_entrypoint);
|
||||||
entrypoint[strlen(env_entrypoint) + 1] = '\0';
|
entrypoint[strlen(env_entrypoint) + 1] = '\0';
|
||||||
#if PY_MAJOR_VERSION > 2
|
|
||||||
entrypoint[strlen(env_entrypoint)] = 'c';
|
entrypoint[strlen(env_entrypoint)] = 'c';
|
||||||
#else
|
|
||||||
entrypoint[strlen(env_entrypoint)] = 'o';
|
|
||||||
#endif
|
|
||||||
if (!file_exists(entrypoint)) {
|
if (!file_exists(entrypoint)) {
|
||||||
/* fallback on pure python version */
|
/* fallback on pure python version */
|
||||||
if (!file_exists(env_entrypoint)) {
|
if (!file_exists(env_entrypoint)) {
|
||||||
|
@ -353,7 +290,7 @@ int main(int argc, char *argv[]) {
|
||||||
strcpy(entrypoint, env_entrypoint);
|
strcpy(entrypoint, env_entrypoint);
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
return -1;
|
||||||
}
|
}
|
||||||
// LOGP("Entrypoint is:");
|
// LOGP("Entrypoint is:");
|
||||||
|
@ -374,8 +311,7 @@ int main(int argc, char *argv[]) {
|
||||||
ret = 1;
|
ret = 1;
|
||||||
PyErr_Print(); /* This exits with the right code if SystemExit. */
|
PyErr_Print(); /* This exits with the right code if SystemExit. */
|
||||||
PyObject *f = PySys_GetObject("stdout");
|
PyObject *f = PySys_GetObject("stdout");
|
||||||
if (PyFile_WriteString(
|
if (PyFile_WriteString("\n", f))
|
||||||
"\n", f)) /* python2 used Py_FlushLine, but this no longer exists */
|
|
||||||
PyErr_Clear();
|
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 android.os.Process;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
import org.kivy.android.PythonUtil;
|
//imports for channel definition
|
||||||
|
import android.app.NotificationManager;
|
||||||
import org.renpy.android.Hardware;
|
import android.app.NotificationChannel;
|
||||||
|
import android.graphics.Color;
|
||||||
|
|
||||||
public class PythonService extends Service implements Runnable {
|
public class PythonService extends Service implements Runnable {
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@ public class PythonService extends Service implements Runnable {
|
||||||
private String serviceEntrypoint;
|
private String serviceEntrypoint;
|
||||||
// Argument to pass to Python code,
|
// Argument to pass to Python code,
|
||||||
private String pythonServiceArgument;
|
private String pythonServiceArgument;
|
||||||
|
|
||||||
|
|
||||||
public static PythonService mService = null;
|
public static PythonService mService = null;
|
||||||
private Intent startIntent = null;
|
private Intent startIntent = null;
|
||||||
|
|
||||||
|
@ -42,10 +44,6 @@ public class PythonService extends Service implements Runnable {
|
||||||
autoRestartService = restart;
|
autoRestartService = restart;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean canDisplayNotification() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int startType() {
|
public int startType() {
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
@ -64,10 +62,15 @@ public class PythonService extends Service implements Runnable {
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
if (pythonThread != null) {
|
if (pythonThread != null) {
|
||||||
Log.v("python service", "service exists, do not start again");
|
Log.v("python service", "service exists, do not start again");
|
||||||
return START_NOT_STICKY;
|
return startType();
|
||||||
|
}
|
||||||
|
//intent is null if OS restarts a STICKY service
|
||||||
|
if (intent == null) {
|
||||||
|
Context context = getApplicationContext();
|
||||||
|
intent = getThisDefaultIntent(context, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
startIntent = intent;
|
startIntent = intent;
|
||||||
Bundle extras = intent.getExtras();
|
Bundle extras = intent.getExtras();
|
||||||
androidPrivate = extras.getString("androidPrivate");
|
androidPrivate = extras.getString("androidPrivate");
|
||||||
androidArgument = extras.getString("androidArgument");
|
androidArgument = extras.getString("androidArgument");
|
||||||
|
@ -75,28 +78,38 @@ public class PythonService extends Service implements Runnable {
|
||||||
pythonName = extras.getString("pythonName");
|
pythonName = extras.getString("pythonName");
|
||||||
pythonHome = extras.getString("pythonHome");
|
pythonHome = extras.getString("pythonHome");
|
||||||
pythonPath = extras.getString("pythonPath");
|
pythonPath = extras.getString("pythonPath");
|
||||||
|
boolean serviceStartAsForeground = (
|
||||||
|
extras.getString("serviceStartAsForeground").equals("true")
|
||||||
|
);
|
||||||
pythonServiceArgument = extras.getString("pythonServiceArgument");
|
pythonServiceArgument = extras.getString("pythonServiceArgument");
|
||||||
|
|
||||||
pythonThread = new Thread(this);
|
pythonThread = new Thread(this);
|
||||||
pythonThread.start();
|
pythonThread.start();
|
||||||
|
|
||||||
if (canDisplayNotification()) {
|
if (serviceStartAsForeground) {
|
||||||
doStartForeground(extras);
|
doStartForeground(extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
return startType();
|
return startType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected int getServiceId() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected void doStartForeground(Bundle extras) {
|
protected void doStartForeground(Bundle extras) {
|
||||||
String serviceTitle = extras.getString("serviceTitle");
|
String serviceTitle = extras.getString("serviceTitle");
|
||||||
String serviceDescription = extras.getString("serviceDescription");
|
String serviceDescription = extras.getString("serviceDescription");
|
||||||
|
|
||||||
Notification notification;
|
Notification notification;
|
||||||
Context context = getApplicationContext();
|
Context context = getApplicationContext();
|
||||||
Intent contextIntent = new Intent(context, PythonActivity.class);
|
Intent contextIntent = new Intent(context, PythonActivity.class);
|
||||||
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
|
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
notification = new Notification(
|
notification = new Notification(
|
||||||
context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis());
|
context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis());
|
||||||
try {
|
try {
|
||||||
|
@ -109,14 +122,26 @@ public class PythonService extends Service implements Runnable {
|
||||||
IllegalArgumentException | InvocationTargetException e) {
|
IllegalArgumentException | InvocationTargetException e) {
|
||||||
}
|
}
|
||||||
} else {
|
} 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.setContentTitle(serviceTitle);
|
||||||
builder.setContentText(serviceDescription);
|
builder.setContentText(serviceDescription);
|
||||||
builder.setContentIntent(pIntent);
|
builder.setContentIntent(pIntent);
|
||||||
builder.setSmallIcon(context.getApplicationInfo().icon);
|
builder.setSmallIcon(context.getApplicationInfo().icon);
|
||||||
notification = builder.build();
|
notification = builder.build();
|
||||||
}
|
}
|
||||||
startForeground(1, notification);
|
startForeground(getServiceId(), notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -137,7 +162,10 @@ public class PythonService extends Service implements Runnable {
|
||||||
@Override
|
@Override
|
||||||
public void onTaskRemoved(Intent rootIntent) {
|
public void onTaskRemoved(Intent rootIntent) {
|
||||||
super.onTaskRemoved(rootIntent);
|
super.onTaskRemoved(rootIntent);
|
||||||
stopSelf();
|
//sticky servcie runtime/restart is managed by the OS. leave it running when app is closed
|
||||||
|
if (startType() != START_STICKY) {
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
package org.kivy.android;
|
package org.kivy.android;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.io.FilenameFilter;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.renpy.android.AssetExtract;
|
||||||
|
|
||||||
public class PythonUtil {
|
public class PythonUtil {
|
||||||
private static final String TAG = "pythonutil";
|
private static final String TAG = "pythonutil";
|
||||||
|
@ -32,21 +40,25 @@ public class PythonUtil {
|
||||||
|
|
||||||
protected static ArrayList<String> getLibraries(File libsDir) {
|
protected static ArrayList<String> getLibraries(File libsDir) {
|
||||||
ArrayList<String> libsList = new ArrayList<String>();
|
ArrayList<String> libsList = new ArrayList<String>();
|
||||||
addLibraryIfExists(libsList, "crystax", libsDir);
|
|
||||||
addLibraryIfExists(libsList, "sqlite3", libsDir);
|
addLibraryIfExists(libsList, "sqlite3", libsDir);
|
||||||
addLibraryIfExists(libsList, "ffi", libsDir);
|
addLibraryIfExists(libsList, "ffi", libsDir);
|
||||||
|
addLibraryIfExists(libsList, "png16", libsDir);
|
||||||
addLibraryIfExists(libsList, "ssl.*", libsDir);
|
addLibraryIfExists(libsList, "ssl.*", libsDir);
|
||||||
addLibraryIfExists(libsList, "crypto.*", 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.5m");
|
||||||
libsList.add("python3.6m");
|
libsList.add("python3.6m");
|
||||||
libsList.add("python3.7m");
|
libsList.add("python3.7m");
|
||||||
|
libsList.add("python3.8");
|
||||||
|
libsList.add("python3.9");
|
||||||
libsList.add("main");
|
libsList.add("main");
|
||||||
return libsList;
|
return libsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadLibraries(File filesDir, File libsDir) {
|
public static void loadLibraries(File filesDir, File libsDir) {
|
||||||
String filesDirPath = filesDir.getAbsolutePath();
|
|
||||||
boolean foundPython = false;
|
boolean foundPython = false;
|
||||||
|
|
||||||
for (String lib : getLibraries(libsDir)) {
|
for (String lib : getLibraries(libsDir)) {
|
||||||
|
@ -61,8 +73,8 @@ public class PythonUtil {
|
||||||
// load, and it has failed, give a more
|
// load, and it has failed, give a more
|
||||||
// general error
|
// general error
|
||||||
Log.v(TAG, "Library loading error: " + e.getMessage());
|
Log.v(TAG, "Library loading error: " + e.getMessage());
|
||||||
if (lib.startsWith("python3.7") && !foundPython) {
|
if (lib.startsWith("python3.9") && !foundPython) {
|
||||||
throw new java.lang.RuntimeException("Could not load any libpythonXXX.so");
|
throw new RuntimeException("Could not load any libpythonXXX.so");
|
||||||
} else if (lib.startsWith("python")) {
|
} else if (lib.startsWith("python")) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,5 +85,174 @@ public class PythonUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.v(TAG, "Loaded everything!");
|
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
|
// spaces amount
|
||||||
package org.renpy.android;
|
package org.renpy.android;
|
||||||
|
|
||||||
import java.io.*;
|
import android.content.Context;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.FileInputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
|
import org.kamranzafar.jtar.TarEntry;
|
||||||
import org.kamranzafar.jtar.*;
|
import org.kamranzafar.jtar.TarInputStream;
|
||||||
|
|
||||||
public class AssetExtract {
|
public class AssetExtract {
|
||||||
|
|
||||||
private AssetManager mAssetManager = null;
|
private AssetManager mAssetManager = null;
|
||||||
private Activity mActivity = null;
|
|
||||||
|
|
||||||
public AssetExtract(Activity act) {
|
public AssetExtract(Context context) {
|
||||||
mActivity = act;
|
mAssetManager = context.getAssets();
|
||||||
mAssetManager = act.getAssets();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean extractTar(String asset, String target) {
|
public boolean extractTar(String asset, String target, String method) {
|
||||||
|
|
||||||
byte buf[] = new byte[1024 * 1024];
|
byte buf[] = new byte[1024 * 1024];
|
||||||
|
|
||||||
|
@ -39,7 +37,12 @@ public class AssetExtract {
|
||||||
TarInputStream tis = null;
|
TarInputStream tis = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
|
if(method == "private"){
|
||||||
|
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
|
||||||
|
} else if (method == "pybundle") {
|
||||||
|
assetStream = new FileInputStream(asset);
|
||||||
|
}
|
||||||
|
|
||||||
tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192));
|
tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e("python", "opening up extract tar", e);
|
Log.e("python", "opening up extract tar", e);
|
||||||
|
@ -51,7 +54,7 @@ public class AssetExtract {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
entry = tis.getNextEntry();
|
entry = tis.getNextEntry();
|
||||||
} catch ( java.io.IOException e ) {
|
} catch ( IOException e ) {
|
||||||
Log.e("python", "extracting tar", e);
|
Log.e("python", "extracting tar", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -76,8 +79,7 @@ public class AssetExtract {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
out = new BufferedOutputStream(new FileOutputStream(path), 8192);
|
out = new BufferedOutputStream(new FileOutputStream(path), 8192);
|
||||||
} catch ( FileNotFoundException e ) {
|
} catch ( FileNotFoundException | SecurityException e ) {}
|
||||||
} catch ( SecurityException e ) { };
|
|
||||||
|
|
||||||
if ( out == null ) {
|
if ( out == null ) {
|
||||||
Log.e("python", "could not open " + path);
|
Log.e("python", "could not open " + path);
|
||||||
|
@ -97,7 +99,7 @@ public class AssetExtract {
|
||||||
|
|
||||||
out.flush();
|
out.flush();
|
||||||
out.close();
|
out.close();
|
||||||
} catch ( java.io.IOException e ) {
|
} catch ( IOException e ) {
|
||||||
Log.e("python", "extracting zip", e);
|
Log.e("python", "extracting zip", e);
|
||||||
return false;
|
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
|
* 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
|
* 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
|
* change. So this is the next best thing.
|
||||||
* org.renpy.pygame.) So this is the next best thing.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.renpy.android;
|
package org.renpy.android;
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
package {{ args.package }};
|
package {{ args.package }};
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.app.Notification;
|
import {{ args.service_class_name }};
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import org.kivy.android.PythonService;
|
|
||||||
import org.kivy.android.PythonActivity;
|
|
||||||
|
|
||||||
|
|
||||||
public class Service{{ name|capitalize }} extends PythonService {
|
public class Service{{ name|capitalize }} extends {{ base_service_class }} {
|
||||||
{% if sticky %}
|
{% if sticky %}
|
||||||
@Override
|
@Override
|
||||||
public int startType() {
|
public int startType() {
|
||||||
|
@ -20,54 +13,35 @@ public class Service{{ name|capitalize }} extends PythonService {
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not foreground %}
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canDisplayNotification() {
|
protected int getServiceId() {
|
||||||
return false;
|
return {{ service_id }};
|
||||||
}
|
|
||||||
{% 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public void start(Context ctx, String pythonServiceArgument) {
|
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);
|
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
|
||||||
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
|
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
|
||||||
intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
|
intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
|
||||||
intent.putExtra("androidArgument", argument);
|
intent.putExtra("androidArgument", argument);
|
||||||
|
intent.putExtra("serviceTitle", "{{ args.name }}");
|
||||||
|
intent.putExtra("serviceDescription", "{{ name|capitalize }}");
|
||||||
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
|
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
|
||||||
intent.putExtra("pythonName", "{{ name }}");
|
intent.putExtra("pythonName", "{{ name }}");
|
||||||
|
intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}");
|
||||||
intent.putExtra("pythonHome", argument);
|
intent.putExtra("pythonHome", argument);
|
||||||
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
|
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
|
||||||
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
|
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) {
|
static public void stop(Context ctx) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ buildscript {
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.1.4'
|
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,23 +13,45 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
flatDir {
|
{%- for repo in args.gradle_repositories %}
|
||||||
dirs 'libs'
|
{{repo}}
|
||||||
}
|
{%- endfor %}
|
||||||
|
flatDir {
|
||||||
|
dirs 'libs'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% if is_library %}
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
{% else %}
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion {{ android_api }}
|
compileSdkVersion {{ android_api }}
|
||||||
buildToolsVersion '{{ build_tools_version }}'
|
buildToolsVersion '{{ build_tools_version }}'
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion {{ args.min_sdk_version }}
|
minSdkVersion {{ args.min_sdk_version }}
|
||||||
targetSdkVersion {{ android_api }}
|
targetSdkVersion {{ android_api }}
|
||||||
versionCode {{ args.numeric_version }}
|
versionCode {{ args.numeric_version }}
|
||||||
versionName '{{ args.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 -%}
|
{% if args.sign -%}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
@ -40,41 +62,73 @@ android {
|
||||||
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
|
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
buildTypes {
|
{% if args.packaging_options -%}
|
||||||
debug {
|
packagingOptions {
|
||||||
}
|
{%- for option in args.packaging_options %}
|
||||||
release {
|
{{option}}
|
||||||
{% if args.sign -%}
|
{%- endfor %}
|
||||||
signingConfig signingConfigs.release
|
}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
}
|
|
||||||
}
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
{% if args.sign -%}
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
{%- endif %}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
{% if args.enable_androidx %}
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
{% else %}
|
||||||
sourceCompatibility JavaVersion.VERSION_1_7
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
targetCompatibility JavaVersion.VERSION_1_7
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
{% endif %}
|
||||||
|
{%- for option in args.compile_options %}
|
||||||
|
{{option}}
|
||||||
|
{%- endfor %}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
jniLibs.srcDir 'libs'
|
jniLibs.srcDir 'libs'
|
||||||
|
java {
|
||||||
|
|
||||||
|
{%- for adir, pattern in args.extra_source_dirs -%}
|
||||||
|
srcDir '{{adir}}'
|
||||||
|
{%- endfor -%}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aaptOptions {
|
||||||
|
noCompress "tflite"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
{%- for aar in aars %}
|
{%- for aar in aars %}
|
||||||
compile(name: '{{ aar }}', ext: 'aar')
|
implementation(name: '{{ aar }}', ext: 'aar')
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- for jar in jars %}
|
{%- for jar in jars %}
|
||||||
compile files('src/main/libs/{{ jar }}')
|
implementation files('src/main/libs/{{ jar }}')
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- if args.depends -%}
|
{%- if args.depends -%}
|
||||||
{%- for depend in args.depends %}
|
{%- for depend in args.depends %}
|
||||||
compile '{{ depend }}'
|
implementation '{{ depend }}'
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{%- endif %}
|
{%- 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):
|
class LbryBootstrap(Bootstrap):
|
||||||
name = 'lbry'
|
name = 'lbry'
|
||||||
|
|
||||||
recipe_depends = ['genericndkbuild', ('python2', 'python3crystax')]
|
recipe_depends = ['genericndkbuild', ('python3', 'python3crystax')]
|
||||||
|
|
||||||
def run_distribute(self):
|
def run_distribute(self):
|
||||||
info_main("# Creating Android project ({})".format(self.name))
|
info_main("# Creating Android project ({})".format(self.name))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python2.7
|
#!/usr/bin/env python3
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
from __future__ import print_function
|
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
|
# Hardcoding python2.7 is okay for now, as python3 skips the
|
||||||
# compilation anyway
|
# compilation anyway
|
||||||
if not exists('crystax_python'):
|
if not exists('crystax_python'):
|
||||||
python_executable = 'python2.7'
|
python_executable = 'python3'
|
||||||
try:
|
try:
|
||||||
subprocess.call([python_executable, '--version'])
|
subprocess.call([python_executable, '--version'])
|
||||||
except (OSError, subprocess.CalledProcessError):
|
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