diff --git a/.travis.yml b/.travis.yml index 7938918ade..d857dc03bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ services: - docker before_install: - - sudo apt update -qq - - sudo apt install -qq --no-install-recommends python2.7 python3 + - travis_retry sudo apt update -qq + - travis_retry sudo apt install -qq --no-install-recommends python2.7 python3 - sudo pip install tox>=2.0 # https://github.com/travis-ci/travis-ci/issues/6069#issuecomment-266546552 - git remote set-branches --add origin master @@ -31,5 +31,5 @@ before_script: - tox script: - - docker build --tag=p4a --file Dockerfile.py2 . + - docker build --tag=p4a --file Dockerfile.py3 . - docker run p4a /bin/sh -c "$COMMAND" diff --git a/Dockerfile.py2 b/Dockerfile.py2 index f8825906eb..05f956aa22 100644 --- a/Dockerfile.py2 +++ b/Dockerfile.py2 @@ -3,13 +3,13 @@ # - python-for-android dependencies # # Build with: -# docker build --tag=p4a --file Dockerfile.py2 . +# docker build --tag=p4apy2 --file Dockerfile.py2 . # # Run with: -# docker run -it --rm p4a /bin/sh -c '. venv/bin/activate && p4a apk --help' +# docker run -it --rm p4apy2 /bin/sh -c '. venv/bin/activate && p4a apk --help' # # Or for interactive shell: -# docker run -it --rm p4a +# docker run -it --rm p4apy2 # # Note: # Use 'docker run' without '--rm' flag for keeping the container and use @@ -19,11 +19,23 @@ FROM ubuntu:18.04 ENV ANDROID_HOME="/opt/android" +# configure locale +RUN apt update -qq > /dev/null && 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 \ - && apt -y install -qq --no-install-recommends curl unzip \ - && apt -y autoremove \ - && apt -y clean + && apt -y install -qq --no-install-recommends curl unzip ca-certificates \ + && apt -y autoremove +# 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 ANDROID_NDK_HOME="${ANDROID_HOME}/android-ndk" ENV ANDROID_NDK_VERSION="17c" @@ -34,7 +46,7 @@ ENV ANDROID_NDK_ARCHIVE="android-ndk-r${ANDROID_NDK_VERSION}-linux-x86_64.zip" ENV ANDROID_NDK_DL_URL="https://dl.google.com/android/repository/${ANDROID_NDK_ARCHIVE}" # download and install Android NDK -RUN curl --location --progress-bar --insecure \ +RUN ${RETRY} curl --location --progress-bar --insecure \ "${ANDROID_NDK_DL_URL}" \ --output "${ANDROID_NDK_ARCHIVE}" \ && mkdir --parents "${ANDROID_NDK_HOME_V}" \ @@ -47,11 +59,12 @@ ENV ANDROID_SDK_HOME="${ANDROID_HOME}/android-sdk" # get the latest version from https://developer.android.com/studio/index.html ENV ANDROID_SDK_TOOLS_VERSION="3859397" +ENV ANDROID_SDK_BUILD_TOOLS_VERSION="26.0.2" ENV ANDROID_SDK_TOOLS_ARCHIVE="sdk-tools-linux-${ANDROID_SDK_TOOLS_VERSION}.zip" ENV ANDROID_SDK_TOOLS_DL_URL="https://dl.google.com/android/repository/${ANDROID_SDK_TOOLS_ARCHIVE}" # download and install Android SDK -RUN curl --location --progress-bar --insecure \ +RUN ${RETRY} curl --location --progress-bar --insecure \ "${ANDROID_SDK_TOOLS_DL_URL}" \ --output "${ANDROID_SDK_TOOLS_ARCHIVE}" \ && mkdir --parents "${ANDROID_SDK_HOME}" \ @@ -64,16 +77,14 @@ RUN mkdir --parents "${ANDROID_SDK_HOME}/.android/" \ > "${ANDROID_SDK_HOME}/.android/repositories.cfg" # accept Android licenses (JDK necessary!) -RUN apt -y update -qq \ - && apt -y install -qq --no-install-recommends openjdk-8-jdk \ - && apt -y autoremove \ - && apt -y clean -RUN yes | "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" --licenses > /dev/null +RUN ${RETRY} apt -y install -qq --no-install-recommends openjdk-8-jdk \ + && apt -y autoremove +RUN yes | "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" > /dev/null # download platforms, API, build tools RUN "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-19" && \ "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-27" && \ - "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;26.0.2" && \ + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" && \ chmod +x "${ANDROID_SDK_HOME}/tools/bin/avdmanager" @@ -83,27 +94,23 @@ ENV WORK_DIR="${HOME_DIR}" \ PATH="${HOME_DIR}/.local/bin:${PATH}" # install system dependencies -RUN apt -y update -qq \ - && apt -y install -qq --no-install-recommends \ +RUN ${RETRY} apt -y install -qq --no-install-recommends \ python virtualenv python-pip wget lbzip2 patch sudo \ - && apt -y autoremove \ - && apt -y clean + && apt -y autoremove # build dependencies # https://buildozer.readthedocs.io/en/latest/installation.html#android-on-ubuntu-16-04-64bit RUN dpkg --add-architecture i386 \ - && apt -y update -qq \ - && apt -y install -qq --no-install-recommends \ + && ${RETRY} apt -y update -qq \ + && ${RETRY} apt -y install -qq --no-install-recommends \ build-essential ccache git python2.7 python2.7-dev \ libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 \ libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 \ zip zlib1g-dev zlib1g:i386 \ - && apt -y autoremove \ - && apt -y clean + && apt -y autoremove # specific recipes dependencies (e.g. libffi requires autoreconf binary) -RUN apt -y update -qq \ - && apt -y install -qq --no-install-recommends \ +RUN ${RETRY} apt -y install -qq --no-install-recommends \ libffi-dev autoconf automake cmake gettext libltdl-dev libtool pkg-config \ && apt -y autoremove \ && apt -y clean @@ -120,10 +127,8 @@ RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers RUN pip install --upgrade cython==0.28.6 WORKDIR ${WORK_DIR} -COPY . ${WORK_DIR} - -# user needs ownership/write access to these directories -RUN chown --recursive ${USER} ${WORK_DIR} ${ANDROID_SDK_HOME} +COPY --chown=user:user . ${WORK_DIR} +RUN chown --recursive ${USER} ${ANDROID_SDK_HOME} USER ${USER} # install python-for-android from current branch diff --git a/Dockerfile.py3 b/Dockerfile.py3 index 878804c76a..307f8ddfda 100644 --- a/Dockerfile.py3 +++ b/Dockerfile.py3 @@ -3,13 +3,13 @@ # - python-for-android dependencies # # Build with: -# docker build --tag=p4apy3 . +# docker build --tag=p4a --file Dockerfile.py3 . # # Run with: -# docker run -it --rm p4apy3 /bin/sh -c '. venv/bin/activate && p4a apk --help' +# docker run -it --rm p4a /bin/sh -c '. venv/bin/activate && p4a apk --help' # # Or for interactive shell: -# docker run -it --rm p4apy3 +# docker run -it --rm p4a # # Note: # Use 'docker run' without '--rm' flag for keeping the container and use @@ -19,11 +19,23 @@ FROM ubuntu:18.04 ENV ANDROID_HOME="/opt/android" +# configure locale +RUN apt update -qq > /dev/null && 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 \ - && apt -y install -qq --no-install-recommends curl unzip \ - && apt -y autoremove \ - && apt -y clean + && apt -y install -qq --no-install-recommends curl unzip ca-certificates \ + && apt -y autoremove +# 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 ANDROID_NDK_HOME="${ANDROID_HOME}/android-ndk" ENV ANDROID_NDK_VERSION="17c" @@ -34,7 +46,7 @@ ENV ANDROID_NDK_ARCHIVE="android-ndk-r${ANDROID_NDK_VERSION}-linux-x86_64.zip" ENV ANDROID_NDK_DL_URL="https://dl.google.com/android/repository/${ANDROID_NDK_ARCHIVE}" # download and install Android NDK -RUN curl --location --progress-bar --insecure \ +RUN ${RETRY} curl --location --progress-bar --insecure \ "${ANDROID_NDK_DL_URL}" \ --output "${ANDROID_NDK_ARCHIVE}" \ && mkdir --parents "${ANDROID_NDK_HOME_V}" \ @@ -47,11 +59,12 @@ ENV ANDROID_SDK_HOME="${ANDROID_HOME}/android-sdk" # get the latest version from https://developer.android.com/studio/index.html ENV ANDROID_SDK_TOOLS_VERSION="3859397" +ENV ANDROID_SDK_BUILD_TOOLS_VERSION="26.0.2" ENV ANDROID_SDK_TOOLS_ARCHIVE="sdk-tools-linux-${ANDROID_SDK_TOOLS_VERSION}.zip" ENV ANDROID_SDK_TOOLS_DL_URL="https://dl.google.com/android/repository/${ANDROID_SDK_TOOLS_ARCHIVE}" # download and install Android SDK -RUN curl --location --progress-bar --insecure \ +RUN ${RETRY} curl --location --progress-bar --insecure \ "${ANDROID_SDK_TOOLS_DL_URL}" \ --output "${ANDROID_SDK_TOOLS_ARCHIVE}" \ && mkdir --parents "${ANDROID_SDK_HOME}" \ @@ -64,16 +77,14 @@ RUN mkdir --parents "${ANDROID_SDK_HOME}/.android/" \ > "${ANDROID_SDK_HOME}/.android/repositories.cfg" # accept Android licenses (JDK necessary!) -RUN apt -y update -qq \ - && apt -y install -qq --no-install-recommends openjdk-8-jdk \ - && apt -y autoremove \ - && apt -y clean -RUN yes | "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" --licenses > /dev/null +RUN ${RETRY} apt -y install -qq --no-install-recommends openjdk-8-jdk \ + && apt -y autoremove +RUN yes | "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" > /dev/null # download platforms, API, build tools RUN "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-19" && \ "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-27" && \ - "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;26.0.2" && \ + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" && \ chmod +x "${ANDROID_SDK_HOME}/tools/bin/avdmanager" @@ -83,27 +94,23 @@ ENV WORK_DIR="${HOME_DIR}" \ PATH="${HOME_DIR}/.local/bin:${PATH}" # install system dependencies -RUN apt -y update -qq \ - && apt -y install -qq --no-install-recommends \ +RUN ${RETRY} apt -y install -qq --no-install-recommends \ python3 virtualenv python3-pip wget lbzip2 patch sudo \ - && apt -y autoremove \ - && apt -y clean + && apt -y autoremove # build dependencies # https://buildozer.readthedocs.io/en/latest/installation.html#android-on-ubuntu-16-04-64bit RUN dpkg --add-architecture i386 \ - && apt -y update -qq \ - && apt -y install -qq --no-install-recommends \ + && ${RETRY} apt -y update -qq \ + && ${RETRY} apt -y install -qq --no-install-recommends \ build-essential ccache git python3 python3-dev \ libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 \ libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 \ zip zlib1g-dev zlib1g:i386 \ - && apt -y autoremove \ - && apt -y clean + && apt -y autoremove # specific recipes dependencies (e.g. libffi requires autoreconf binary) -RUN apt -y update -qq \ - && apt -y install -qq --no-install-recommends \ +RUN ${RETRY} apt -y install -qq --no-install-recommends \ libffi-dev autoconf automake cmake gettext libltdl-dev libtool pkg-config \ && apt -y autoremove \ && apt -y clean @@ -120,10 +127,8 @@ RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers RUN pip3 install --upgrade cython==0.28.6 WORKDIR ${WORK_DIR} -COPY . ${WORK_DIR} - -# user needs ownership/write access to these directories -RUN chown --recursive ${USER} ${WORK_DIR} ${ANDROID_SDK_HOME} +COPY --chown=user:user . ${WORK_DIR} +RUN chown --recursive ${USER} ${ANDROID_SDK_HOME} USER ${USER} # install python-for-android from current branch diff --git a/MANIFEST.in b/MANIFEST.in index 06c844dd38..1c26fb073d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,7 +7,7 @@ prune doc/build recursive-include pythonforandroid *.py *.tmpl biglink liblink recursive-include pythonforandroid/recipes *.py *.patch *.c *.pyx Setup *.h -recursive-include pythonforandroid/bootstraps *.properties *.xml *.java *.tmpl *.txt *.png *.aidl *.py *.sh *.c *.h *.html +recursive-include pythonforandroid/bootstraps *.properties *.xml *.java *.tmpl *.txt *.png *.aidl *.py *.sh *.c *.h *.html *.patch prune .git prune pythonforandroid/bootstraps/pygame/build/libs diff --git a/README.rst b/README.md similarity index 53% rename from README.rst rename to README.md index 9f0fca181c..e5eadf4a05 100644 --- a/README.rst +++ b/README.md @@ -1,10 +1,9 @@ python-for-android ================== -|Build Status| - -.. |Build Status| image:: https://secure.travis-ci.org/kivy/python-for-android.png?branch=master - :target: https://travis-ci.org/kivy/python-for-android +[![Build Status](https://travis-ci.org/kivy/python-for-android.svg?branch=master)](https://travis-ci.org/kivy/python-for-android) +[![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 packager for Python apps on Android. You can create your own Python distribution including the modules and @@ -27,45 +26,17 @@ For documentation and support, see: - Mailing list: https://groups.google.com/forum/#!forum/kivy-users or https://groups.google.com/forum/#!forum/python-android. -In 2015 these tools were rewritten to provide a new, easier to use and -extend 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 -In the last quarter of 2018 the python recipes has been changed, the new recipe -for python3 (3.7.1) has a new build system which has been applied to the ancient -python recipe, allowing us to bump the python2 version number to 2.7.15. This -change, unifies the build process for both python recipes, and probably solve -some issues detected over the years, but unfortunately, this change breaks the -pygame bootstrap (in a near future we will fix it or remove it). Also should be -mentioned that the old python recipe is still usable, and has been renamed to -`python2legacy`. This `python2legacy` recipe allow us to build with a minimum -api lower than 21, which is not the case for the new python recipes which are -limited to a minimum api of 21. All this work has been done using android ndk -version r17c, and your build should success with that version...but be aware -that the project is in constant development so...the ndk version will change -at some time. - -Those mentioned changes has been done this way to make easier the transition -between python3 and python2. We will slowly phase out python2 support -towards 2020...so...if you are using python2 in your projects you should -consider to migrate it into python3. - -Documentation -============= - -Follow the `quickstart -instructions `__ +Follow the [quickstart +instructions]() to install and begin creating APKs. -Quick instructions to start would be:: +Quick instructions to start would be: pip install python-for-android -or to test the master branch:: +or to test the master branch: pip install git+https://github.com/kivy/python-for-android.git @@ -79,28 +50,26 @@ This should return a list of recipes available to be built. To build any distributions, you need to set up the Android SDK and NDK as described in the documentation linked above. -If you did this, to build an APK with SDL2 you can try e.g.:: +If you did this, to build an APK with SDL2 you can try e.g.: 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 `__. +For full instructions and parameter options, see [the +documentation](https://python-for-android.readthedocs.io/en/latest/quickstart/#usage). -Support -======= +## Support If you need assistance, you can ask for help on our mailing list: - User Group: https://groups.google.com/group/kivy-users - Email: kivy-users@googlegroups.com -We also have `#support Discord channel `_. +We also have [#support Discord channel](https://chat.kivy.org/). -Contributing -============ +## Contributing We love pull requests and discussing novel ideas. Check out our -`contribution guide `__ and feel +[contribution guide](http://kivy.org/docs/contribute.html) and feel free to improve python-for-android. The following mailing list and IRC channel are used exclusively for @@ -109,10 +78,67 @@ discussions about developing the Kivy framework and its sister projects: - Dev Group: https://groups.google.com/group/kivy-dev - Email: kivy-dev@googlegroups.com -We also have `#dev Discord channel `_. +We also have [#dev Discord channel](https://chat.kivy.org/). -License -======= +## License python-for-android is released under the terms of the MIT License. Please refer to the LICENSE file. + +## History + +In 2015 these tools were rewritten to provide a new, easier to use and +extend 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. + +In the last quarter of 2018 the python recipes has been changed, the new recipe +for python3 (3.7.1) has a new build system which has been applied to the ancient +python recipe, allowing us to bump the python2 version number to 2.7.15. This +change, unifies the build process for both python recipes, and probably solve +some issues detected over the years, but unfortunately, this change breaks the +pygame bootstrap (in a near future we will fix it or remove it). Also should be +mentioned that the old python recipe is still usable, and has been renamed to +`python2legacy`. This `python2legacy` recipe allow us to build with a minimum +api lower than 21, which is not the case for the new python recipes which are +limited to a minimum api of 21. All this work has been done using android ndk +version r17c, and your build should success with that version...but be aware +that the project is in constant development so...the ndk version will change +at some time. + +Those mentioned changes has been done this way to make easier the transition +between python3 and python2. We will slowly phase out python2 support +towards 2020...so...if you are using python2 in your projects you should +consider to migrate it into python3. + +## Contributors + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + + +## Backers + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/kivy#backer)] + + + + +## 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)] + + + + + + + + + + + + diff --git a/ci/constants.py b/ci/constants.py index a6a8f99702..5484f15de3 100644 --- a/ci/constants.py +++ b/ci/constants.py @@ -16,8 +16,6 @@ class TargetPython(Enum): # https://github.com/kivy/python-for-android/issues/550 'audiostream', 'brokenrecipe', - # https://github.com/kivy/python-for-android/issues/1409 - 'enaml', 'evdev', # distutils.errors.DistutilsError # Could not find suitable distribution for Requirement.parse('cython') @@ -25,10 +23,7 @@ class TargetPython(Enum): 'flask', 'groestlcoin_hash', 'hostpython3crystax', - # https://github.com/kivy/python-for-android/issues/1398 - 'ifaddrs', # https://github.com/kivy/python-for-android/issues/1354 - 'kivent_core', 'kivent_cymunk', 'kivent_particles', 'kivent_polygen', 'kiwisolver', # https://github.com/kivy/python-for-android/issues/1399 'libglob', @@ -37,17 +32,23 @@ class TargetPython(Enum): 'libtribler', 'ndghttpsclient', 'm2crypto', + # ImportError: No module named setuptools 'netifaces', 'Pillow', # depends on cffi that still seems to have compilation issues 'protobuf_cpp', 'xeddsa', 'x3dh', + # fatal error: crypt.h: No such file or directory + 'pyleveldb', 'pynacl', 'doubleratchet', + # The opencv recipe fails to pass travis tests due to the long processing + # when building it and the lack of console output, so, it's only broken + # for travis, see: https://github.com/kivy/python-for-android/pull/1661 + 'opencv', 'omemo', - 'cryptography', - # https://github.com/kivy/python-for-android/issues/1405 + # requires `libpq-dev` system dependency e.g. for `pg_config` binary 'psycopg2', 'pygame', # most likely some setup in the Docker container, because it works in host @@ -65,31 +66,27 @@ class TargetPython(Enum): 'zeroconf', 'zope', ]) -BROKEN_RECIPES_PYTHON3_CRYSTAX = set([ +BROKEN_RECIPES_PYTHON3 = set([ 'brokenrecipe', # enum34 is not compatible with Python 3.6 standard library # https://stackoverflow.com/a/45716067/185510 'enum34', - # https://github.com/kivy/python-for-android/issues/1398 - 'ifaddrs', # https://github.com/kivy/python-for-android/issues/1399 'libglob', - # cannot find -lcrystax - 'cffi', 'pycryptodome', 'pymuk', 'secp256k1', - # https://github.com/kivy/python-for-android/issues/1404 - 'cryptography', - # https://github.com/kivy/python-for-android/issues/1294 - 'ffmpeg', 'ffpyplayer', - # https://github.com/kivy/python-for-android/pull/1307 ? - 'gevent', + # build_dir = glob.glob('build/lib.*')[0] + # IndexError: list index out of range + 'secp256k1', + 'ffpyplayer', 'icu', # https://github.com/kivy/python-for-android/issues/1354 - 'kivent_core', 'kivent_cymunk', 'kivent_particles', 'kivent_polygen', - # https://github.com/kivy/python-for-android/issues/1405 - 'libpq', 'psycopg2', - 'netifaces', - # https://github.com/kivy/python-for-android/issues/1315 ? + # The opencv recipe fails to pass travis tests due to the long processing + # when building it and the lack of console output, so, it's only broken + # for travis, see: https://github.com/kivy/python-for-android/pull/1661 'opencv', + # requires `libpq-dev` system dependency e.g. for `pg_config` binary + 'psycopg2', + # fatal error: crypt.h: No such file or directory + 'pyleveldb', 'protobuf_cpp', # most likely some setup in the Docker container, because it works in host 'pyjnius', 'pyopenal', @@ -99,13 +96,9 @@ class TargetPython(Enum): 'sympy', 'vlc', ]) -# to be created via https://github.com/kivy/python-for-android/issues/1514 -BROKEN_RECIPES_PYTHON3 = set([ -]) BROKEN_RECIPES = { TargetPython.python2: BROKEN_RECIPES_PYTHON2, - TargetPython.python3crystax: BROKEN_RECIPES_PYTHON3_CRYSTAX, TargetPython.python3: BROKEN_RECIPES_PYTHON3, } # recipes that were already built will be skipped diff --git a/ci/rebuild_updated_recipes.py b/ci/rebuild_updated_recipes.py index f2d8d0a387..a2e070b009 100755 --- a/ci/rebuild_updated_recipes.py +++ b/ci/rebuild_updated_recipes.py @@ -24,8 +24,10 @@ import sh import os from pythonforandroid.build import Context +from pythonforandroid import logger from pythonforandroid.graph import get_recipe_order_and_bootstrap from pythonforandroid.toolchain import current_directory +from pythonforandroid.util import BuildInterruptingException from ci.constants import TargetPython, CORE_RECIPES, BROKEN_RECIPES @@ -46,7 +48,7 @@ def modified_recipes(branch='origin/master'): return recipes -def build(target_python, target_bootstrap, requirements): +def build(target_python, requirements, bootstrap='sdl2'): """ Builds an APK given a target Python and a set of requirements. """ @@ -59,40 +61,40 @@ def build(target_python, target_bootstrap, requirements): testapp = 'setup_testapp_python3.py' requirements.add(target_python.name) requirements = ','.join(requirements) - print('requirements:', requirements) + logger.info('requirements: {}'.format(requirements)) with current_directory('testapps/'): - try: - for line in sh.python( - testapp, 'apk', - '--sdk-dir', android_sdk_home, - '--ndk-dir', android_ndk_home, - '--bootstrap', target_bootstrap, - '--requirements', requirements, - _err_to_out=True, _iter=True): - print(line) - except sh.ErrorReturnCode as e: - raise + for line in sh.python( + testapp, 'apk', + '--sdk-dir', android_sdk_home, + '--ndk-dir', android_ndk_home, + '--bootstrap', bootstrap, + '--requirements', requirements, + _err_to_out=True, _iter=True): + print(line) def main(): target_python = TargetPython.python3 recipes = modified_recipes() - print('recipes modified:', recipes) + logger.info('recipes modified: {}'.format(recipes)) recipes -= CORE_RECIPES - print('recipes to build:', recipes) + logger.info('recipes to build: {}'.format(recipes)) context = Context() - build_order, python_modules, bs = get_recipe_order_and_bootstrap( - context, recipes, None) - # fallback to python2 if default target is not compatible - if target_python.name not in build_order: - print('incompatible with {}'.format(target_python.name)) + # forces the default target + recipes_and_target = recipes | set([target_python.name]) + try: + build_order, python_modules, bs = get_recipe_order_and_bootstrap( + context, recipes_and_target, None) + except BuildInterruptingException: + # fallback to python2 if default target is not compatible + logger.info('incompatible with {}'.format(target_python.name)) target_python = TargetPython.python2 - print('falling back to {}'.format(target_python.name)) + logger.info('falling back to {}'.format(target_python.name)) # removing the known broken recipe for the given target broken_recipes = BROKEN_RECIPES[target_python] recipes -= broken_recipes - print('recipes to build (no broken):', recipes) - build(target_python, bs.name, recipes) + logger.info('recipes to build (no broken): {}'.format(recipes)) + build(target_python, recipes, bs.name) if __name__ == '__main__': diff --git a/doc/source/apis.rst b/doc/source/apis.rst index bb46f2e339..abd4a22447 100644 --- a/doc/source/apis.rst +++ b/doc/source/apis.rst @@ -6,177 +6,42 @@ This page gives details on accessing Android APIs and managing other interactions on Android. -Accessing Android APIs ----------------------- +Runtime permissions +------------------- -When writing an Android application you may want to access the normal -Android Java APIs, in order to control your application's appearance -(fullscreen, orientation etc.), interact with other apps or use -hardware like vibration and sensors. +With API level >= 21, you will need to request runtime permissions +to access the SD card, the camera, and other things. -You can access these with `Pyjnius -`_, a Python library for -automatically wrapping Java and making it callable from Python -code. Pyjnius is fairly simple to use, but not very Pythonic and it -inherits Java's verbosity. For this reason the Kivy organisation also -created `Plyer `_, 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. +This can be done through the `android` module which is *available per default* +unless you blacklist it. Use it in your app like this:: -Pyjnius and Plyer are independent projects whose documentation is -linked above. See below for some simple introductory examples, and -explanation of how to include these modules in your APKs. + from android.permissions import request_permission, Permission + request_permission(Permission.WRITE_EXTERNAL_STORAGE) -This page also documents the ``android`` module which you can include -with p4a, but this is mostly replaced by Pyjnius and is not -recommended for use in new applications. +The available permissions are listed here: +https://developer.android.com/reference/android/Manifest.permission -Using Pyjnius -~~~~~~~~~~~~~ -Pyjnius lets you call the Android API directly from Python Pyjnius is -works by dynamically wrapping Java classes, so you don't have to wait -for any particular feature to be pre-supported. - -You can include Pyjnius in your APKs by adding `pyjnius` to your build -requirements, e.g. :code:`--requirements=flask,pyjnius`. It is -automatically included in any APK containing Kivy, in which case you -don't need to specify it manually. - -The basic mechanism of Pyjnius is the `autoclass` command, which wraps -a Java class. For instance, here is the code to vibrate your device:: - - from jnius import autoclass - - # We need a reference to the Java activity running the current - # application, this reference is stored automatically by - # Kivy's PythonActivity bootstrap - - # This one works with Pygame - # PythonActivity = autoclass('org.renpy.android.PythonActivity') - - # This one works with SDL2 - PythonActivity = autoclass('org.kivy.android.PythonActivity') - - activity = PythonActivity.mActivity - - Context = autoclass('android.content.Context') - vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) - - vibrator.vibrate(10000) # the argument is in milliseconds - -Things to note here are: - -- The class that must be wrapped depends on the bootstrap. This is - because Pyjnius is using the bootstrap's java source code to get a - reference to the current activity, which both the Pygame and SDL2 - bootstraps store in the ``mActivity`` static variable. This - difference isn't always important, but it's important to know about. -- The code closely follows the Java API - this is exactly the same set - of function calls that you'd use to achieve the same thing from Java - code. -- This is quite verbose - it's a lot of lines to achieve a simple - vibration! - -These emphasise both the advantages and disadvantage of Pyjnius; you -*can* achieve just about any API call with it (though the syntax is -sometimes a little more involved, particularly if making Java classes -from Python code), but it's not Pythonic and it's not short. These are -problems that Plyer, explained below, attempts to address. - -You can check the `Pyjnius documentation `_ 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 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 `_ 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 -`__. - -One useful facility of this module is to make -:code:`webbrowser.open()` work on Android. You can replicate this -effect without using the android module via the following -code:: - - from jnius import autoclass - - def open_url(url): - Intent = autoclass('android.content.Intent') - Uri = autoclass('android.net.Uri') - browserIntent = Intent() - browserIntent.setAction(Intent.ACTION_VIEW) - browserIntent.setData(Uri.parse(url)) - currentActivity = cast('android.app.Activity', mActivity) - currentActivity.startActivity(browserIntent) - - class AndroidBrowser(object): - def open(self, url, new=0, autoraise=True): - open_url(url) - def open_new(self, url): - open_url(url) - def open_new_tab(self, url): - open_url(url) - - import webbrowser - webbrowser.register('android', AndroidBrowser, None, -1) - - -Working with the App lifecycle ------------------------------- +Other common tasks +------------------ Dismissing the splash screen ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -With the SDL2 bootstrap, the app's splash screen may not be dismissed -immediately when your app has finished loading, due to a limitation -with the way we check if the app has properly started. In this case, -the splash screen overlaps the app gui for a short time. +With the SDL2 bootstrap, the app's splash screen may be visible +longer than necessary (with your app already being loaded) due to a +limitation with the way we check if the app has properly started. +In this case, the splash screen overlaps the app gui for a short time. -You can dismiss the splash screen by running this code from your -app build method (or use ``kivy.clock.Clock.schedule_once`` to run it -in the following frame):: +To dismiss the loading screen explicitely in your code, use the `android` +module:: - from jnius import autoclass - activity = autoclass('org.kivy.android.PythonActivity').mActivity - activity.removeLoadingScreen() + from android import hide_loading_screen + hide_loading_screen() -This problem does not affect the Pygame bootstrap, as it uses a -different splash screen method. +You can call it e.g. using ``kivy.clock.Clock.schedule_once`` to run it +in the first active frame of your app, or use the app build method. Handling the back button @@ -222,3 +87,109 @@ With Kivy, add an ``on_pause`` method to your App class, which returns True:: With the webview bootstrap, pausing should work automatically. Under SDL2, you can handle the `appropriate events `__ (see SDL_APP_WILLENTERBACKGROUND etc.). + + +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 per default* +unless you blacklist it. + +The available functionality of this module is not separately documented. +You can read the source `on +Github +`__. + +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 `_ 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 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 `_ for further details. + diff --git a/doc/source/buildoptions.rst b/doc/source/buildoptions.rst index 9af0554b51..4a078e6846 100644 --- a/doc/source/buildoptions.rst +++ b/doc/source/buildoptions.rst @@ -243,3 +243,25 @@ options (this list may not be exhaustive): - ``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 diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 02af5cfdb0..836387cba1 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -67,7 +67,7 @@ supply those that you need. distribution must contain, as a comma separated list. These must be names of recipes or the pypi names of Python modules. -``--force_build BOOL`` +``--force-build BOOL`` Whether the distribution must be compiled from scratch. ``--arch`` diff --git a/doc/source/index.rst b/doc/source/index.rst index 49bc2f058d..16d6162dc3 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,16 +29,15 @@ Contents quickstart buildoptions commands + apis + launcher distutils recipes bootstraps services - apis troubleshooting - launcher docker contribute - old_toolchain/index.rst Indices and tables diff --git a/doc/source/old_toolchain/Screenshot_Kivy_Kompass.png b/doc/source/old_toolchain/Screenshot_Kivy_Kompass.png deleted file mode 100644 index 828ce41952..0000000000 Binary files a/doc/source/old_toolchain/Screenshot_Kivy_Kompass.png and /dev/null differ diff --git a/doc/source/old_toolchain/android.rst b/doc/source/old_toolchain/android.rst deleted file mode 100644 index 5d898749af..0000000000 --- a/doc/source/old_toolchain/android.rst +++ /dev/null @@ -1,369 +0,0 @@ -Python API -========== - -The Python for Android project includes a Python module called -``android`` which consists of multiple parts that are mostly there to -facilitate the use of the Java API. - -This module is not designed to be comprehensive. Most of the Java API -is also accessible with PyJNIus, so if you can't find what you need -here you can try using the Java API directly instead. - - -Android (``android``) ---------------------- - -.. module:: android - -.. function:: check_pause() - - This should be called on a regular basis to check to see if Android - expects the application to pause. If it returns true, the app should call - :func:`android.wait_for_resume()`, after storing its state as necessary. - -.. function:: wait_for_resume() - - This function should be called after :func:`android.check_pause()` and returns - true. It does not return until Android has resumed from the pause. While in - this function, Android may kill the app without further notice. - -.. function:: map_key(keycode, keysym) - - This maps an android keycode to a python keysym. The android - keycodes are available as constants in the android module. - - -Activity (``android.activity``) -------------------------------- - -.. module:: android.activity - -The default PythonActivity has a observer pattern for `onActivityResult `_ and `onNewIntent `_. - -.. 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) - # ... - - -Billing (``android.billing``) ------------------------------ - -.. module:: android.billing - -This billing module gives an access to the `In-App Billing `_: - -#. `Setup a test account `_, and get your Public Key -#. Export your public key:: - - export BILLING_PUBKEY="Your public key here" - -#. `Setup some In-App product `_ to buy. Let's say you've created a product with the id "org.kivy.gopremium" - -#. In your application, you can use the ``billing`` module like this:: - - - from android.billing import BillingService - from kivy.clock import Clock - - class MyBillingService(object): - - def __init__(self): - super(MyBillingService, self).__init__() - - # Start the billing service, and attach our callback - self.service = BillingService(billing_callback) - - # Start a clock to check billing service message every second - Clock.schedule_interval(self.service.check, 1) - - def billing_callback(self, action, *largs): - '''Callback that will receive all the events from the Billing service - ''' - if action == BillingService.BILLING_ACTION_ITEMSCHANGED: - items = largs[0] - if 'org.kivy.gopremium' in items: - print "Congratulations, you have a premium acess" - else: - print "Unfortunately, you don't have premium access" - - def buy(self, sku): - # Method to buy something. - self.service.buy(sku) - - def get_purchased_items(self): - # Return all the items purchased - return self.service.get_purchased_items() - -#. To initiate an in-app purchase, just call the ``buy()`` method:: - - # Note: start the service at the start, and never twice! - bs = MyBillingService() - bs.buy('org.kivy.gopremium') - - # Later, when you get the notification that items have been changed, you - # can still check all the items you bought: - print bs.get_purchased_items() - {'org.kivy.gopremium': {'qt: 1}} - -#. You'll receive all the notifications about the billing process in the callback. - -#. Last step, create your application with ``--with-billing $BILLING_PUBKEY``:: - - ./build.py ... --with-billing $BILLING_PUBKEY - - -Broadcast (``android.broadcast``) ---------------------------------- - -.. module:: android.broadcast - -Implementation of the android `BroadcastReceiver -`_. -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() - - -Mixer (``android.mixer``) -------------------------- - -.. module:: android.mixer - -The `android.mixer` module contains a subset of the functionality in found -in the `pygame.mixer `_ module. It's -intended to be imported as an alternative to pygame.mixer, using code like: :: - - try: - import pygame.mixer as mixer - except ImportError: - import android.mixer as mixer - -Note that if you're using the `kivy.core.audio -`_ module, you don't have to do -anything, it is all automatic. - -The `android.mixer` module is a wrapper around the Android MediaPlayer -class. This allows it to take advantage of any hardware acceleration -present, and also eliminates the need to ship codecs as part of an -application. - -It has several differences with the pygame mixer: - -* The init() and pre_init() methods work, but are ignored - Android chooses - appropriate settings automatically. - -* Only filenames and true file objects can be used - file-like objects - will probably not work. - -* Fadeout does not work - it causes a stop to occur. - -* Looping is all or nothing, there is no way to choose the number of - loops that occur. For looping to work, the - :func:`android.mixer.periodic` function should be called on a - regular basis. - -* Volume control is ignored. - -* End events are not implemented. - -* The mixer.music object is a class (with static methods on it), - rather than a module. Calling methods like :func:`mixer.music.play` - should work. - - -Runnable (``android.runnable``) -------------------------------- - -.. module:: android.runnable - -:class:`Runnable` is a wrapper around the Java `Runnable -`_ 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. - - - -Service (``android.service``) ------------------------------ - -Services of an application are controlled through the class :class:`AndroidService`. - -.. module:: android.service - -.. class:: AndroidService(title, description) - - Run ``service/main.py`` from the application directory as a service. - - :param title: Notification title, default to 'Python service' - :param description: Notification text, default to 'Kivy Python service started' - :type title: str - :type description: str - - .. method:: start(arg) - - Start the service. - - :param arg: Argument to pass to a service, through the environment variable - ``PYTHON_SERVICE_ARGUMENT``. Defaults to '' - :type arg: str - - .. method:: stop() - - Stop the service. - -Application activity part example, ``main.py``: - -.. code-block:: python - - from android import AndroidService - - ... - - class ServiceExample(App): - - ... - - def start_service(self): - self.service = AndroidService('Sevice example', 'service is running') - self.service.start('Hello From Service') - - def stop_service(self): - self.service.stop() - -Application service part example, ``service/main.py``: - -.. code-block:: python - - import os - import time - - # get the argument passed - arg = os.getenv('PYTHON_SERVICE_ARGUMENT') - - while True: - # this will print 'Hello From Service' continually, even when the application is switched - print arg - time.sleep(1) - diff --git a/doc/source/old_toolchain/conf.py b/doc/source/old_toolchain/conf.py deleted file mode 100644 index 02498acb27..0000000000 --- a/doc/source/old_toolchain/conf.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Python for Android documentation build configuration file, created by -# sphinx-quickstart on Wed Jan 11 02:31:33 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Python for Android' -copyright = u'2012/2013, Kivy organisation' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '1.2' -# The full version, including alpha/beta/rc tags. -release = '1.2' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'PythonForAndroiddoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'PythonForAndroid.tex', u'Python for Android Documentation', - u'Mathieu Virbel', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'pythonforandroid', u'Python for Android Documentation', - [u'Mathieu Virbel'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'PythonForAndroid', u'Python for Android Documentation', - u'Mathieu Virbel', 'PythonForAndroid', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' diff --git a/doc/source/old_toolchain/contribute.rst b/doc/source/old_toolchain/contribute.rst deleted file mode 100644 index dcc0fbe7c6..0000000000 --- a/doc/source/old_toolchain/contribute.rst +++ /dev/null @@ -1,105 +0,0 @@ -Contribute -========== - -Extending Python for android native support -------------------------------------------- - -So, you want to get into python-for-android and extend what's available -to Python on Android ? - -Turns out it's not very complicated, here is a little introduction on how to go -about it. Without Pyjnius, the schema to access the Java API from Cython is:: - - [1] Cython -> [2] C JNI -> [3] Java - -Think about acceleration sensors: you want to get the acceleration -values in Python, but nothing is available natively. Lukcily you have -a Java API for that : the Google API is available here -http://developer.android.com/reference/android/hardware/Sensor.html - -You can't use it directly, you need to do your own API to use it in python, -this is done in 3 steps - -Step 1: write the java code to create very simple functions to use -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -like : accelerometer Enable/Reading -In our project, this is done in the Hardware.java: -https://github.com/kivy/python-for-android/blob/master/src/src/org/renpy/android/Hardware.java -you can see how it's implemented - -Step 2 : write a jni wrapper -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This is a C file to be able to invoke/call Java functions from C, in our case, -step 2 (and 3) are done in the android python module. The JNI part is done in -the android_jni.c: -https://github.com/kivy/python-for-android/blob/master/recipes/android/src/android_jni.c - -Step 3 : you have the java part, that you can call from the C -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can now do the Python extension around it, all the android python part is -done in -https://github.com/kivy/python-for-android/blob/master/recipes/android/src/android.pyx - -→ [python] android.accelerometer_reading ⇒ [C] android_accelerometer_reading -⇒ [Java] Hardware.accelerometer_reading() - -The jni part is really a C api to call java methods. a little bit hard to get -it with the syntax, but working with current example should be ok - -Example with bluetooth -~~~~~~~~~~~~~~~~~~~~~~ -Start directly from a fork of https://github.com/kivy/python-for-android - -The first step is to identify where and how they are doing it in sl4a, it's -really easy, because everything is already done as a client/server -client/consumer approach, for bluetooth, they have a "Bluetooth facade" in -java. - -http://code.google.com/p/android-scripting/source/browse/android/BluetoothFacade/src/com/googlecode/android_scripting/facade/BluetoothFacade.java - -You can learn from it, and see how is it's can be used as is, or if you can -simplify / remove stuff you don't want. - -From this point, create a bluetooth file in -python-for-android/tree/master/src/src/org/renpy/android in Java. - -Do a good API (enough simple to be able to write the jni in a very easy manner, -like, don't pass any custom java object in argument). - -Then write the JNI, and then the python part. - -3 steps, once you get it, the real difficult part is to write the java part :) - -Jni gottchas -~~~~~~~~~~~~ - -- package must be org.renpy.android, don't change it. - - -Create your own recipes ------------------------ - -A recipe is a script that contains the "definition" of a module to compile. -The directory layout of a recipe for a is something like:: - - python-for-android/recipes//recipe.sh - python-for-android/recipes//patches/ - python-for-android/recipes//patches/fix-path.patch - -When building, all the recipe builds must go to:: - - python-for-android/build// - -For example, if you want to create a recipe for sdl, do:: - - cd python-for-android/recipes - mkdir sdl - cp recipe.sh.tmpl sdl/recipe.sh - sed -i 's#XXX#sdl#' sdl/recipe.sh - -Then, edit the sdl/recipe.sh to adjust other information (version, url) and -complete the build function. - diff --git a/doc/source/old_toolchain/customize.rst b/doc/source/old_toolchain/customize.rst deleted file mode 100644 index 5b9d954d27..0000000000 --- a/doc/source/old_toolchain/customize.rst +++ /dev/null @@ -1,30 +0,0 @@ -Customize your distribution ---------------------------- - -The basic layout of a distribution is:: - - AndroidManifest.xml - (*) android manifest (generated from templates) - assets/ - private.mp3 - (*) fake package that will contain all the python installation - public.mp3 - (*) fake package that will contain your application - bin/ - contain all the apk generated from build.py - blacklist.txt - list of file patterns to not include in the APK - buildlib/ - internals libraries for build.py - build.py - build script to use for packaging your application - build.xml - (*) build settings (generated from templates) - default.properties - settings generated from your distribute.sh - libs/ - contain all the compiled libraries - local.properties - settings generated from your distribute.sh - private/ - private directory containing all the python files - lib/ this is where you can remove or add python libs. - python2.7/ by default, some modules are already removed (tests, idlelib, ...) - project.properties - settings generated from your distribute.sh - python-install/ - the whole python installation, generated from distribute.sh - not included in the final package. - res/ - (*) android resource (generated from build.py) - src/ - Java bootstrap - templates/ - Templates used by build.py - - (*): Theses files are automatically generated from build.py, don't change them directly ! - - diff --git a/doc/source/old_toolchain/example_compass.rst b/doc/source/old_toolchain/example_compass.rst deleted file mode 100644 index bff430eafe..0000000000 --- a/doc/source/old_toolchain/example_compass.rst +++ /dev/null @@ -1,61 +0,0 @@ -Compass -------- - -The following example is an extract from the Compass app as provided in the Kivy -`examples/android/compass `__ -folder: - -.. code-block:: python - - # ... imports - Hardware = autoclass('org.renpy.android.Hardware') - - class CompassApp(App): - - needle_angle = NumericProperty(0) - - def build(self): - self._anim = None - Hardware.magneticFieldSensorEnable(True) - Clock.schedule_interval(self.update_compass, 1 / 10.) - - def update_compass(self, *args): - # read the magnetic sensor from the Hardware class - (x, y, z) = Hardware.magneticFieldSensorReading() - - # calculate the angle - needle_angle = Vector(x , y).angle((0, 1)) + 90. - - # animate the needle - if self._anim: - self._anim.stop(self) - self._anim = Animation(needle_angle=needle_angle, d=.2, t='out_quad') - self._anim.start(self) - - def on_pause(self): - # when you are going on pause, don't forget to stop the sensor - Hardware.magneticFieldSensorEnable(False) - return True - - def on_resume(self): - # reactivate the sensor when you are back to the app - Hardware.magneticFieldSensorEnable(True) - - if __name__ == '__main__': - CompassApp().run() - - -If you compile this app, you will get an APK which outputs the following -screen: - -.. figure:: Screenshot_Kivy_Kompass.png - :width: 100% - :scale: 60% - :figwidth: 80% - :alt: Screenshot Kivy Compass - - Screenshot of the Kivy Compass App - (Source of the Compass Windrose: `Wikipedia `__) - - - diff --git a/doc/source/old_toolchain/example_helloworld.rst b/doc/source/old_toolchain/example_helloworld.rst deleted file mode 100644 index dab760ad4b..0000000000 --- a/doc/source/old_toolchain/example_helloworld.rst +++ /dev/null @@ -1,96 +0,0 @@ -Hello world ------------ - -If you don't know how to start with Python for Android, here is a simple -tutorial for creating an UI using `Kivy `_, and make an APK -with this project. - -.. note:: - - Don't forget that Python for Android is not Kivy only, and you - might want to use other toolkit libraries. When other toolkits - will be available, this documentation will be enhanced. - -Let's create a simple Hello world application, with one Label and one Button. - -#. Ensure you've correctly installed and configured the project as said in the - :doc:`prerequisites` - -#. Create a directory named ``helloworld``:: - - mkdir helloworld - cd helloworld - -#. Create a file named ``main.py``, with this content:: - - import kivy - kivy.require('1.0.9') - from kivy.lang import Builder - from kivy.uix.gridlayout import GridLayout - from kivy.properties import NumericProperty - from kivy.app import App - - Builder.load_string(''' - : - cols: 1 - Label: - text: 'Welcome to the Hello world' - Button: - text: 'Click me! %d' % root.counter - on_release: root.my_callback() - ''') - - class HelloWorldScreen(GridLayout): - counter = NumericProperty(0) - def my_callback(self): - print 'The button has been pushed' - self.counter += 1 - - class HelloWorldApp(App): - def build(self): - return HelloWorldScreen() - - if __name__ == '__main__': - HelloWorldApp().run() - -#. Go to the ``python-for-android`` directory - -#. Create a distribution with kivy:: - - ./distribute.sh -m kivy - -#. Go to the newly created ``default`` distribution:: - - cd dist/default - -#. Plug your android device, and ensure you can install development - application - -#. Build your hello world application in debug mode:: - - ./build.py --package org.hello.world --name "Hello world" \ - --version 1.0 --dir /PATH/TO/helloworld debug installd - -#. Take your device, and start the application! - -#. If something goes wrong, open the logcat by doing:: - - adb logcat - -The final debug APK will be located in ``bin/hello-world-1.0-debug.apk``. - -If you want to release your application instead of just making a debug APK, you must: - -#. Generate a non-signed APK:: - - ./build.py --package org.hello.world --name "Hello world" \ - --version 1.0 --dir /PATH/TO/helloworld release - -#. Continue by reading http://developer.android.com/guide/publishing/app-signing.html - - -.. seealso:: - - `Kivy demos `_ - You can use them for creating APK too. - diff --git a/doc/source/old_toolchain/examples.rst b/doc/source/old_toolchain/examples.rst deleted file mode 100644 index 4d7408fa0c..0000000000 --- a/doc/source/old_toolchain/examples.rst +++ /dev/null @@ -1,21 +0,0 @@ -Examples -======== - -Prebuilt VirtualBox -------------------- - -A good starting point to build an APK are prebuilt VirtualBox images, -where the Android NDK, the Android SDK, and the Kivy -Python-For-Android sources are prebuilt in an VirtualBox image. Please -search the `Download Section `__ for such -an image. You will also need to create a device filter for the Android -USB device using the VirtualBox OS settings. - -.. include:: example_helloworld.rst -.. include:: example_compass.rst - -.. toctree:: - :hidden: - - example_helloworld - example_compass diff --git a/doc/source/old_toolchain/faq.rst b/doc/source/old_toolchain/faq.rst deleted file mode 100644 index 22ffe11512..0000000000 --- a/doc/source/old_toolchain/faq.rst +++ /dev/null @@ -1,41 +0,0 @@ -FAQ -=== - -arm-linux-androideabi-gcc: Internal error: Killed (program cc1) ---------------------------------------------------------------- - -This could happen if you are not using a validated SDK/NDK with Python for -Android. Go to :doc:`prerequisites` to see which one are working. - -_sqlite3.so not found ---------------------- - -We recently fixed sqlite3 compilation. In case of this error, you -must: - -* Install development headers for sqlite3 if they are not already - installed. On Ubuntu: - - apt-get install libsqlite3-dev - -* Compile the distribution with (sqlite3 must be the first argument):: - - ./distribute.sh -m 'sqlite3 kivy' - -* Go into your distribution at `dist/default` -* Edit blacklist.txt, and remove all the lines concerning sqlite3:: - - sqlite3/* - lib-dynload/_sqlite3.so - -Then sqlite3 will be compiled and included in your APK. - -Too many levels of symbolic links ------------------------------------------------------ - -Python for Android does not work within a virtual enviroment. The Python for -Android directory must be outside of the virtual enviroment prior to running - - ./distribute.sh -m "kivy" - -or else you may encounter OSError: [Errno 40] Too many levels of symbolic links. \ No newline at end of file diff --git a/doc/source/old_toolchain/index.rst b/doc/source/old_toolchain/index.rst deleted file mode 100644 index 1b873be657..0000000000 --- a/doc/source/old_toolchain/index.rst +++ /dev/null @@ -1,38 +0,0 @@ - -Old p4a toolchain doc -===================== - -This is the documentation for the old python-for-android toolchain, -using distribute.sh and build.py. This it entirely superseded by the -new toolchain, you do not need to read it unless using this old -method. - -.. warning:: The old toolchain is deprecated and no longer - supported. You should instead use the :doc:`current version - <../quickstart>`. - -Python for android is a project to create your own Python distribution -including the modules you want, and create an apk including python, libs, and -your application. - -- Forum: https://groups.google.com/forum/#!forum/python-android -- Mailing list: python-android@googlegroups.com - -.. toctree:: - :maxdepth: 2 - - toolchain.rst - examples.rst - android.rst - javaapi.rst - contribute.rst - related.rst - faq.rst - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/doc/source/old_toolchain/javaapi.rst b/doc/source/old_toolchain/javaapi.rst deleted file mode 100644 index 73fc888f8e..0000000000 --- a/doc/source/old_toolchain/javaapi.rst +++ /dev/null @@ -1,239 +0,0 @@ -Java API (pyjnius) -================== - -Using `PyJNIus `__ to access the Android API -restricts the usage to a simple call of the **autoclass** constructor function -and a second call to instantiate this class. - -You can access through this method the entire Java Android API, e.g., -the ``DisplayMetrics`` of an Android device could be fetched using the -following piece of code: - -.. code-block:: python - - DisplayMetrics = autoclass('android.util.DisplayMetrics') - metrics = DisplayMetrics() - metrics.setToDefaults() - self.densityDpi = metrics.densityDpi - -You can access all fields and methods as described in the `Java -Android DisplayMetrics API -`__ -as shown here with the method `setToDefaults()` and the field -`densityDpi`. Before you use a view field, you should always call -`setToDefaults` to initiate to the default values of the device. - -Currently only JavaMethod, JavaStaticMethod, JavaField, -JavaStaticField and JavaMultipleMethod are built into PyJNIus, -therefore such constructs like registerListener or something like this -must still be coded in Java. For this the Android module described -below is available to access some of the hardware on Android devices. - -.. module:: org.renpy.android - - -Activity --------- - -If you want the instance of the current Activity, use: - -- :data:`PythonActivity.mActivity` if you are running an application -- :data:`PythonService.mService` if you are running a service - -.. class:: PythonActivity - - .. data:: mInfo - - Instance of an `ApplicationInfo - `_ - - .. data:: mActivity - - Instance of :class:`PythonActivity`. - - .. method:: registerNewIntentListener(NewIntentListener listener) - - Register a new instance of :class:`NewIntentListener` to be called when - `onNewIntent - `_ - is called. - - .. method:: unregisterNewIntentListener(NewIntentListener listener) - - Unregister a previously registered listener from - :meth:`registerNewIntentListener` - - .. method:: registerActivityResultListener(ActivityResultListener listener) - - Register a new instance of :class:`ActivityResultListener` to be called - when `onActivityResult - `_ is called. - - .. method:: unregisterActivityResultListener(ActivityResultListener listener) - - Unregister a previously registered listener from - :meth:`PythonActivity.registerActivityResultListener` - -.. class:: PythonActivity_ActivityResultListener - - .. note:: - - This class is a subclass of PythonActivity, so the notation will be - ``PythonActivity$ActivityResultListener`` - - Listener interface for onActivityResult. You need to implementing it, - create an instance and use it with :meth:`PythonActivity.registerActivityResultListener`. - - .. method:: onActivityResult(int requestCode, int resultCode, Intent data) - - Method to implement - -.. class:: PythonActivity_NewIntentListener - - .. note:: - - This class is a subclass of PythonActivity, so the notation will be - ``PythonActivity$NewIntentListener`` - - Listener interface for onNewIntent. You need to implementing it, create - an instance and use it with :meth:`registerNewIntentListener`. - - .. method:: onNewIntent(Intent intent) - - Method to implement - - -Action ------- - -.. class:: Action - - This module is built to deliver data to someone else. - - .. method:: send(mimetype, filename, subject, text, chooser_title) - - Deliver data to someone else. This method is a wrapper around `ACTION_SEND - `_ - - :Parameters: - `mimetype`: str - Must be a valid mimetype, that represent the content to sent. - `filename`: str, default to None - (optional) Name of the file to attach. Must be a absolute path. - `subject`: str, default to None - (optional) Default subject - `text`: str, default to None - (optional) Content to send. - `chooser_title`: str, default to None - (optional) Title of the android chooser window, default to 'Send email...' - - Sending a simple hello world text:: - - android.action_send('text/plain', text='Hello world', - subject='Test from python') - - Sharing an image file:: - - # let's say you've make an image in /sdcard/image.png - android.action_send('image/png', filename='/sdcard/image.png') - - Sharing an image with a default text too:: - - android.action_send('image/png', filename='/sdcard/image.png', - text='Hi,\n\tThis is my awesome image, what do you think about it ?') - - -Hardware --------- - -.. class:: Hardware - - This module is built for accessing hardware devices of an Android device. - All the methods are static and public, you don't need an instance. - - - .. method:: vibrate(s) - - Causes the phone to vibrate for `s` seconds. This requires that your - application have the VIBRATE permission. - - - .. method:: getHardwareSensors() - - Returns a string of all hardware sensors of an Android device where each - line lists the informations about one sensor in the following format: - - Name=name,Vendor=vendor,Version=version,MaximumRange=maximumRange,MinDelay=minDelay,Power=power,Type=type - - For more information about this informations look into the original Java - API for the `Sensors Class - `__ - - .. attribute:: accelerometerSensor - - This variable links to a generic3AxisSensor instance and their functions to - access the accelerometer sensor - - .. attribute:: orientationSensor - - This variable links to a generic3AxisSensor instance and their functions to - access the orientation sensor - - .. attribute:: magenticFieldSensor - - - The following two instance methods of the generic3AxisSensor class should be - used to enable/disable the sensor and to read the sensor - - - .. method:: changeStatus(boolean enable) - - Changes the status of the sensor, the status of the sensor is enabled, - if `enable` is true or disabled, if `enable` is false. - - .. method:: readSensor() - - Returns an (x, y, z) tuple of floats that gives the sensor reading, the - units depend on the sensor as shown on the Java API page for - `SensorEvent - `_. - The sesnor must be enabled before this function is called. If the tuple - contains three zero values, the accelerometer is not enabled, not - available, defective, has not returned a reading, or the device is in - free-fall. - - .. method:: get_dpi() - - Returns the screen density in dots per inch. - - .. method:: show_keyboard() - - Shows the soft keyboard. - - .. method:: hide_keyboard() - - Hides the soft keyboard. - - .. method:: wifi_scanner_enable() - - Enables wifi scanning. - - .. note:: - - ACCESS_WIFI_STATE and CHANGE_WIFI_STATE permissions are required. - - .. method:: wifi_scan() - - Returns a String for each visible WiFi access point - - (SSID, BSSID, SignalLevel) - -Further Modules -~~~~~~~~~~~~~~~ - -Some further modules are currently available but not yet documented. Please -have a look into the code and you are very welcome to contribute to this -documentation. - - diff --git a/doc/source/old_toolchain/prerequisites.rst b/doc/source/old_toolchain/prerequisites.rst deleted file mode 100644 index eb5cd6dcd0..0000000000 --- a/doc/source/old_toolchain/prerequisites.rst +++ /dev/null @@ -1,79 +0,0 @@ -Prerequisites -------------- - -.. note:: There is a VirtualBox Image we provide with the - prerequisites along with the Android SDK and NDK preinstalled to - ease your installation woes. You can download it from `here - `__. - -.. warning:: - - The current version is tested only on Ubuntu oneiric (11.10) and - precise (12.04). If it doesn't work on other platforms, send us a - patch, not a bug report. Python for Android works on Linux and Mac - OS X, not Windows. - -You need the minimal environment for building python. Note that other -libraries might need other tools (cython is used by some recipes, and -ccache to speedup the build):: - - sudo apt-get install build-essential patch git-core ccache ant python-pip python-dev - -If you are on a 64 bit distro, you should install these packages too :: - - sudo apt-get install ia32-libs libc6-dev-i386 - -On debian Squeeze amd64, those packages were found to be necessary :: - - sudo apt-get install lib32stdc++6 lib32z1 - -Ensure you have the latest Cython version:: - - pip install --upgrade cython - -You must have android SDK and NDK. The SDK defines the Android -functions you can use. The NDK is used for compilation. Right now, -it's preferred to use: - -- SDK API 8 or 14 (15 will only work with a newly released NDK) -- NDK r5b or r7 - -You can download them at:: - - http://developer.android.com/sdk/index.html - http://developer.android.com/sdk/ndk/index.html - - -In general, Python for Android currently works with Android 2.3 to L. - -If it's your very first time using the Android SDK, don't forget to -follow the documentation for recommended components at:: - - http://developer.android.com/sdk/installing/adding-packages.html - - You need to download at least one platform into your environment, so - that you will be able to compile your application and set up an Android - Virtual Device (AVD) to run it on (in the emulator). To start with, - just download the latest version of the platform. Later, if you plan to - publish your application, you will want to download other platforms as - well, so that you can test your application on the full range of - Android platform versions that your application supports. - -After installing them, export both installation paths, NDK version, -and API to use:: - - export ANDROIDSDK=/path/to/android-sdk - export ANDROIDNDK=/path/to/android-ndk - export ANDROIDNDKVER=rX - export ANDROIDAPI=X - - # example - export ANDROIDSDK="/home/tito/code/android/android-sdk-linux_86" - export ANDROIDNDK="/home/tito/code/android/android-ndk-r7" - export ANDROIDNDKVER=r7 - export ANDROIDAPI=14 - -Also, you must configure your PATH to add the ``android`` binary:: - - export PATH=$ANDROIDNDK:$ANDROIDSDK/platform-tools:$ANDROIDSDK/tools:$PATH - diff --git a/doc/source/old_toolchain/related.rst b/doc/source/old_toolchain/related.rst deleted file mode 100644 index ea694f619c..0000000000 --- a/doc/source/old_toolchain/related.rst +++ /dev/null @@ -1,7 +0,0 @@ -Related projects -================ - -- PGS4A: http://pygame.renpy.org/ (thanks to Renpy to make it possible) -- Android scripting: http://code.google.com/p/android-scripting/ -- Python on a chip: http://code.google.com/p/python-on-a-chip/ - diff --git a/doc/source/old_toolchain/toolchain.rst b/doc/source/old_toolchain/toolchain.rst deleted file mode 100644 index b301a5a9f5..0000000000 --- a/doc/source/old_toolchain/toolchain.rst +++ /dev/null @@ -1,66 +0,0 @@ -Toolchain -========= - -Introduction ------------- - -In terms of comparaison, you can check how Python for android can be useful -compared to other projects. - -+--------------------+---------------+---------------+----------------+--------------+ -| Project | Native Python | GUI libraries | APK generation | Custom build | -+====================+===============+===============+================+==============+ -| Python for android | Yes | Yes | Yes | Yes | -+--------------------+---------------+---------------+----------------+--------------+ -| PGS4A | Yes | Yes | Yes | No | -+--------------------+---------------+---------------+----------------+--------------+ -| Android scripting | No | No | No | No | -+--------------------+---------------+---------------+----------------+--------------+ -| Python on a chip | No | No | No | No | -+--------------------+---------------+---------------+----------------+--------------+ - -.. note:: - - For the moment, we are shipping only one "java bootstrap" (needed for - decompressing your packaged zip file project, create an OpenGL ES 2.0 - surface, handle touch input and manage an audio thread). - - If you want to use it without kivy module (an opengl es 2.0 ui toolkit), - then you might want a lighter java bootstrap, that we don't have right now. - Help is welcome :) - - So for the moment, Python for Android can only be used with the kivy GUI toolkit: - http://kivy.org/#home - - -How does it work ? ------------------- - -To be able to run Python on android, you need to compile it for android. And -you need to compile all the libraries you want for android too. -Since Python is a language, not a toolkit, you cannot draw any user interface -with it: you need to use a toolkit for it. Kivy can be one of them. - -So for a simple ui project, the first step is to compile Python + Kivy + all -others libraries. Then you'll have what we call a "distribution". -A distribution is composed of: - -- Python -- Python libraries -- All selected libraries (kivy, pygame, pil...) -- A java bootstrap -- A build script - -You'll use the build script for create an "apk": an android package. - - -.. include:: prerequisites.rst -.. include:: usage.rst -.. include:: customize.rst - -.. toctree:: - :hidden: - - prerequisites - usage - customize diff --git a/doc/source/old_toolchain/usage.rst b/doc/source/old_toolchain/usage.rst deleted file mode 100644 index 7d83b23888..0000000000 --- a/doc/source/old_toolchain/usage.rst +++ /dev/null @@ -1,167 +0,0 @@ -Usage ------ - -Step 1: compile the toolchain -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want to compile the toolchain with only the kivy module:: - - ./distribute.sh -m "kivy" - -.. warning:: - Do not run the above command from `within a virtual enviroment <../faq/#too-many-levels-of-symbolic-links>`_. - -After a long time, you'll get a "dist/default" directory containing -all the compiled libraries and a build.py script to package your -application using thoses libraries. - -You can include other modules (or "recipes") to compile using `-m`:: - - ./distribute.sh -m "openssl kivy" - ./distribute.sh -m "pil ffmpeg kivy" - -.. note:: - - Recipes are instructions for compiling Python modules that require C extensions. - The list of recipes we currently have is at: - https://github.com/kivy/python-for-android/tree/master/recipes - -You can also specify a specific version for each package. Please note -that the compilation might **break** if you don't use the default -version. Most recipes have patches to fix Android issues, and might -not apply if you specify a version. We also recommend to clean build -before changing version.:: - - ./distribute.sh -m "openssl kivy==master" - -Python modules that don't need C extensions don't need a recipe and -can be included this way. From python-for-android 1.1 on, you can now -specify pure-python package into the distribution. It will use -virtualenv and pip to install pure-python modules into the -distribution. Please note that the compiler is deactivated, and will -break any module which tries to compile something. If compilation is -needed, write a recipe:: - - ./distribute.sh -m "requests pygments kivy" - -.. note:: - - Recipes download a defined version of their needed package from the - internet, and build from it. If you know what you are doing, and - want to override that, you can export the env variable - `P4A_recipe_name_DIR` and this directory will be copied and used - instead. - -Available options to `distribute.sh`:: - - -d directory Name of the distribution directory - -h Show this help - -l Show a list of available modules - -m 'mod1 mod2' Modules to include - -f Restart from scratch (remove the current build) - -u 'mod1 mod2' Modules to update (if already compiled) - -Step 2: package your application -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Go to your custom Python distribution:: - - cd dist/default - -Use the build.py for creating the APK:: - - ./build.py --package org.test.touchtracer --name touchtracer \ - --version 1.0 --dir ~/code/kivy/examples/demo/touchtracer debug - -Then, the Android package (APK) will be generated at: - - bin/touchtracer-1.0-debug.apk - -.. warning:: - - Some files and modules for python are blacklisted by default to - save a few megabytes on the final APK file. In case your - applications doesn't find a standard python module, check the - src/blacklist.txt file, remove the module you need from the list, - and try again. - -Available options to `build.py`:: - - -h, --help show this help message and exit - --package PACKAGE The name of the java package the project will be - packaged under. - --name NAME The human-readable name of the project. - --version VERSION The version number of the project. This should consist - of numbers and dots, and should have the same number - of groups of numbers as previous versions. - --numeric-version NUMERIC_VERSION - The numeric version number of the project. If not - given, this is automatically computed from the - version. - --dir DIR The directory containing public files for the project. - --private PRIVATE The directory containing additional private files for - the project. - --launcher Provide this argument to build a multi-app launcher, - rather than a single app. - --icon-name ICON_NAME - The name of the project's launcher icon. - --orientation ORIENTATION - The orientation that the game will display in. Usually - one of "landscape", "portrait" or "sensor". - --permission PERMISSIONS - The permissions to give this app. - --ignore-path IGNORE_PATH - Ignore path when building the app - --icon ICON A png file to use as the icon for the application. - --presplash PRESPLASH - A jpeg file to use as a screen while the application - is loading. - --install-location INSTALL_LOCATION - The default install location. Should be "auto", - "preferExternal" or "internalOnly". - --compile-pyo Compile all .py files to .pyo, and only distribute the - compiled bytecode. - --intent-filters INTENT_FILTERS - Add intent-filters xml rules to AndroidManifest.xml - --blacklist BLACKLIST - Use a blacklist file to match unwanted file in the - final APK - --sdk SDK_VERSION Android SDK version to use. Default to 8 - --minsdk MIN_SDK_VERSION - Minimum Android SDK version to use. Default to 8 - --window Indicate if the application will be windowed - -Meta-data ---------- - -.. versionadded:: 1.3 - -You can extend the `AndroidManifest.xml` with application meta-data. If you are -using external toolkits like Google Maps, you might want to set your API key in -the meta-data. You could do it like this:: - - ./build.py ... --meta-data com.google.android.maps.v2.API_KEY=YOURAPIKEY - -Some meta-data can be used to interact with the behavior of our internal -component. - -.. list-table:: - :widths: 100 500 - :header-rows: 1 - - * - Token - - Description - * - `surface.transparent` - - If set to 1, the created surface will be transparent (can be used - to add background Android widget in the background, or use accelerated - widgets) - * - `surface.depth` - - Size of the depth component, default to 0. 0 means automatic, but you - can force it to a specific value. Be warned, some old phone might not - support the depth you want. - * - `surface.stencil` - - Size of the stencil component, default to 8. - * - `android.background_color` - - Color (32bits RGBA color), used for the background window. Usually, the - background is covered by the OpenGL Background, unless - `surface.transparent` is set. diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 3797ffacec..967d6ed755 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -55,7 +55,7 @@ p4a has several dependencies that must be installed: - ant - python2 - cython (can be installed via pip) -- a Java JDK (e.g. openjdk-7) +- a Java JDK (e.g. openjdk-8) - zlib (including 32 bit) - libncurses (including 32 bit) - unzip @@ -70,7 +70,7 @@ install most of these with:: sudo dpkg --add-architecture i386 sudo apt-get update - sudo apt-get install -y build-essential ccache git zlib1g-dev python2.7 python2.7-dev libncurses5:i386 libstdc++6:i386 zlib1g:i386 openjdk-7-jdk unzip ant ccache autoconf libtool + sudo apt-get install -y build-essential ccache git zlib1g-dev python2.7 python2.7-dev libncurses5:i386 libstdc++6:i386 zlib1g:i386 openjdk-8-jdk unzip ant ccache autoconf libtool On Arch Linux (64 bit) you should be able to run the following to install most of the dependencies (note: this list may not be @@ -115,8 +115,10 @@ the latest usable NDK version is r10e, which can be downloaded here: release with the legacy version of python is version `0.6.0 `_. -First, install a platform to target (you can also replace ``27`` with -a different platform number, this will be used again later):: +First, install an API platform to target. You can replace ``27`` with +a different platform number, but keep in mind **other API versions +are less well-tested**, and older devices are still supported +(down to the specified *minimum* API/NDK API level): $SDK_DIR/tools/bin/sdkmanager "platforms;android-27" @@ -147,18 +149,34 @@ You have the possibility to configure on any command the PATH to the SDK, NDK an Usage ----- -Build a Kivy application -~~~~~~~~~~~~~~~~~~~~~~~~ +Build a Kivy or SDL2 application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To build your application, you need to have a name, version, a package -identifier, and explicitly write the bootstrap you want to use, as -well as the requirements:: +To build your application, you need to specify name, version, a package +identifier, the bootstrap you want to use (`sdl2` for kivy or sdl2 apps) +and the requirements:: + + p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My application" --version 0.1 --bootstrap=sdl2 --requirements=python3,kivy - p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My application" --version 0.1 --bootstrap=sdl2 --requirements=python2,kivy +**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 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. +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. -You can also use ``--bootstrap=pygame``, but this bootstrap is deprecated for use with Kivy and SDL2 is preferred. +*Compatibility notes:* + +- While python2 is still supported by python-for-android, + it will possibly no longer receive patches by the python creators + themselves in 2020. Migration to Python 3 is recommended! + +- You can also use ``--bootstrap=pygame``, but this bootstrap + is deprecated and not well-tested. Build a WebView application ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -169,26 +187,14 @@ well as the requirements:: p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My WebView Application" --version 0.1 --bootstrap=webview --requirements=flask --port=5000 +**Please note as with kivy/SDL2, you need to specify all your +additional requirements/depenencies.** + You can also replace flask with another web framework. Replace ``--port=5000`` with the port on which your app will serve a website. The default for Flask is 5000. -Build an SDL2 based application -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This includes e.g. `PySDL2 -`__. - -To build your application, you need to have a name, version, a package -identifier, and explicitly write the sdl2 bootstrap, as well as the -requirements:: - - p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My SDL2 application" --version 0.1 --bootstrap=sdl2 --requirements=your_requirements - -Add your required modules in place of ``your_requirements``, -e.g. ``--requirements=pysdl2`` or ``--requirements=vispy``. - Other options ~~~~~~~~~~~~~ @@ -196,7 +202,7 @@ You can pass other command line arguments to control app behaviours such as orientation, wakelock and app permissions. See :ref:`bootstrap_build_options`. - + Rebuild everything ~~~~~~~~~~~~~~~~~~ @@ -204,11 +210,11 @@ Rebuild everything If anything goes wrong and you want to clean the downloads and builds to retry everything, run:: p4a clean_all - + If you just want to clean the builds to avoid redownloading dependencies, run:: p4a clean_builds && p4a clean_dists - + Getting help ~~~~~~~~~~~~ @@ -267,7 +273,7 @@ You can list the available distributions:: And clean all of them:: p4a clean_dists - + Configuration file ~~~~~~~~~~~~~~~~~~ @@ -280,6 +286,17 @@ include such as:: --android_api 27 --requirements kivy,openssl +Overriding recipes sources +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can override the source of any recipe using the +``$P4A_recipename_DIR`` environment variable. For instance, to test +your own Kivy branch you might set:: + + export P4A_kivy_DIR=/home/username/kivy + +The specified directory will be copied into python-for-android instead +of downloading from the normal url specified in the recipe. Going further ~~~~~~~~~~~~~ diff --git a/pythonforandroid/__init__.py b/pythonforandroid/__init__.py index 1f199f195d..3699b0d245 100644 --- a/pythonforandroid/__init__.py +++ b/pythonforandroid/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.6.0' +__version__ = '0.7.1' diff --git a/pythonforandroid/archs.py b/pythonforandroid/archs.py index 8e897dbc6e..ec5a6fb3d5 100644 --- a/pythonforandroid/archs.py +++ b/pythonforandroid/archs.py @@ -1,11 +1,10 @@ +from distutils.spawn import find_executable +from os import environ from os.path import (exists, join, dirname, split) -from os import environ, uname from glob import glob -import sys -from distutils.spawn import find_executable from pythonforandroid.recipe import Recipe -from pythonforandroid.util import BuildInterruptingException +from pythonforandroid.util import BuildInterruptingException, build_platform class Arch(object): @@ -20,6 +19,12 @@ def __init__(self, ctx): super(Arch, self).__init__() self.ctx = ctx + # Allows injecting additional linker paths used by any recipe. + # This can also be modified by recipes (like the librt recipe) + # to make sure that some sort of global resource is available & + # linked for all others. + self.extra_global_link_paths = [] + def __str__(self): return self.arch @@ -51,11 +56,18 @@ def get_env(self, with_flags_in_cc=True, clang=False): toolchain = '{android_host}-{toolchain_version}'.format( android_host=self.ctx.toolchain_prefix, toolchain_version=self.ctx.toolchain_version) - toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain, 'prebuilt', 'linux-x86_64') + toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain, + 'prebuilt', build_platform) cflags.append('-gcc-toolchain {}'.format(toolchain)) env['CFLAGS'] = ' '.join(cflags) - env['LDFLAGS'] = ' ' + + # Link the extra global link paths first before anything else + # (such that overriding system libraries with them is possible) + env['LDFLAGS'] = ' ' + " ".join([ + "-L'" + l.replace("'", "'\"'\"'") + "'" # no shlex.quote in py2 + for l in self.extra_global_link_paths + ]) + ' ' sysroot = join(self.ctx._ndk_dir, 'sysroot') if exists(sysroot): @@ -83,10 +95,6 @@ def get_env(self, with_flags_in_cc=True, clang=False): if self.ctx.ndk == 'crystax': env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch) - py_platform = sys.platform - if py_platform in ['linux2', 'linux3']: - py_platform = 'linux' - toolchain_prefix = self.ctx.toolchain_prefix toolchain_version = self.ctx.toolchain_version command_prefix = self.command_prefix @@ -106,7 +114,7 @@ def get_env(self, with_flags_in_cc=True, clang=False): llvm_dirname = split( glob(join(self.ctx.ndk_dir, 'toolchains', 'llvm*'))[-1])[-1] clang_path = join(self.ctx.ndk_dir, 'toolchains', llvm_dirname, - 'prebuilt', 'linux-x86_64', 'bin') + 'prebuilt', build_platform, 'bin') environ['PATH'] = '{clang_path}:{path}'.format( clang_path=clang_path, path=environ['PATH']) exe = join(clang_path, 'clang') @@ -159,8 +167,8 @@ def get_env(self, with_flags_in_cc=True, clang=False): 'host' + self.ctx.python_recipe.name, self.ctx) env['BUILDLIB_PATH'] = join( hostpython_recipe.get_build_dir(self.arch), - 'build', 'lib.linux-{}-{}'.format( - uname()[-1], 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'] diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py old mode 100644 new mode 100755 index 2304f281ff..b4a9a9e4c2 --- a/pythonforandroid/bootstrap.py +++ b/pythonforandroid/bootstrap.py @@ -1,6 +1,7 @@ from os.path import (join, dirname, isdir, normpath, splitext, basename) from os import listdir, walk, sep import sh +import shlex import glob import importlib import os @@ -9,7 +10,7 @@ from pythonforandroid.logger import (warning, shprint, info, logger, debug) from pythonforandroid.util import (current_directory, ensure_dir, - temp_directory, which) + temp_directory) from pythonforandroid.recipe import Recipe @@ -48,7 +49,11 @@ class Bootstrap(object): dist_name = None distribution = None - recipe_depends = ['sdl2'] + # All bootstraps should include Python in some way: + recipe_depends = [ + ("python2", "python2legacy", "python3", "python3crystax"), + 'android', + ] can_be_chosen_automatically = True '''Determines whether the bootstrap can be chosen as one that @@ -166,7 +171,7 @@ def get_bootstrap_from_recipes(cls, recipes, ctx): for recipe in recipes: try: recipe = Recipe.get_recipe(recipe, ctx) - except IOError: + except ValueError: conflicts = [] else: conflicts = recipe.conflicts @@ -174,7 +179,7 @@ def get_bootstrap_from_recipes(cls, recipes, ctx): for conflict in conflicts]): ok = False break - if ok: + if ok and bs not in acceptable_bootstraps: acceptable_bootstraps.append(bs) info('Found {} acceptable bootstraps: {}'.format( len(acceptable_bootstraps), @@ -263,11 +268,10 @@ def strip_libraries(self, arch): info('Python was loaded from CrystaX, skipping strip') return env = arch.get_env() - strip = which('arm-linux-androideabi-strip', env['PATH']) - if strip is None: - warning('Can\'t find strip in PATH...') - return - strip = sh.Command(strip) + tokens = shlex.split(env['STRIP']) + strip = sh.Command(tokens[0]) + if len(tokens) > 1: + strip = strip.bake(tokens[1:]) libs_dir = join(self.dist_dir, '_python_bundle', '_python_bundle', 'modules') @@ -278,6 +282,8 @@ def strip_libraries(self, arch): logger.info('Stripping libraries in private dir') for filen in filens.split('\n'): + if not filen: + continue # skip the last '' try: strip(filen, _env=env) except sh.ErrorReturnCode_1: diff --git a/pythonforandroid/bootstraps/common/build/build.py b/pythonforandroid/bootstraps/common/build/build.py index e55676f7fb..f455e06d64 100644 --- a/pythonforandroid/bootstraps/common/build/build.py +++ b/pythonforandroid/bootstraps/common/build/build.py @@ -14,6 +14,7 @@ import subprocess import sys import tarfile +import tempfile import time from zipfile import ZipFile @@ -22,16 +23,28 @@ import jinja2 -def get_bootstrap_name(): +def get_dist_info_for(key): try: with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh: info = json.load(fileh) - bootstrap = str(info["bootstrap"]) + value = str(info[key]) except (OSError, KeyError) as e: - print("BUILD FAILURE: Couldn't extract bootstrap name " + + print("BUILD FAILURE: Couldn't extract the key `" + key + "` " + "from dist_info.json: " + str(e)) sys.exit(1) - return bootstrap + return value + + +def get_hostpython(): + return get_dist_info_for('hostpython') + + +def get_python_version(): + return get_dist_info_for('python_version') + + +def get_bootstrap_name(): + return get_dist_info_for('bootstrap') if os.name == 'nt': @@ -43,9 +56,9 @@ def get_bootstrap_name(): curdir = dirname(__file__) -# Try to find a host version of Python that matches our ARM version. -PYTHON = join(curdir, 'python-install', 'bin', 'python.host') -if not exists(PYTHON): +PYTHON = get_hostpython() +PYTHON_VERSION = get_python_version() +if PYTHON is not None and not exists(PYTHON): PYTHON = None BLACKLIST_PATTERNS = [ @@ -55,17 +68,18 @@ def get_bootstrap_name(): '^*.bzr/*', '^*.svn/*', - # pyc/py - '*.pyc', - # '*.py', - # temp files '~', '*.bak', '*.swp', ] +# pyc/py if PYTHON is not None: BLACKLIST_PATTERNS.append('*.py') + if PYTHON_VERSION and int(PYTHON_VERSION[0]) == 2: + # we only blacklist `.pyc` for python2 because in python3 the compiled + # extension is `.pyc` (.pyo files not exists for python >= 3.6) + BLACKLIST_PATTERNS.append('*.pyc') WHITELIST_PATTERNS = [] if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'): @@ -236,16 +250,23 @@ def compile_dir(dfn, optimize_python=True): Compile *.py in directory `dfn` to *.pyo ''' - if get_bootstrap_name() != "sdl2": - # HISTORICALLY DISABLED for other than sdl2. NEEDS REVIEW! -JonasT - return - # -OO = strip docstrings if PYTHON is None: return - args = [PYTHON, '-m', 'compileall', '-f', dfn] + + if int(PYTHON_VERSION[0]) >= 3: + args = [PYTHON, '-m', 'compileall', '-b', '-f', dfn] + else: + args = [PYTHON, '-m', 'compileall', '-f', dfn] if optimize_python: + # -OO = strip docstrings args.insert(1, '-OO') - subprocess.call(args) + return_code = subprocess.call(args) + + if return_code != 0: + print('Error while running "{}"'.format(' '.join(args))) + print('This probably means one of your Python files has a syntax ' + 'error, see logs above') + exit(1) def make_package(args): @@ -273,8 +294,17 @@ def make_package(args): # construct a python27.zip make_python_zip() + # Add extra environment variable file into tar-able directory: + env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-") + with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f: + f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n") + if hasattr(args, "orientation"): + f.write("P4A_ORIENTATION=" + str(args.orientation) + "\n") + f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n") + f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n") + # Package up the private data (public not supported). - tar_dirs = [] + tar_dirs = [env_vars_tarpath] if args.private: tar_dirs.append(args.private) for python_bundle_dir in ('private', 'crystax_python', '_python_bundle'): @@ -287,6 +317,9 @@ def make_package(args): join(assets_dir, 'private.mp3'), tar_dirs, args.ignore_path, optimize_python=args.optimize_python) + # Remove extra env vars tar-able directory: + shutil.rmtree(env_vars_tarpath) + # Prepare some variables for templating process res_dir = "src/main/res" default_icon = 'templates/kivy-icon.png' @@ -308,7 +341,7 @@ def make_package(args): if not exists(jarname): print('Requested jar does not exist: {}'.format(jarname)) sys.exit(-1) - shutil.copy(jarname, 'libs') + shutil.copy(jarname, 'src/main/libs') jars.append(basename(jarname)) # If extra aar were requested, copy them into the libs directory @@ -336,9 +369,11 @@ def make_package(args): with open(args.intent_filters) as fd: args.intent_filters = fd.read() - # if get_bootstrap_name() == "sdl2": - args.add_activity = args.add_activity or [] - args.activity_launch_mode = args.activity_launch_mode or '' + if not args.add_activity: + args.add_activity = [] + + if not args.activity_launch_mode: + args.activity_launch_mode = '' if args.extra_source_dirs: esd = [] @@ -391,7 +426,7 @@ def make_package(args): target = fileh.read().strip() android_api = target.split('-')[1] try: - android_api_int_test = int(android_api) + int(android_api) except (ValueError, TypeError): raise ValueError( "failed to extract the Android API level from " + @@ -480,6 +515,31 @@ def make_package(args): if exists('build.properties'): os.remove('build.properties') + # Apply java source patches if any are present: + if exists(join('src', 'patches')): + print("Applying Java source code patches...") + for patch_name in os.listdir(join('src', 'patches')): + patch_path = join('src', 'patches', patch_name) + print("Applying patch: " + str(patch_path)) + try: + subprocess.check_output([ + # -N: insist this is FORWARd patch, don't reverse apply + # -p1: strip first path component + # -t: batch mode, don't ask questions + "patch", "-N", "-p1", "-t", "-i", patch_path + ]) + except subprocess.CalledProcessError as e: + if e.returncode == 1: + # Return code 1 means it didn't apply, this will + # usually mean it is already applied. + print("Warning: failed to apply patch (" + + "exit code 1), " + + "assuming it is already applied: " + + str(patch_path) + ) + else: + raise e + def parse_args(args=None): global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON @@ -506,7 +566,8 @@ def parse_args(args=None): # --private is required unless for sdl2, where there's also --launcher ap.add_argument('--private', dest='private', - help='the dir of user files', + help='the directory with the app source code files' + + ' (containing your main.py entrypoint)', required=(get_bootstrap_name() != "sdl2")) ap.add_argument('--package', dest='package', help=('The name of the java package the project will be' @@ -536,6 +597,9 @@ def parse_args(args=None): ap.add_argument('--icon', dest='icon', help=('A png file to use as the icon for ' 'the application.')) + ap.add_argument('--service', dest='services', action='append', + help='Declare a new service entrypoint: ' + 'NAME:PATH_TO_PY[:foreground]') if get_bootstrap_name() != "service_only": ap.add_argument('--presplash', dest='presplash', help=('A jpeg file to use as a screen while the ' @@ -564,10 +628,6 @@ def parse_args(args=None): 'https://developer.android.com/guide/' 'topics/manifest/' 'activity-element.html')) - else: - ap.add_argument('--service', dest='services', action='append', - help='Declare a new service entrypoint: ' - 'NAME:PATH_TO_PY[:foreground]') ap.add_argument('--wakelock', dest='wakelock', action='store_true', help=('Indicate if the application needs the device ' 'to stay on')) @@ -624,7 +684,9 @@ def parse_args(args=None): 'the appropriate environment variables.')) ap.add_argument('--add-activity', dest='add_activity', action='append', help='Add this Java class as an Activity to the manifest.') - ap.add_argument('--activity-launch-mode', dest='activity_launch_mode', + ap.add_argument('--activity-launch-mode', + dest='activity_launch_mode', + default='singleTask', help='Set the launch mode of the main activity in the manifest.') ap.add_argument('--allow-backup', dest='allow_backup', default='true', help="if set to 'false', then android won't backup the application.") @@ -687,7 +749,7 @@ def _read_configuration(): if args.meta_data is None: args.meta_data = [] - if (not hasattr(args, 'services')) or args.services is None: + if args.services is None: args.services = [] if args.try_system_python_compile: diff --git a/pythonforandroid/bootstraps/common/build/jni/application/src/start.c b/pythonforandroid/bootstraps/common/build/jni/application/src/start.c index 7cb8dc7e5f..afc8c3ad1d 100644 --- a/pythonforandroid/bootstraps/common/build/jni/application/src/start.c +++ b/pythonforandroid/bootstraps/common/build/jni/application/src/start.c @@ -67,7 +67,7 @@ int dir_exists(char *filename) { int file_exists(const char *filename) { FILE *file; - if (file = fopen(filename, "r")) { + if ((file = fopen(filename, "r"))) { fclose(file); return 1; } @@ -84,24 +84,66 @@ int main(int argc, char *argv[]) { int ret = 0; FILE *fd; - setenv("P4A_BOOTSTRAP", bootstrap_name, 1); // env var to identify p4a to applications - LOGP("Initializing Python for Android"); + + // Set a couple of built-in environment vars: + setenv("P4A_BOOTSTRAP", bootstrap_name, 1); // env var to identify p4a to applications env_argument = getenv("ANDROID_ARGUMENT"); setenv("ANDROID_APP_PATH", env_argument, 1); env_entrypoint = getenv("ANDROID_ENTRYPOINT"); env_logname = getenv("PYTHON_NAME"); - if (!getenv("ANDROID_UNPACK")) { /* ANDROID_UNPACK currently isn't set in services */ setenv("ANDROID_UNPACK", env_argument, 1); } - if (env_logname == NULL) { env_logname = "python"; setenv("PYTHON_NAME", "python", 1); } + // Set additional file-provided environment vars: + LOGP("Setting additional env vars from p4a_env_vars.txt"); + char env_file_path[256]; + snprintf(env_file_path, sizeof(env_file_path), + "%s/p4a_env_vars.txt", getenv("ANDROID_UNPACK")); + FILE *env_file_fd = fopen(env_file_path, "r"); + if (env_file_fd) { + char* line = NULL; + size_t len = 0; + while (getline(&line, &len, env_file_fd) != -1) { + if (strlen(line) > 0) { + char *eqsubstr = strstr(line, "="); + if (eqsubstr) { + size_t eq_pos = eqsubstr - line; + + // Extract name: + char env_name[256]; + strncpy(env_name, line, sizeof(env_name)); + env_name[eq_pos] = '\0'; + + // Extract value (with line break removed: + char env_value[256]; + strncpy(env_value, (char*)(line + eq_pos + 1), sizeof(env_value)); + if (strlen(env_value) > 0 && + env_value[strlen(env_value)-1] == '\n') { + env_value[strlen(env_value)-1] = '\0'; + if (strlen(env_value) > 0 && + env_value[strlen(env_value)-1] == '\r') { + // Also remove windows line breaks (\r\n) + env_value[strlen(env_value)-1] = '\0'; + } + } + + // Set value: + setenv(env_name, env_value, 1); + } + } + } + fclose(env_file_fd); + } else { + LOGP("Warning: no p4a_env_vars.txt found / failed to open!"); + } + LOGP("Changing directory to the one provided by ANDROID_ARGUMENT"); LOGP(env_argument); chdir(env_argument); @@ -110,7 +152,11 @@ int main(int argc, char *argv[]) { Py_NoSiteFlag=1; #endif +#if PY_MAJOR_VERSION < 3 + Py_SetProgramName("android_python"); +#else Py_SetProgramName(L"android_python"); +#endif #if PY_MAJOR_VERSION >= 3 /* our logging module for android @@ -263,6 +309,11 @@ int main(int argc, char *argv[]) { /* Get the entrypoint, search the .pyo then .py */ char *dot = strrchr(env_entrypoint, '.'); +#if PY_MAJOR_VERSION > 2 + char *ext = ".pyc"; +#else + char *ext = ".pyo"; +#endif if (dot <= 0) { LOGP("Invalid entrypoint, abort."); return -1; @@ -271,14 +322,14 @@ int main(int argc, char *argv[]) { LOGP("Entrypoint path is too long, try increasing ENTRYPOINT_MAXLEN."); return -1; } - if (!strcmp(dot, ".pyo")) { + if (!strcmp(dot, ext)) { if (!file_exists(env_entrypoint)) { /* fallback on .py */ strcpy(entrypoint, env_entrypoint); entrypoint[strlen(env_entrypoint) - 1] = '\0'; LOGP(entrypoint); if (!file_exists(entrypoint)) { - LOGP("Entrypoint not found (.pyo, fallback on .py), abort"); + LOGP("Entrypoint not found (.pyc/.pyo, fallback on .py), abort"); return -1; } } else { @@ -288,7 +339,11 @@ int main(int argc, char *argv[]) { /* if .py is passed, check the pyo version first */ strcpy(entrypoint, env_entrypoint); entrypoint[strlen(env_entrypoint) + 1] = '\0'; +#if PY_MAJOR_VERSION > 2 + entrypoint[strlen(env_entrypoint)] = 'c'; +#else entrypoint[strlen(env_entrypoint)] = 'o'; +#endif if (!file_exists(entrypoint)) { /* fallback on pure python version */ if (!file_exists(env_entrypoint)) { @@ -298,7 +353,7 @@ int main(int argc, char *argv[]) { strcpy(entrypoint, env_entrypoint); } } else { - LOGP("Entrypoint have an invalid extension (must be .py or .pyo), abort."); + LOGP("Entrypoint have an invalid extension (must be .py or .pyc/.pyo), abort."); return -1; } // LOGP("Entrypoint is:"); @@ -313,6 +368,7 @@ int main(int argc, char *argv[]) { /* run python ! */ ret = PyRun_SimpleFile(fd, entrypoint); + fclose(fd); if (PyErr_Occurred() != NULL) { ret = 1; @@ -323,12 +379,36 @@ int main(int argc, char *argv[]) { PyErr_Clear(); } - /* close everything + LOGP("Python for android ended."); + + /* Shut down: since regular shutdown causes issues sometimes + (seems to be an incomplete shutdown breaking next launch) + we'll use sys.exit(ret) to shutdown, since that one works. + + Reference discussion: + + https://github.com/kivy/kivy/pull/6107#issue-246120816 + */ + char terminatecmd[256]; + snprintf( + terminatecmd, sizeof(terminatecmd), + "import sys; sys.exit(%d)\n", ret + ); + PyRun_SimpleString(terminatecmd); + + /* This should never actually be reached, but we'll leave the clean-up + * here just to be safe. */ +#if PY_MAJOR_VERSION < 3 Py_Finalize(); - fclose(fd); + LOGP("Unexpectedly reached Py_FinalizeEx(), but was successful."); +#else + if (Py_FinalizeEx() != 0) // properly check success on Python 3 + LOGP("Unexpectedly reached Py_FinalizeEx(), and got error!"); + else + LOGP("Unexpectedly reached Py_FinalizeEx(), but was successful."); +#endif - LOGP("Python for android ended."); return ret; } @@ -378,19 +458,20 @@ JNIEXPORT void JNICALL Java_org_kivy_android_PythonService_nativeStart( #if defined(BOOTSTRAP_NAME_WEBVIEW) || defined(BOOTSTRAP_NAME_SERVICEONLY) // Webview and service_only uses some more functions: -void Java_org_kivy_android_PythonActivity_nativeSetEnv( - JNIEnv* env, jclass jcls, - jstring j_name, jstring j_value) -/* JNIEXPORT void JNICALL Java_org_libsdl_app_SDLActivity_nativeSetEnv( */ -/* JNIEnv* env, jclass jcls, */ -/* jstring j_name, jstring j_value) */ +void Java_org_kivy_android_PythonActivity_nativeSetenv( + JNIEnv* env, jclass cls, + jstring name, jstring value) +//JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetenv)( +// JNIEnv* env, jclass cls, +// jstring name, jstring value) { - jboolean iscopy; - const char *name = (*env)->GetStringUTFChars(env, j_name, &iscopy); - const char *value = (*env)->GetStringUTFChars(env, j_value, &iscopy); - setenv(name, value, 1); - (*env)->ReleaseStringUTFChars(env, j_name, name); - (*env)->ReleaseStringUTFChars(env, j_value, value); + const char *utfname = (*env)->GetStringUTFChars(env, name, NULL); + const char *utfvalue = (*env)->GetStringUTFChars(env, value, NULL); + + SDL_setenv(utfname, utfvalue, 1); + + (*env)->ReleaseStringUTFChars(env, name, utfname); + (*env)->ReleaseStringUTFChars(env, value, utfvalue); } diff --git a/pythonforandroid/bootstraps/pygame/build/buildlib/argparse.py b/pythonforandroid/bootstraps/pygame/build/buildlib/argparse.py index f8c3d305ef..347b67816b 100644 --- a/pythonforandroid/bootstraps/pygame/build/buildlib/argparse.py +++ b/pythonforandroid/bootstraps/pygame/build/buildlib/argparse.py @@ -1468,7 +1468,7 @@ def _check_conflict(self, action): def _handle_conflict_error(self, action, conflicting_actions): message = _('conflicting option string(s): %s') conflict_string = ', '.join([option_string - for option_string, action + for option_string, action # noqa F812 in conflicting_actions]) raise ArgumentError(action, message % conflict_string) @@ -2071,7 +2071,7 @@ def _parse_optional(self, arg_string): # if multiple actions match, the option string was ambiguous if len(option_tuples) > 1: options = ', '.join( - [option_string for action, option_string, explicit_arg in option_tuples]) + [option_string for action, option_string, explicit_arg in option_tuples]) # noqa F812 tup = arg_string, options self.error(_('ambiguous option: %s could match %s') % tup) diff --git a/pythonforandroid/bootstraps/pygame/build/jni/application/Android.mk b/pythonforandroid/bootstraps/pygame/build/jni/application/Android.mk index e30f708b7f..dcf8d643f9 100644 --- a/pythonforandroid/bootstraps/pygame/build/jni/application/Android.mk +++ b/pythonforandroid/bootstraps/pygame/build/jni/application/Android.mk @@ -18,7 +18,7 @@ LOCAL_CFLAGS := $(foreach D, $(APP_SUBDIRS), -I$(LOCAL_PATH)/$(D)) \ -I$(LOCAL_PATH)/../jpeg \ -I$(LOCAL_PATH)/../intl \ -I$(LOCAL_PATH)/.. \ - -I$(LOCAL_PATH)/../../../../other_builds/$(MK_PYTHON_INCLUDE_ROOT) + -I$(PYTHON_INCLUDE_ROOT) LOCAL_CFLAGS += $(APPLICATION_ADDITIONAL_CFLAGS) @@ -38,7 +38,7 @@ LOCAL_LDLIBS := -lpython2.7 -lGLESv1_CM -ldl -llog -lz # AND: Another hardcoded path that should be templated # AND: NOT TEMPALTED! We can use $ARCH -LOCAL_LDFLAGS += -L$(LOCAL_PATH)/../../../../other_builds/$(MK_PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) +LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) LIBS_WITH_LONG_SYMBOLS := $(strip $(shell \ for f in $(LOCAL_PATH)/../../libs/$ARCH/*.so ; do \ diff --git a/pythonforandroid/bootstraps/sdl2/__init__.py b/pythonforandroid/bootstraps/sdl2/__init__.py index 971d23a39b..b865348907 100644 --- a/pythonforandroid/bootstraps/sdl2/__init__.py +++ b/pythonforandroid/bootstraps/sdl2/__init__.py @@ -8,14 +8,14 @@ class SDL2GradleBootstrap(Bootstrap): name = 'sdl2' - recipe_depends = ['sdl2'] + recipe_depends = list( + set(Bootstrap.recipe_depends).union({'sdl2'}) + ) def run_distribute(self): info_main("# Creating Android project ({})".format(self.name)) arch = self.ctx.archs[0] - python_install_dir = self.ctx.get_python_install_dir() - from_crystax = self.ctx.python_recipe.from_crystax if len(self.ctx.archs) > 1: raise ValueError("SDL2/gradle support only one arch") diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java index 91b2169469..327ae18f4f 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java @@ -7,33 +7,40 @@ import java.io.FileWriter; import java.io.File; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.UnsatisfiedLinkError; +import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.ArrayList; +import java.util.Timer; +import java.util.TimerTask; -import android.view.ViewGroup; -import android.view.SurfaceView; import android.app.Activity; -import android.content.Intent; -import android.util.Log; -import android.widget.Toast; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.PowerManager; -import android.graphics.PixelFormat; -import android.view.SurfaceHolder; import android.content.Context; +import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ApplicationInfo; -import android.content.Intent; -import android.widget.ImageView; -import java.io.InputStream; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.PixelFormat; +import android.Manifest; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.PowerManager; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.Toast; +import org.libsdl.app.SDL; import org.libsdl.app.SDLActivity; import org.kivy.android.PythonUtil; @@ -51,16 +58,16 @@ public class PythonActivity extends SDLActivity { private ResourceManager resourceManager = null; private Bundle mMetaData = null; private PowerManager.WakeLock mWakeLock = null; + private static boolean appliedWindowedModeHack = false; public String getAppRoot() { String app_root = getFilesDir().getAbsolutePath() + "/app"; return app_root; } - @Override protected void onCreate(Bundle savedInstanceState) { - Log.v(TAG, "My oncreate running"); + Log.v(TAG, "PythonActivity onCreate running"); resourceManager = new ResourceManager(this); Log.v(TAG, "About to do super onCreate"); @@ -143,9 +150,10 @@ protected void onPostExecute(String result) { File path = new File(getIntent().getData().getSchemeSpecificPart()); Project p = Project.scanDirectory(path); - SDLActivity.nativeSetEnv("ANDROID_ENTRYPOINT", p.dir + "/main.py"); - SDLActivity.nativeSetEnv("ANDROID_ARGUMENT", p.dir); - SDLActivity.nativeSetEnv("ANDROID_APP_PATH", p.dir); + 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) { @@ -164,18 +172,19 @@ protected void onPostExecute(String result) { // pass } } else { - SDLActivity.nativeSetEnv("ANDROID_ENTRYPOINT", "main.pyo"); - SDLActivity.nativeSetEnv("ANDROID_ARGUMENT", app_root_dir); - SDLActivity.nativeSetEnv("ANDROID_APP_PATH", app_root_dir); + 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"); + 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..."); @@ -184,8 +193,8 @@ protected void onPostExecute(String result) { 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(); + 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."); @@ -196,6 +205,20 @@ protected void onPostExecute(String result) { } } 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.onResume(); + } } @Override @@ -341,15 +364,16 @@ protected void onActivityResult(int requestCode, int resultCode, Intent intent) } } - public static void start_service(String serviceTitle, String serviceDescription, + public static void start_service(String serviceTitle, String serviceDescription, String pythonServiceArgument) { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String filesDirectory = argument; 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/main.pyo"); + 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"); @@ -364,118 +388,234 @@ public static void stop_service() { PythonActivity.mActivity.stopService(serviceIntent); } - /** Loading screen implementation - * keepActive() is a method plugged in pollInputDevices in SDLActivity. - * Once it's called twice, the loading screen will be removed. - * The first call happen as soon as the window is created, but no image has been - * displayed first. My tests showed that we can wait one more. This might delay - * the real available of few hundred milliseconds. - * The real deal is to know if a rendering has already happen. The previous - * python-for-android and kivy was having something for that, but this new version - * is not compatible, and would require a new kivy version. - * In case of, the method PythonActivty.mActivity.removeLoadingScreen() can be called. - */ + /** Loading screen view **/ public static ImageView mImageView = null; - int mLoadingCount = 2; + /** 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 keepActive() { - if (this.mLoadingCount > 0) { - this.mLoadingCount -= 1; - if (this.mLoadingCount == 0) { - this.removeLoadingScreen(); + public void appConfirmedActive() { + if (!mAppConfirmedActive) { + Log.v(TAG, "appConfirmedActive() -> preparing loading screen removal"); + mAppConfirmedActive = true; + considerLoadingScreenRemoval(); } - } } - public void removeLoadingScreen() { - runOnUiThread(new Runnable() { - public void run() { - if (PythonActivity.mImageView != null && - PythonActivity.mImageView.getParent() != null) { - ((ViewGroup)PythonActivity.mImageView.getParent()).removeView( - PythonActivity.mImageView); - PythonActivity.mImageView = null; - } - } - }); + /** 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() { + if (PythonActivity.mImageView != null && + PythonActivity.mImageView.getParent() != null) { + ((ViewGroup)PythonActivity.mImageView.getParent()).removeView( + PythonActivity.mImageView); + PythonActivity.mImageView = null; + } + } + }); + } - protected void showLoadingScreen() { - // load the bitmap - // 1. if the image is valid and we don't have layout yet, assign this bitmap - // as main view. - // 2. if we have a layout, just set it in the layout. - // 3. If we have an mImageView already, then do nothing because it will have - // already been made the content view or added to the layout. - - if (mImageView == null) { - 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) {}; + public String getEntryPoint(String search_dir) { + /* Get the main file (.pyc|.pyo|.py) depending on if we + * have a compiled version or not. + */ + List entryPoints = new ArrayList(); + entryPoints.add("main.pyo"); // python 2 compiled files + entryPoints.add("main.pyc"); // python 3 compiled files + for (String value : entryPoints) { + File mainFile = new File(search_dir + "/" + value); + if (mainFile.exists()) { + return value; + } } - - mImageView = new ImageView(this); - mImageView.setImageBitmap(bitmap); - - /* - * 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 { - mImageView.setBackgroundColor(Color.parseColor(backgroundColor)); - } catch (IllegalArgumentException e) {} - } - mImageView.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.FILL_PARENT)); - mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); - + return "main.py"; } - if (mLayout == null) { - setContentView(mImageView); - } else if (PythonActivity.mImageView.getParent() == null){ - mLayout.addView(mImageView); - } + protected void showLoadingScreen() { + // load the bitmap + // 1. if the image is valid and we don't have layout yet, assign this bitmap + // as main view. + // 2. if we have a layout, just set it in the layout. + // 3. If we have an mImageView already, then do nothing because it will have + // already been made the content view or added to the layout. + + if (mImageView == null) { + 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); + + /* + * 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 { + mImageView.setBackgroundColor(Color.parseColor(backgroundColor)); + } catch (IllegalArgumentException e) {} + } + mImageView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); + } + try { + if (mLayout == null) { + setContentView(mImageView); + } else if (PythonActivity.mImageView.getParent() == null) { + mLayout.addView(mImageView); + } + } 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.") + } } @Override protected void onPause() { - // fooabc - if ( this.mWakeLock != null && mWakeLock.isHeld()){ - this.mWakeLock.release(); + if (this.mWakeLock != null && mWakeLock.isHeld()) { + this.mWakeLock.release(); } Log.v(TAG, "onPause()"); - super.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()"); - super.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 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", java.lang.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 a permission + **/ + public void requestNewPermission(String permission) { + if (android.os.Build.VERSION.SDK_INT < 23) + return; + + try { + java.lang.reflect.Method methodRequestPermission = + Activity.class.getMethod("requestPermissions", + java.lang.String[].class, int.class); + methodRequestPermission.invoke(this, new String[] {permission}, 1); + } catch (IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + } + } } diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDevice.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDevice.java new file mode 100644 index 0000000000..aa358d1fc3 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDevice.java @@ -0,0 +1,19 @@ +package org.libsdl.app; + +interface HIDDevice +{ + public int getId(); + public int getVendorId(); + public int getProductId(); + public String getSerialNumber(); + public int getVersion(); + public String getManufacturerName(); + public String getProductName(); + public boolean open(); + public int sendFeatureReport(byte[] report); + public int sendOutputReport(byte[] report); + public boolean getFeatureReport(byte[] report); + public void setFrozen(boolean frozen); + public void close(); + public void shutdown(); +} diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java new file mode 100644 index 0000000000..4cf114a299 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -0,0 +1,642 @@ +package org.libsdl.app; + +import android.content.Context; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothGattService; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +//import com.android.internal.util.HexDump; + +import java.lang.Runnable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.UUID; + +class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { + + private static final String TAG = "hidapi"; + private HIDDeviceManager mManager; + private BluetoothDevice mDevice; + private int mDeviceId; + private BluetoothGatt mGatt; + private boolean mIsRegistered = false; + private boolean mIsConnected = false; + private boolean mIsChromebook = false; + private boolean mIsReconnecting = false; + private boolean mFrozen = false; + private LinkedList mOperations; + GattOperation mCurrentOperation = null; + private Handler mHandler; + + private static final int TRANSPORT_AUTO = 0; + private static final int TRANSPORT_BREDR = 1; + private static final int TRANSPORT_LE = 2; + + private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; + + static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); + static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); + static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; + + static class GattOperation { + private enum Operation { + CHR_READ, + CHR_WRITE, + ENABLE_NOTIFICATION + } + + Operation mOp; + UUID mUuid; + byte[] mValue; + BluetoothGatt mGatt; + boolean mResult = true; + + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + } + + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mValue = value; + } + + public void run() { + // This is executed in main thread + BluetoothGattCharacteristic chr; + + switch (mOp) { + case CHR_READ: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Reading characteristic " + chr.getUuid()); + if (!mGatt.readCharacteristic(chr)) { + Log.e(TAG, "Unable to read characteristic " + mUuid.toString()); + mResult = false; + break; + } + mResult = true; + break; + case CHR_WRITE: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value)); + chr.setValue(mValue); + if (!mGatt.writeCharacteristic(chr)) { + Log.e(TAG, "Unable to write characteristic " + mUuid.toString()); + mResult = false; + break; + } + mResult = true; + break; + case ENABLE_NOTIFICATION: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Writing descriptor of " + chr.getUuid()); + if (chr != null) { + BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); + if (cccd != null) { + int properties = chr.getProperties(); + byte[] value; + if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) { + value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; + } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) { + value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; + } else { + Log.e(TAG, "Unable to start notifications on input characteristic"); + mResult = false; + return; + } + + mGatt.setCharacteristicNotification(chr, true); + cccd.setValue(value); + if (!mGatt.writeDescriptor(cccd)) { + Log.e(TAG, "Unable to write descriptor " + mUuid.toString()); + mResult = false; + return; + } + mResult = true; + } + } + } + } + + public boolean finish() { + return mResult; + } + + private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { + BluetoothGattService valveService = mGatt.getService(steamControllerService); + if (valveService == null) + return null; + return valveService.getCharacteristic(uuid); + } + + static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) { + return new GattOperation(gatt, Operation.CHR_READ, uuid); + } + + static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) { + return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value); + } + + static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { + return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); + } + } + + public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { + mManager = manager; + mDevice = device; + mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); + mIsRegistered = false; + mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + mOperations = new LinkedList(); + mHandler = new Handler(Looper.getMainLooper()); + + mGatt = connectGatt(); + final HIDDeviceBLESteamController finalThis = this; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + finalThis.checkConnectionForChromebookIssue(); + } + }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); + } + + public String getIdentifier() { + return String.format("SteamController.%s", mDevice.getAddress()); + } + + public BluetoothGatt getGatt() { + return mGatt; + } + + // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead + // of TRANSPORT_LE. Let's force ourselves to connect low energy. + private BluetoothGatt connectGatt(boolean managed) { + try { + Method m = mDevice.getClass().getDeclaredMethod("connectGatt", Context.class, boolean.class, BluetoothGattCallback.class, int.class); + return (BluetoothGatt) m.invoke(mDevice, mManager.getContext(), managed, this, TRANSPORT_LE); + } catch (Exception e) { + return mDevice.connectGatt(mManager.getContext(), managed, this); + } + } + + private BluetoothGatt connectGatt() { + return connectGatt(false); + } + + protected int getConnectionState() { + + Context context = mManager.getContext(); + if (context == null) { + // We are lacking any context to get our Bluetooth information. We'll just assume disconnected. + return BluetoothProfile.STATE_DISCONNECTED; + } + + BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE); + if (btManager == null) { + // This device doesn't support Bluetooth. We should never be here, because how did + // we instantiate a device to start with? + return BluetoothProfile.STATE_DISCONNECTED; + } + + return btManager.getConnectionState(mDevice, BluetoothProfile.GATT); + } + + public void reconnect() { + + if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) { + mGatt.disconnect(); + mGatt = connectGatt(); + } + + } + + protected void checkConnectionForChromebookIssue() { + if (!mIsChromebook) { + // We only do this on Chromebooks, because otherwise it's really annoying to just attempt + // over and over. + return; + } + + int connectionState = getConnectionState(); + + switch (connectionState) { + case BluetoothProfile.STATE_CONNECTED: + if (!mIsConnected) { + // We are in the Bad Chromebook Place. We can force a disconnect + // to try to recover. + Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect."); + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + } + else if (!isRegistered()) { + if (mGatt.getServices().size() > 0) { + Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover."); + probeService(this); + } + else { + Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover."); + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + } + } + else { + Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!"); + return; + } + break; + + case BluetoothProfile.STATE_DISCONNECTED: + Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover."); + + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + + case BluetoothProfile.STATE_CONNECTING: + Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer."); + break; + } + + final HIDDeviceBLESteamController finalThis = this; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + finalThis.checkConnectionForChromebookIssue(); + } + }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); + } + + private boolean isRegistered() { + return mIsRegistered; + } + + private void setRegistered() { + mIsRegistered = true; + } + + private boolean probeService(HIDDeviceBLESteamController controller) { + + if (isRegistered()) { + return true; + } + + if (!mIsConnected) { + return false; + } + + Log.v(TAG, "probeService controller=" + controller); + + for (BluetoothGattService service : mGatt.getServices()) { + if (service.getUuid().equals(steamControllerService)) { + Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); + + for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { + if (chr.getUuid().equals(inputCharacteristic)) { + Log.v(TAG, "Found input characteristic"); + // Start notifications + BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); + if (cccd != null) { + enableNotification(chr.getUuid()); + } + } + } + return true; + } + } + + if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) { + Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us."); + mIsConnected = false; + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + } + + return false; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void finishCurrentGattOperation() { + GattOperation op = null; + synchronized (mOperations) { + if (mCurrentOperation != null) { + op = mCurrentOperation; + mCurrentOperation = null; + } + } + if (op != null) { + boolean result = op.finish(); // TODO: Maybe in main thread as well? + + // Our operation failed, let's add it back to the beginning of our queue. + if (!result) { + mOperations.addFirst(op); + } + } + executeNextGattOperation(); + } + + private void executeNextGattOperation() { + synchronized (mOperations) { + if (mCurrentOperation != null) + return; + + if (mOperations.isEmpty()) + return; + + mCurrentOperation = mOperations.removeFirst(); + } + + // Run in main thread + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mOperations) { + if (mCurrentOperation == null) { + Log.e(TAG, "Current operation null in executor?"); + return; + } + + mCurrentOperation.run(); + // now wait for the GATT callback and when it comes, finish this operation + } + } + }); + } + + private void queueGattOperation(GattOperation op) { + synchronized (mOperations) { + mOperations.add(op); + } + executeNextGattOperation(); + } + + private void enableNotification(UUID chrUuid) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); + queueGattOperation(op); + } + + public void writeCharacteristic(UUID uuid, byte[] value) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); + queueGattOperation(op); + } + + public void readCharacteristic(UUID uuid) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); + queueGattOperation(op); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////// BluetoothGattCallback overridden methods + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + public void onConnectionStateChange(BluetoothGatt g, int status, int newState) { + //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState); + mIsReconnecting = false; + if (newState == 2) { + mIsConnected = true; + // Run directly, without GattOperation + if (!isRegistered()) { + mHandler.post(new Runnable() { + @Override + public void run() { + mGatt.discoverServices(); + } + }); + } + } + else if (newState == 0) { + mIsConnected = false; + } + + // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent. + } + + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + //Log.v(TAG, "onServicesDiscovered status=" + status); + if (status == 0) { + if (gatt.getServices().size() == 0) { + Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack."); + mIsReconnecting = true; + mIsConnected = false; + gatt.disconnect(); + mGatt = connectGatt(false); + } + else { + probeService(this); + } + } + } + + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid()); + + if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { + mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue()); + } + + finishCurrentGattOperation(); + } + + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid()); + + if (characteristic.getUuid().equals(reportCharacteristic)) { + // Only register controller with the native side once it has been fully configured + if (!isRegistered()) { + Log.v(TAG, "Registering Steam Controller with ID: " + getId()); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0); + setRegistered(); + } + } + + finishCurrentGattOperation(); + } + + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + // Enable this for verbose logging of controller input reports + //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); + + if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { + mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); + } + } + + public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + //Log.v(TAG, "onDescriptorRead status=" + status); + } + + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); + //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); + + if (chr.getUuid().equals(inputCharacteristic)) { + boolean hasWrittenInputDescriptor = true; + BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); + if (reportChr != null) { + Log.v(TAG, "Writing report characteristic to enter valve mode"); + reportChr.setValue(enterValveMode); + gatt.writeCharacteristic(reportChr); + } + } + + finishCurrentGattOperation(); + } + + public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { + //Log.v(TAG, "onReliableWriteCompleted status=" + status); + } + + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + //Log.v(TAG, "onReadRemoteRssi status=" + status); + } + + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + //Log.v(TAG, "onMtuChanged status=" + status); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + //////// Public API + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public int getId() { + return mDeviceId; + } + + @Override + public int getVendorId() { + // Valve Corporation + final int VALVE_USB_VID = 0x28DE; + return VALVE_USB_VID; + } + + @Override + public int getProductId() { + // We don't have an easy way to query from the Bluetooth device, but we know what it is + final int D0G_BLE2_PID = 0x1106; + return D0G_BLE2_PID; + } + + @Override + public String getSerialNumber() { + // This will be read later via feature report by Steam + return "12345"; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public String getManufacturerName() { + return "Valve Corporation"; + } + + @Override + public String getProductName() { + return "Steam Controller"; + } + + @Override + public boolean open() { + return true; + } + + @Override + public int sendFeatureReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return -1; + } + + // We need to skip the first byte, as that doesn't go over the air + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report)); + writeCharacteristic(reportCharacteristic, actual_report); + return report.length; + } + + @Override + public int sendOutputReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return -1; + } + + //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report)); + writeCharacteristic(reportCharacteristic, report); + return report.length; + } + + @Override + public boolean getFeatureReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return false; + } + + //Log.v(TAG, "getFeatureReport"); + readCharacteristic(reportCharacteristic); + return true; + } + + @Override + public void close() { + } + + @Override + public void setFrozen(boolean frozen) { + mFrozen = frozen; + } + + @Override + public void shutdown() { + close(); + + BluetoothGatt g = mGatt; + if (g != null) { + g.disconnect(); + g.close(); + mGatt = null; + } + mManager = null; + mIsRegistered = false; + mIsConnected = false; + mOperations.clear(); + } + +} + diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceManager.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceManager.java new file mode 100644 index 0000000000..db9400f6d6 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -0,0 +1,682 @@ +package org.libsdl.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.util.Log; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.hardware.usb.*; +import android.os.Handler; +import android.os.Looper; + +import java.util.HashMap; +import java.util.ArrayList; +import java.util.List; + +public class HIDDeviceManager { + private static final String TAG = "hidapi"; + private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION"; + + private static HIDDeviceManager sManager; + private static int sManagerRefCount = 0; + + public static HIDDeviceManager acquire(Context context) { + if (sManagerRefCount == 0) { + sManager = new HIDDeviceManager(context); + } + ++sManagerRefCount; + return sManager; + } + + public static void release(HIDDeviceManager manager) { + if (manager == sManager) { + --sManagerRefCount; + if (sManagerRefCount == 0) { + sManager.close(); + sManager = null; + } + } + } + + private Context mContext; + private HashMap mDevicesById = new HashMap(); + private HashMap mUSBDevices = new HashMap(); + private HashMap mBluetoothDevices = new HashMap(); + private int mNextDeviceId = 0; + private SharedPreferences mSharedPreferences = null; + private boolean mIsChromebook = false; + private UsbManager mUsbManager; + private Handler mHandler; + private BluetoothManager mBluetoothManager; + private List mLastBluetoothDevices; + + private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDeviceAttached(usbDevice); + } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDeviceDetached(usbDevice); + } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); + } + } + }; + + private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + // Bluetooth device was connected. If it was a Steam Controller, handle it + if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.d(TAG, "Bluetooth device connected: " + device); + + if (isSteamController(device)) { + connectBluetoothDevice(device); + } + } + + // Bluetooth device was disconnected, remove from controller manager (if any) + if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.d(TAG, "Bluetooth device disconnected: " + device); + + disconnectBluetoothDevice(device); + } + } + }; + + private HIDDeviceManager(final Context context) { + mContext = context; + + // Make sure we have the HIDAPI library loaded with the native functions + try { + SDL.loadLibrary("hidapi"); + } catch (Throwable e) { + Log.w(TAG, "Couldn't load hidapi: " + e.toString()); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setCancelable(false); + builder.setTitle("SDL HIDAPI Error"); + builder.setMessage("Please report the following error to the SDL maintainers: " + e.getMessage()); + builder.setNegativeButton("Quit", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + // If our context is an activity, exit rather than crashing when we can't + // call our native functions. + Activity activity = (Activity)context; + + activity.finish(); + } + catch (ClassCastException cce) { + // Context wasn't an activity, there's nothing we can do. Give up and return. + } + } + }); + builder.show(); + + return; + } + + HIDDeviceRegisterCallback(); + + mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); + mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + +// if (shouldClear) { +// SharedPreferences.Editor spedit = mSharedPreferences.edit(); +// spedit.clear(); +// spedit.commit(); +// } +// else + { + mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); + } + + initializeUSB(); + initializeBluetooth(); + } + + public Context getContext() { + return mContext; + } + + public int getDeviceIDForIdentifier(String identifier) { + SharedPreferences.Editor spedit = mSharedPreferences.edit(); + + int result = mSharedPreferences.getInt(identifier, 0); + if (result == 0) { + result = mNextDeviceId++; + spedit.putInt("next_device_id", mNextDeviceId); + } + + spedit.putInt(identifier, result); + spedit.commit(); + return result; + } + + private void initializeUSB() { + mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); + + /* + // Logging + for (UsbDevice device : mUsbManager.getDeviceList().values()) { + Log.i(TAG,"Path: " + device.getDeviceName()); + Log.i(TAG,"Manufacturer: " + device.getManufacturerName()); + Log.i(TAG,"Product: " + device.getProductName()); + Log.i(TAG,"ID: " + device.getDeviceId()); + Log.i(TAG,"Class: " + device.getDeviceClass()); + Log.i(TAG,"Protocol: " + device.getDeviceProtocol()); + Log.i(TAG,"Vendor ID " + device.getVendorId()); + Log.i(TAG,"Product ID: " + device.getProductId()); + Log.i(TAG,"Interface count: " + device.getInterfaceCount()); + Log.i(TAG,"---------------------------------------"); + + // Get interface details + for (int index = 0; index < device.getInterfaceCount(); index++) { + UsbInterface mUsbInterface = device.getInterface(index); + Log.i(TAG," ***** *****"); + Log.i(TAG," Interface index: " + index); + Log.i(TAG," Interface ID: " + mUsbInterface.getId()); + Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass()); + Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass()); + Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); + Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); + + // Get endpoint details + for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) + { + UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); + Log.i(TAG," ++++ ++++ ++++"); + Log.i(TAG," Endpoint index: " + epi); + Log.i(TAG," Attributes: " + mEndpoint.getAttributes()); + Log.i(TAG," Direction: " + mEndpoint.getDirection()); + Log.i(TAG," Number: " + mEndpoint.getEndpointNumber()); + Log.i(TAG," Interval: " + mEndpoint.getInterval()); + Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize()); + Log.i(TAG," Type: " + mEndpoint.getType()); + } + } + } + Log.i(TAG," No more devices connected."); + */ + + // Register for USB broadcasts and permission completions + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); + mContext.registerReceiver(mUsbBroadcast, filter); + + for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { + handleUsbDeviceAttached(usbDevice); + } + } + + UsbManager getUSBManager() { + return mUsbManager; + } + + private void shutdownUSB() { + try { + mContext.unregisterReceiver(mUsbBroadcast); + } catch (Exception e) { + // We may not have registered, that's okay + } + } + + private boolean isHIDDeviceUSB(UsbDevice usbDevice) { + for (int interface_number = 0; interface_number < usbDevice.getInterfaceCount(); ++interface_number) { + if (isHIDDeviceInterface(usbDevice, interface_number)) { + return true; + } + } + return false; + } + + private boolean isHIDDeviceInterface(UsbDevice usbDevice, int interface_number) { + UsbInterface usbInterface = usbDevice.getInterface(interface_number); + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { + return true; + } + if (interface_number == 0) { + if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) { + return true; + } + } + return false; + } + + private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) { + final int XB360_IFACE_SUBCLASS = 93; + final int XB360_IFACE_PROTOCOL = 1; // Wired only + final int[] SUPPORTED_VENDORS = { + 0x0079, // GPD Win 2 + 0x044f, // Thrustmaster + 0x045e, // Microsoft + 0x046d, // Logitech + 0x056e, // Elecom + 0x06a3, // Saitek + 0x0738, // Mad Catz + 0x07ff, // Mad Catz + 0x0e6f, // Unknown + 0x0f0d, // Hori + 0x11c9, // Nacon + 0x12ab, // Unknown + 0x1430, // RedOctane + 0x146b, // BigBen + 0x1532, // Razer Sabertooth + 0x15e4, // Numark + 0x162e, // Joytech + 0x1689, // Razer Onza + 0x1bad, // Harmonix + 0x24c6, // PowerA + }; + + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && + usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS && + usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL) { + int vendor_id = usbDevice.getVendorId(); + for (int supportedVid : SUPPORTED_VENDORS) { + if (vendor_id == supportedVid) { + return true; + } + } + } + return false; + } + + private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) { + final int XB1_IFACE_SUBCLASS = 71; + final int XB1_IFACE_PROTOCOL = 208; + final int[] SUPPORTED_VENDORS = { + 0x045e, // Microsoft + 0x0738, // Mad Catz + 0x0e6f, // Unknown + 0x0f0d, // Hori + 0x1532, // Razer Wildcat + 0x24c6, // PowerA + }; + + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && + usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS && + usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) { + int vendor_id = usbDevice.getVendorId(); + for (int supportedVid : SUPPORTED_VENDORS) { + if (vendor_id == supportedVid) { + return true; + } + } + } + return false; + } + + private void handleUsbDeviceAttached(UsbDevice usbDevice) { + if (isHIDDeviceUSB(usbDevice)) { + connectHIDDeviceUSB(usbDevice); + } + } + + private void handleUsbDeviceDetached(UsbDevice usbDevice) { + HIDDeviceUSB device = mUSBDevices.get(usbDevice); + if (device == null) + return; + + int id = device.getId(); + mUSBDevices.remove(usbDevice); + mDevicesById.remove(id); + device.shutdown(); + HIDDeviceDisconnected(id); + } + + private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) { + HIDDeviceUSB device = mUSBDevices.get(usbDevice); + if (device == null) + return; + + boolean opened = false; + if (permission_granted) { + opened = device.open(); + } + HIDDeviceOpenResult(device.getId(), opened); + } + + private void connectHIDDeviceUSB(UsbDevice usbDevice) { + synchronized (this) { + for (int interface_number = 0; interface_number < usbDevice.getInterfaceCount(); interface_number++) { + if (isHIDDeviceInterface(usbDevice, interface_number)) { + HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_number); + int id = device.getId(); + mUSBDevices.put(usbDevice, device); + mDevicesById.put(id, device); + HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), interface_number); + break; + } + } + } + } + + private void initializeBluetooth() { + Log.d(TAG, "Initializing Bluetooth"); + + if (mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); + return; + } + + // Find bonded bluetooth controllers and create SteamControllers for them + mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE); + if (mBluetoothManager == null) { + // This device doesn't support Bluetooth. + return; + } + + BluetoothAdapter btAdapter = mBluetoothManager.getAdapter(); + if (btAdapter == null) { + // This device has Bluetooth support in the codebase, but has no available adapters. + return; + } + + // Get our bonded devices. + for (BluetoothDevice device : btAdapter.getBondedDevices()) { + + Log.d(TAG, "Bluetooth device available: " + device); + if (isSteamController(device)) { + connectBluetoothDevice(device); + } + + } + + // NOTE: These don't work on Chromebooks, to my undying dismay. + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + mContext.registerReceiver(mBluetoothBroadcast, filter); + + if (mIsChromebook) { + mHandler = new Handler(Looper.getMainLooper()); + mLastBluetoothDevices = new ArrayList<>(); + + // final HIDDeviceManager finalThis = this; + // mHandler.postDelayed(new Runnable() { + // @Override + // public void run() { + // finalThis.chromebookConnectionHandler(); + // } + // }, 5000); + } + } + + private void shutdownBluetooth() { + try { + mContext.unregisterReceiver(mBluetoothBroadcast); + } catch (Exception e) { + // We may not have registered, that's okay + } + } + + // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly. + // This function provides a sort of dummy version of that, watching for changes in the + // connected devices and attempting to add controllers as things change. + public void chromebookConnectionHandler() { + if (!mIsChromebook) { + return; + } + + ArrayList disconnected = new ArrayList<>(); + ArrayList connected = new ArrayList<>(); + + List currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); + + for (BluetoothDevice bluetoothDevice : currentConnected) { + if (!mLastBluetoothDevices.contains(bluetoothDevice)) { + connected.add(bluetoothDevice); + } + } + for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) { + if (!currentConnected.contains(bluetoothDevice)) { + disconnected.add(bluetoothDevice); + } + } + + mLastBluetoothDevices = currentConnected; + + for (BluetoothDevice bluetoothDevice : disconnected) { + disconnectBluetoothDevice(bluetoothDevice); + } + for (BluetoothDevice bluetoothDevice : connected) { + connectBluetoothDevice(bluetoothDevice); + } + + final HIDDeviceManager finalThis = this; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + finalThis.chromebookConnectionHandler(); + } + }, 10000); + } + + public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { + Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice); + synchronized (this) { + if (mBluetoothDevices.containsKey(bluetoothDevice)) { + Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect"); + + HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); + device.reconnect(); + + return false; + } + HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice); + int id = device.getId(); + mBluetoothDevices.put(bluetoothDevice, device); + mDevicesById.put(id, device); + + // The Steam Controller will mark itself connected once initialization is complete + } + return true; + } + + public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { + synchronized (this) { + HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); + if (device == null) + return; + + int id = device.getId(); + mBluetoothDevices.remove(bluetoothDevice); + mDevicesById.remove(id); + device.shutdown(); + HIDDeviceDisconnected(id); + } + } + + public boolean isSteamController(BluetoothDevice bluetoothDevice) { + // Sanity check. If you pass in a null device, by definition it is never a Steam Controller. + if (bluetoothDevice == null) { + return false; + } + + // If the device has no local name, we really don't want to try an equality check against it. + if (bluetoothDevice.getName() == null) { + return false; + } + + return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); + } + + private void close() { + shutdownUSB(); + shutdownBluetooth(); + synchronized (this) { + for (HIDDevice device : mDevicesById.values()) { + device.shutdown(); + } + mDevicesById.clear(); + mBluetoothDevices.clear(); + HIDDeviceReleaseCallback(); + } + } + + public void setFrozen(boolean frozen) { + synchronized (this) { + for (HIDDevice device : mDevicesById.values()) { + device.setFrozen(frozen); + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private HIDDevice getDevice(int id) { + synchronized (this) { + HIDDevice result = mDevicesById.get(id); + if (result == null) { + Log.v(TAG, "No device for id: " + id); + Log.v(TAG, "Available devices: " + mDevicesById.keySet()); + } + return result; + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////// JNI interface functions + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + public boolean openDevice(int deviceID) { + // Look to see if this is a USB device and we have permission to access it + for (HIDDeviceUSB device : mUSBDevices.values()) { + if (deviceID == device.getId()) { + UsbDevice usbDevice = device.getDevice(); + if (!mUsbManager.hasPermission(usbDevice)) { + HIDDeviceOpenPending(deviceID); + try { + mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), 0)); + } catch (Exception e) { + Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); + HIDDeviceOpenResult(deviceID, false); + } + return false; + } + break; + } + } + + try { + Log.v(TAG, "openDevice deviceID=" + deviceID); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return false; + } + + return device.open(); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return false; + } + + public int sendOutputReport(int deviceID, byte[] report) { + try { + Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return -1; + } + + return device.sendOutputReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return -1; + } + + public int sendFeatureReport(int deviceID, byte[] report) { + try { + Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return -1; + } + + return device.sendFeatureReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return -1; + } + + public boolean getFeatureReport(int deviceID, byte[] report) { + try { + Log.v(TAG, "getFeatureReport deviceID=" + deviceID); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return false; + } + + return device.getFeatureReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return false; + } + + public void closeDevice(int deviceID) { + try { + Log.v(TAG, "closeDevice deviceID=" + deviceID); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return; + } + + device.close(); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + } + + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + /////////////// Native methods + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private native void HIDDeviceRegisterCallback(); + private native void HIDDeviceReleaseCallback(); + + native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number); + native void HIDDeviceOpenPending(int deviceID); + native void HIDDeviceOpenResult(int deviceID, boolean opened); + native void HIDDeviceDisconnected(int deviceID); + + native void HIDDeviceInputReport(int deviceID, byte[] report); + native void HIDDeviceFeatureReport(int deviceID, byte[] report); +} diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceUSB.java new file mode 100644 index 0000000000..c9fc58ece2 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/HIDDeviceUSB.java @@ -0,0 +1,307 @@ +package org.libsdl.app; + +import android.hardware.usb.*; +import android.os.Build; +import android.util.Log; +import java.util.Arrays; + +class HIDDeviceUSB implements HIDDevice { + + private static final String TAG = "hidapi"; + + protected HIDDeviceManager mManager; + protected UsbDevice mDevice; + protected int mInterface; + protected int mDeviceId; + protected UsbDeviceConnection mConnection; + protected UsbEndpoint mInputEndpoint; + protected UsbEndpoint mOutputEndpoint; + protected InputThread mInputThread; + protected boolean mRunning; + protected boolean mFrozen; + + public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_number) { + mManager = manager; + mDevice = usbDevice; + mInterface = interface_number; + mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); + mRunning = false; + } + + public String getIdentifier() { + return String.format("%s/%x/%x", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId()); + } + + @Override + public int getId() { + return mDeviceId; + } + + @Override + public int getVendorId() { + return mDevice.getVendorId(); + } + + @Override + public int getProductId() { + return mDevice.getProductId(); + } + + @Override + public String getSerialNumber() { + String result = null; + if (Build.VERSION.SDK_INT >= 21) { + result = mDevice.getSerialNumber(); + } + if (result == null) { + result = ""; + } + return result; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public String getManufacturerName() { + String result = null; + if (Build.VERSION.SDK_INT >= 21) { + result = mDevice.getManufacturerName(); + } + if (result == null) { + result = String.format("%x", getVendorId()); + } + return result; + } + + @Override + public String getProductName() { + String result = null; + if (Build.VERSION.SDK_INT >= 21) { + result = mDevice.getProductName(); + } + if (result == null) { + result = String.format("%x", getProductId()); + } + return result; + } + + public UsbDevice getDevice() { + return mDevice; + } + + public String getDeviceName() { + return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; + } + + @Override + public boolean open() { + mConnection = mManager.getUSBManager().openDevice(mDevice); + if (mConnection == null) { + Log.w(TAG, "Unable to open USB device " + getDeviceName()); + return false; + } + + // Force claim all interfaces + for (int i = 0; i < mDevice.getInterfaceCount(); i++) { + UsbInterface iface = mDevice.getInterface(i); + + if (!mConnection.claimInterface(iface, true)) { + Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName()); + close(); + return false; + } + } + + // Find the endpoints + UsbInterface iface = mDevice.getInterface(mInterface); + for (int j = 0; j < iface.getEndpointCount(); j++) { + UsbEndpoint endpt = iface.getEndpoint(j); + switch (endpt.getDirection()) { + case UsbConstants.USB_DIR_IN: + if (mInputEndpoint == null) { + mInputEndpoint = endpt; + } + break; + case UsbConstants.USB_DIR_OUT: + if (mOutputEndpoint == null) { + mOutputEndpoint = endpt; + } + break; + } + } + + // Make sure the required endpoints were present + if (mInputEndpoint == null || mOutputEndpoint == null) { + Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); + close(); + return false; + } + + // Start listening for input + mRunning = true; + mInputThread = new InputThread(); + mInputThread.start(); + + return true; + } + + @Override + public int sendFeatureReport(byte[] report) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + ++offset; + --length; + skipped_report_id = true; + } + + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, + 0x09/*HID set_report*/, + (3/*HID feature*/ << 8) | report_number, + 0, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); + return -1; + } + + if (skipped_report_id) { + ++length; + } + return length; + } + + @Override + public int sendOutputReport(byte[] report) { + int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); + if (r != report.length) { + Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); + } + return r; + } + + @Override + public boolean getFeatureReport(byte[] report) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + /* Offset the return buffer by 1, so that the report ID + will remain in byte 0. */ + ++offset; + --length; + skipped_report_id = true; + } + + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, + 0x01/*HID get_report*/, + (3/*HID feature*/ << 8) | report_number, + 0, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName()); + return false; + } + + if (skipped_report_id) { + ++res; + ++length; + } + + byte[] data; + if (res == length) { + data = report; + } else { + data = Arrays.copyOfRange(report, 0, res); + } + mManager.HIDDeviceFeatureReport(mDeviceId, data); + + return true; + } + + @Override + public void close() { + mRunning = false; + if (mInputThread != null) { + while (mInputThread.isAlive()) { + mInputThread.interrupt(); + try { + mInputThread.join(); + } catch (InterruptedException e) { + // Keep trying until we're done + } + } + mInputThread = null; + } + if (mConnection != null) { + for (int i = 0; i < mDevice.getInterfaceCount(); i++) { + UsbInterface iface = mDevice.getInterface(i); + mConnection.releaseInterface(iface); + } + mConnection.close(); + mConnection = null; + } + } + + @Override + public void shutdown() { + close(); + mManager = null; + } + + @Override + public void setFrozen(boolean frozen) { + mFrozen = frozen; + } + + protected class InputThread extends Thread { + @Override + public void run() { + int packetSize = mInputEndpoint.getMaxPacketSize(); + byte[] packet = new byte[packetSize]; + while (mRunning) { + int r; + try + { + r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000); + } + catch (Exception e) + { + Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e); + break; + } + if (r < 0) { + // Could be a timeout or an I/O error + } + if (r > 0) { + byte[] data; + if (r == packetSize) { + data = packet; + } else { + data = Arrays.copyOfRange(packet, 0, r); + } + + if (!mFrozen) { + mManager.HIDDeviceInputReport(mDeviceId, data); + } + } + } + } + } +} diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDL.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDL.java new file mode 100644 index 0000000000..fb7f7319a8 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDL.java @@ -0,0 +1,84 @@ +package org.libsdl.app; + +import android.content.Context; + +import java.lang.reflect.*; + +/** + SDL library initialization +*/ +public class SDL { + + // This function should be called first and sets up the native code + // so it can call into the Java classes + public static void setupJNI() { + SDLActivity.nativeSetupJNI(); + SDLAudioManager.nativeSetupJNI(); + SDLControllerManager.nativeSetupJNI(); + } + + // This function should be called each time the activity is started + public static void initialize() { + setContext(null); + + SDLActivity.initialize(); + SDLAudioManager.initialize(); + SDLControllerManager.initialize(); + } + + // This function stores the current activity (SDL or not) + public static void setContext(Context context) { + mContext = context; + } + + public static Context getContext() { + return mContext; + } + + public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { + + if (libraryName == null) { + throw new NullPointerException("No library name provided."); + } + + try { + // Let's see if we have ReLinker available in the project. This is necessary for + // some projects that have huge numbers of local libraries bundled, and thus may + // trip a bug in Android's native library loader which ReLinker works around. (If + // loadLibrary works properly, ReLinker will simply use the normal Android method + // internally.) + // + // To use ReLinker, just add it as a dependency. For more information, see + // https://github.com/KeepSafe/ReLinker for ReLinker's repository. + // + Class relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker"); + Class relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener"); + Class contextClass = mContext.getClassLoader().loadClass("android.content.Context"); + Class stringClass = mContext.getClassLoader().loadClass("java.lang.String"); + + // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if + // they've changed during updates. + Method forceMethod = relinkClass.getDeclaredMethod("force"); + Object relinkInstance = forceMethod.invoke(null); + Class relinkInstanceClass = relinkInstance.getClass(); + + // Actually load the library! + Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass); + loadMethod.invoke(relinkInstance, mContext, libraryName, null, null); + } + catch (final Throwable e) { + // Fall back + try { + System.loadLibrary(libraryName); + } + catch (final UnsatisfiedLinkError ule) { + throw ule; + } + catch (final SecurityException se) { + throw se; + } + } + } + + protected static Context mContext; +} diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLActivity.java index e1dc08468d..311b2f1df4 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLActivity.java @@ -2,40 +2,74 @@ import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import java.util.Hashtable; import java.lang.reflect.Method; +import java.lang.Math; import android.app.*; import android.content.*; +import android.content.res.Configuration; +import android.text.InputType; import android.view.*; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.widget.AbsoluteLayout; +import android.widget.RelativeLayout; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import android.os.*; +import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; import android.graphics.*; import android.graphics.drawable.Drawable; -import android.media.*; import android.hardware.*; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ApplicationInfo; /** SDL Activity */ -public class SDLActivity extends Activity { +public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { private static final String TAG = "SDL"; - // Keep track of the paused state - public static boolean mIsPaused, mIsSurfaceReady, mHasFocus; + public static boolean mIsResumedCalled, mIsSurfaceReady, mHasFocus; + + // Cursor types + private static final int SDL_SYSTEM_CURSOR_NONE = -1; + private static final int SDL_SYSTEM_CURSOR_ARROW = 0; + private static final int SDL_SYSTEM_CURSOR_IBEAM = 1; + private static final int SDL_SYSTEM_CURSOR_WAIT = 2; + private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3; + private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4; + private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5; + private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6; + private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7; + private static final int SDL_SYSTEM_CURSOR_SIZENS = 8; + private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; + private static final int SDL_SYSTEM_CURSOR_NO = 10; + private static final int SDL_SYSTEM_CURSOR_HAND = 11; + + protected static final int SDL_ORIENTATION_UNKNOWN = 0; + protected static final int SDL_ORIENTATION_LANDSCAPE = 1; + protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2; + protected static final int SDL_ORIENTATION_PORTRAIT = 3; + protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; + + protected static int mCurrentOrientation; + + // Handle the state of the native layer + public enum NativeState { + INIT, RESUMED, PAUSED + } + + public static NativeState mNextNativeState; + public static NativeState mCurrentNativeState; + public static boolean mExitCalledFromJava; /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ @@ -49,14 +83,54 @@ public class SDLActivity extends Activity { protected static SDLActivity mSingleton; protected static SDLSurface mSurface; protected static View mTextEdit; + protected static boolean mScreenKeyboardShown; protected static ViewGroup mLayout; - protected static SDLJoystickHandler mJoystickHandler; + protected static SDLClipboardHandler mClipboardHandler; + protected static Hashtable mCursors; + protected static int mLastCursorID; + protected static SDLGenericMotionListener_API12 mMotionListener; + protected static HIDDeviceManager mHIDDeviceManager; // This is what SDL runs in. It invokes SDL_main(), eventually protected static Thread mSDLThread; - // Audio - protected static AudioTrack mAudioTrack; + protected static SDLGenericMotionListener_API12 getMotionListener() { + if (mMotionListener == null) { + if (Build.VERSION.SDK_INT >= 26) { + mMotionListener = new SDLGenericMotionListener_API26(); + } else + if (Build.VERSION.SDK_INT >= 24) { + mMotionListener = new SDLGenericMotionListener_API24(); + } else { + mMotionListener = new SDLGenericMotionListener_API12(); + } + } + + return mMotionListener; + } + + /** + * This method returns the name of the shared object with the application entry point + * It can be overridden by derived classes. + */ + protected String getMainSharedObject() { + String library; + String[] libraries = SDLActivity.mSingleton.getLibraries(); + if (libraries.length > 0) { + library = "lib" + libraries[libraries.length - 1] + ".so"; + } else { + library = "libmain.so"; + } + return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; + } + + /** + * This method returns the name of the application entry point + * It can be overridden by derived classes. + */ + protected String getMainFunction() { + return "SDL_main"; + } /** * This method is called by SDL before loading the native shared libraries. @@ -80,7 +154,7 @@ protected String[] getLibraries() { // Load the .so public void loadLibraries() { for (String lib : getLibraries()) { - System.loadLibrary(lib); + SDL.loadLibrary(lib); } } @@ -101,32 +175,27 @@ public static void initialize() { mSurface = null; mTextEdit = null; mLayout = null; - mJoystickHandler = null; + mClipboardHandler = null; + mCursors = new Hashtable(); + mLastCursorID = 0; mSDLThread = null; - mAudioTrack = null; mExitCalledFromJava = false; mBrokenLibraries = false; - mIsPaused = false; + mIsResumedCalled = false; mIsSurfaceReady = false; mHasFocus = true; + mNextNativeState = NativeState.INIT; + mCurrentNativeState = NativeState.INIT; } // Setup @Override protected void onCreate(Bundle savedInstanceState) { - Log.v("SDL", "Device: " + android.os.Build.DEVICE); - Log.v("SDL", "Model: " + android.os.Build.MODEL); - Log.v("SDL", "onCreate():" + mSingleton); + Log.v(TAG, "Device: " + Build.DEVICE); + Log.v(TAG, "Model: " + Build.MODEL); + 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() { // Load shared libraries String errorMsgBrokenLib = ""; try { @@ -143,6 +212,7 @@ protected void finishLoad() { if (mBrokenLibraries) { + mSingleton = this; AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." + System.getProperty("line.separator") @@ -163,52 +233,120 @@ public void onClick(DialogInterface dialog,int id) { return; } - // Set up the surface - mSurface = new SDLSurface(getApplication()); + // Set up JNI + SDL.setupJNI(); - if(Build.VERSION.SDK_INT >= 12) { - mJoystickHandler = new SDLJoystickHandler_API12(); - } - else { - mJoystickHandler = new SDLJoystickHandler(); + // Initialize state + SDL.initialize(); + + // So we can call stuff from static callbacks + mSingleton = this; + SDL.setContext(this); + + if (Build.VERSION.SDK_INT >= 11) { + mClipboardHandler = new SDLClipboardHandler_API11(); + } else { + /* Before API 11, no clipboard notification (eg no SDL_CLIPBOARDUPDATE) */ + mClipboardHandler = new SDLClipboardHandler_Old(); } - mLayout = new AbsoluteLayout(this); + mHIDDeviceManager = HIDDeviceManager.acquire(this); + + // Set up the surface + mSurface = new SDLSurface(getApplication()); + + mLayout = new RelativeLayout(this); mLayout.addView(mSurface); + // Get our current screen orientation and pass it down. + mCurrentOrientation = SDLActivity.getCurrentOrientation(); + SDLActivity.onNativeOrientationChanged(mCurrentOrientation); + setContentView(mLayout); + + setWindowStyle(false); + + getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); + + // Get filename from "Open with" of another application + Intent intent = getIntent(); + if (intent != null && intent.getData() != null) { + String filename = intent.getData().getPath(); + if (filename != null) { + Log.v(TAG, "Got filename: " + filename); + SDLActivity.onNativeDropFile(filename); + } + } } // Events @Override protected void onPause() { - Log.v("SDL", "onPause()"); + Log.v(TAG, "onPause()"); super.onPause(); + mNextNativeState = NativeState.PAUSED; + mIsResumedCalled = false; if (SDLActivity.mBrokenLibraries) { return; } - SDLActivity.handlePause(); + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(true); + } + + SDLActivity.handleNativeState(); } @Override protected void onResume() { - Log.v("SDL", "onResume()"); + Log.v(TAG, "onResume()"); super.onResume(); + mNextNativeState = NativeState.RESUMED; + mIsResumedCalled = true; if (SDLActivity.mBrokenLibraries) { return; } - SDLActivity.handleResume(); + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(false); + } + + SDLActivity.handleNativeState(); } + public static int getCurrentOrientation() { + final Context context = SDLActivity.getContext(); + final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + + int result = SDL_ORIENTATION_UNKNOWN; + + switch (display.getRotation()) { + case Surface.ROTATION_0: + result = SDL_ORIENTATION_PORTRAIT; + break; + + case Surface.ROTATION_90: + result = SDL_ORIENTATION_LANDSCAPE; + break; + + case Surface.ROTATION_180: + result = SDL_ORIENTATION_PORTRAIT_FLIPPED; + break; + + case Surface.ROTATION_270: + result = SDL_ORIENTATION_LANDSCAPE_FLIPPED; + break; + } + + return result; + } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); - Log.v("SDL", "onWindowFocusChanged(): " + hasFocus); + Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); if (SDLActivity.mBrokenLibraries) { return; @@ -216,13 +354,18 @@ public void onWindowFocusChanged(boolean hasFocus) { SDLActivity.mHasFocus = hasFocus; if (hasFocus) { - SDLActivity.handleResume(); + mNextNativeState = NativeState.RESUMED; + SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded(); + } else { + mNextNativeState = NativeState.PAUSED; } + + SDLActivity.handleNativeState(); } @Override public void onLowMemory() { - Log.v("SDL", "onLowMemory()"); + Log.v(TAG, "onLowMemory()"); super.onLowMemory(); if (SDLActivity.mBrokenLibraries) { @@ -234,7 +377,12 @@ public void onLowMemory() { @Override protected void onDestroy() { - Log.v("SDL", "onDestroy()"); + Log.v(TAG, "onDestroy()"); + + if (mHIDDeviceManager != null) { + HIDDeviceManager.release(mHIDDeviceManager); + mHIDDeviceManager = null; + } if (SDLActivity.mBrokenLibraries) { super.onDestroy(); @@ -243,6 +391,9 @@ protected void onDestroy() { return; } + mNextNativeState = NativeState.PAUSED; + SDLActivity.handleNativeState(); + // Send a quit message to the application SDLActivity.mExitCalledFromJava = true; SDLActivity.nativeQuit(); @@ -252,19 +403,54 @@ protected void onDestroy() { try { SDLActivity.mSDLThread.join(); } catch(Exception e) { - Log.v("SDL", "Problem stopping thread: " + e); + Log.v(TAG, "Problem stopping thread: " + e); } SDLActivity.mSDLThread = null; - //Log.v("SDL", "Finished waiting for SDL thread"); + //Log.v(TAG, "Finished waiting for SDL thread"); } super.onDestroy(); + // Reset everything in case the user re opens the app SDLActivity.initialize(); + } + + @Override + public void onBackPressed() { + // Check if we want to block the back button in case of mouse right click. + // + // If we do, the normal hardware back button will no longer work and people have to use home, + // but the mouse right click will work. + // + String trapBack = SDLActivity.nativeGetHint("SDL_ANDROID_TRAP_BACK_BUTTON"); + if ((trapBack != null) && trapBack.equals("1")) { + // Exit and let the mouse handler handle this button (if appropriate) + return; + } + + // Default system back button behavior. + super.onBackPressed(); + } + + // Called by JNI from SDL. + public static void manualBackButton() { + mSingleton.pressBackButton(); + } - // Completely closes application. - System.exit(0); + // Used to get us onto the activity's main thread + public void pressBackButton() { + runOnUiThread(new Runnable() { + @Override + public void run() { + SDLActivity.this.superOnBackPressed(); + } + }); + } + + // Used to access the system back behavior. + public void superOnBackPressed() { + super.onBackPressed(); } @Override @@ -279,53 +465,77 @@ public boolean dispatchKeyEvent(KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_CAMERA || - keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */ - keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */ + keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ + keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ ) { return false; } return super.dispatchKeyEvent(event); } - /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed - * is the first to be called, mIsSurfaceReady should still be set - * to 'true' during the call to onPause (in a usual scenario). - */ - public static void handlePause() { - if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) { - SDLActivity.mIsPaused = true; - SDLActivity.nativePause(); - mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, false); + /* Transition to next state */ + public static void handleNativeState() { + + if (mNextNativeState == mCurrentNativeState) { + // Already in same state, discard. + return; } - } - /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready. - * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume - * every time we get one of those events, only if it comes after surfaceDestroyed - */ - public static void handleResume() { - if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) { - SDLActivity.mIsPaused = false; - SDLActivity.nativeResume(); - mSurface.handleResume(); + // Try a transition to init state + if (mNextNativeState == NativeState.INIT) { + + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to paused state + if (mNextNativeState == NativeState.PAUSED) { + nativePause(); + if (mSurface != null) + mSurface.handlePause(); + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to resumed state + if (mNextNativeState == NativeState.RESUMED) { + if (mIsSurfaceReady && mHasFocus && mIsResumedCalled) { + if (mSDLThread == null) { + // This is the entry point to the C app. + // Start up the C app thread and enable sensor input for the first time + // FIXME: Why aren't we enabling sensor input at start? + + mSDLThread = new Thread(new SDLMain(), "SDLThread"); + mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); + mSDLThread.start(); + } + + nativeResume(); + mSurface.handleResume(); + mCurrentNativeState = mNextNativeState; + } } } /* The native thread has finished */ public static void handleNativeExit() { SDLActivity.mSDLThread = null; - mSingleton.finish(); + if (mSingleton != null) { + mSingleton.finish(); + } } // Messages from the SDLMain thread static final int COMMAND_CHANGE_TITLE = 1; - static final int COMMAND_UNUSED = 2; + static final int COMMAND_CHANGE_WINDOW_STYLE = 2; static final int COMMAND_TEXTEDIT_HIDE = 3; static final int COMMAND_SET_KEEP_SCREEN_ON = 5; protected static final int COMMAND_USER = 0x8000; + protected static boolean mFullscreenModeActive; + /** * This method is called by SDL if SDL did not handle a message itself. * This happens if a received message contains an unsupported command. @@ -346,7 +556,7 @@ protected boolean onUnhandledMessage(int command, Object param) { protected static class SDLCommandHandler extends Handler { @Override public void handleMessage(Message msg) { - Context context = getContext(); + Context context = SDL.getContext(); if (context == null) { Log.e(TAG, "error handling message, getContext() returned null"); return; @@ -359,22 +569,60 @@ public void handleMessage(Message msg) { Log.e(TAG, "error handling message, getContext() returned no Activity"); } break; + case COMMAND_CHANGE_WINDOW_STYLE: + if (Build.VERSION.SDK_INT < 19) { + // This version of Android doesn't support the immersive fullscreen mode + break; + } + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) { + int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; + window.getDecorView().setSystemUiVisibility(flags); + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + SDLActivity.mFullscreenModeActive = true; + } else { + int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; + window.getDecorView().setSystemUiVisibility(flags); + window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + SDLActivity.mFullscreenModeActive = false; + } + } + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); + } + break; case COMMAND_TEXTEDIT_HIDE: if (mTextEdit != null) { - mTextEdit.setVisibility(View.GONE); + // Note: On some devices setting view to GONE creates a flicker in landscape. + // Setting the View's sizes to 0 is similar to GONE but without the flicker. + // The sizes will be set to useful values when the keyboard is shown again. + mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); + + mScreenKeyboardShown = false; } break; case COMMAND_SET_KEEP_SCREEN_ON: { - Window window = ((Activity) context).getWindow(); - if (window != null) { - if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } } } break; @@ -395,59 +643,216 @@ boolean sendCommand(int command, Object data) { Message msg = commandHandler.obtainMessage(); msg.arg1 = command; msg.obj = data; - return commandHandler.sendMessage(msg); + boolean result = commandHandler.sendMessage(msg); + + if ((Build.VERSION.SDK_INT >= 19) && (command == COMMAND_CHANGE_WINDOW_STYLE)) { + // Ensure we don't return until the resize has actually happened, + // or 500ms have passed. + + boolean bShouldWait = false; + + if (data instanceof Integer) { + // Let's figure out if we're already laid out fullscreen or not. + Display display = ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + android.util.DisplayMetrics realMetrics = new android.util.DisplayMetrics(); + display.getRealMetrics( realMetrics ); + + boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && + (realMetrics.heightPixels == mSurface.getHeight())); + + if (((Integer)data).intValue() == 1) { + // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going + // to change size and should wait for surfaceChanged() before we return, so the size + // is right back in native code. If we're already laid out fullscreen, though, we're + // not going to change size even if we change decor modes, so we shouldn't wait for + // surfaceChanged() -- which may not even happen -- and should return immediately. + bShouldWait = !bFullscreenLayout; + } + else { + // If we're laid out fullscreen (even if the status bar and nav bar are present), + // or are actively in fullscreen, we're going to change size and should wait for + // surfaceChanged before we return, so the size is right back in native code. + bShouldWait = bFullscreenLayout; + } + } + + if (bShouldWait) { + // We'll wait for the surfaceChanged() method, which will notify us + // when called. That way, we know our current size is really the + // size we need, instead of grabbing a size that's still got + // the navigation and/or status bars before they're hidden. + // + // We'll wait for up to half a second, because some devices + // take a surprisingly long time for the surface resize, but + // then we'll just give up and return. + // + synchronized(SDLActivity.getContext()) { + try { + SDLActivity.getContext().wait(500); + } + catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + } + + return result; } // C functions we call - public static native int nativeInit(Object arguments); + public static native int nativeSetupJNI(); + public static native int nativeRunMain(String library, String function, Object arguments); public static native void nativeLowMemory(); public static native void nativeQuit(); public static native void nativePause(); public static native void nativeResume(); - public static native void onNativeResize(int x, int y, int format, float rate); - public static native int onNativePadDown(int device_id, int keycode); - public static native int onNativePadUp(int device_id, int keycode); - public static native void onNativeJoy(int device_id, int axis, - float value); - public static native void onNativeHat(int device_id, int hat_id, - int x, int y); - public static native void nativeSetEnv(String j_name, String j_value); + public static native void onNativeDropFile(String filename); + public static native void onNativeResize(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, int format, float rate); public static native void onNativeKeyDown(int keycode); public static native void onNativeKeyUp(int keycode); public static native void onNativeKeyboardFocusLost(); - public static native void onNativeMouse(int button, int action, float x, float y); + public static native void onNativeMouse(int button, int action, float x, float y, boolean relative); public static native void onNativeTouch(int touchDevId, int pointerFingerId, int action, float x, float y, float p); public static native void onNativeAccel(float x, float y, float z); + public static native void onNativeClipboardChanged(); public static native void onNativeSurfaceChanged(); public static native void onNativeSurfaceDestroyed(); - public static native void nativeFlipBuffers(); - public static native int nativeAddJoystick(int device_id, String name, - int is_accelerometer, int nbuttons, - int naxes, int nhats, int nballs); - public static native int nativeRemoveJoystick(int device_id); public static native String nativeGetHint(String name); + public static native void nativeSetenv(String name, String value); + public static native void onNativeOrientationChanged(int orientation); /** * This method is called by SDL using JNI. */ - public static void flipBuffers() { - SDLActivity.nativeFlipBuffers(); + public static boolean setActivityTitle(String title) { + // Called from SDLMain() thread and can't directly affect the view + return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); } /** * This method is called by SDL using JNI. */ - public static boolean setActivityTitle(String title) { + public static void setWindowStyle(boolean fullscreen) { // Called from SDLMain() thread and can't directly affect the view - return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); + mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0); + } + + /** + * This method is called by SDL using JNI. + * This is a static method for JNI convenience, it calls a non-static method + * so that is can be overridden + */ + public static void setOrientation(int w, int h, boolean resizable, String hint) + { + if (mSingleton != null) { + mSingleton.setOrientationBis(w, h, resizable, hint); + } + } + + /** + * This can be overridden + */ + public void setOrientationBis(int w, int h, boolean resizable, String hint) + { + int orientation = -1; + + if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else if (hint.contains("LandscapeRight")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + } else if (hint.contains("LandscapeLeft")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } else if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } else if (hint.contains("Portrait")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } else if (hint.contains("PortraitUpsideDown")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } + + /* no valid hint */ + if (orientation == -1) { + if (resizable) { + /* no fixed orientation */ + } else { + if (w > h) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } + } + } + + Log.v("SDL", "setOrientation() orientation=" + orientation + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); + if (orientation != -1) { + mSingleton.setRequestedOrientation(orientation); + } + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isScreenKeyboardShown() + { + if (mTextEdit == null) { + return false; + } + + if (!mScreenKeyboardShown) { + return false; + } + + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.isAcceptingText(); + + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean supportsRelativeMouse() + { + // ChromeOS doesn't provide relative mouse motion via the Android 7 APIs + if (isChromebook()) { + return false; + } + + // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under + // Android 7 APIs, and simply returns no data under Android 8 APIs. + // + // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and + // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result, + // we should stick to relative mode. + // + if ((Build.VERSION.SDK_INT < 27) && isDeXMode()) { + return false; + } + + return SDLActivity.getMotionListener().supportsRelativeMouse(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean setRelativeMouseEnabled(boolean enabled) + { + if (enabled && !supportsRelativeMouse()) { + return false; + } + + return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled); } /** * This method is called by SDL using JNI. */ public static boolean sendMessage(int command, int param) { + if (mSingleton == null) { + return false; + } return mSingleton.sendCommand(command, Integer.valueOf(param)); } @@ -455,36 +860,105 @@ public static boolean sendMessage(int command, int param) { * This method is called by SDL using JNI. */ public static Context getContext() { - return mSingleton; + return SDL.getContext(); } /** * This method is called by SDL using JNI. - * @return result of getSystemService(name) but executed on UI thread. */ - public Object getSystemServiceFromUiThread(final String name) { - final Object lock = new Object(); - final Object[] results = new Object[2]; // array for writable variables - synchronized (lock) { - runOnUiThread(new Runnable() { - @Override - public void run() { - synchronized (lock) { - results[0] = getSystemService(name); - results[1] = Boolean.TRUE; - lock.notify(); - } - } - }); - if (results[1] == null) { - try { - lock.wait(); - } catch (InterruptedException ex) { - ex.printStackTrace(); + public static boolean isAndroidTV() { + UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { + return true; + } + if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { + return true; + } + if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { + return true; + } + return false; + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isTablet() { + DisplayMetrics metrics = new DisplayMetrics(); + Activity activity = (Activity)getContext(); + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + double dWidthInches = metrics.widthPixels / (double)metrics.xdpi; + double dHeightInches = metrics.heightPixels / (double)metrics.ydpi; + + double dDiagonal = Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); + + // If our diagonal size is seven inches or greater, we consider ourselves a tablet. + return (dDiagonal >= 7.0); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isChromebook() { + return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isDeXMode() { + if (Build.VERSION.SDK_INT < 24) { + return false; + } + try { + final Configuration config = getContext().getResources().getConfiguration(); + final Class configClass = config.getClass(); + return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) + == configClass.getField("semDesktopModeEnabled").getInt(config); + } catch(Exception ignored) { + return false; + } + } + + /** + * This method is called by SDL using JNI. + */ + public static DisplayMetrics getDisplayDPI() { + return getContext().getResources().getDisplayMetrics(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean getManifestEnvironmentVariables() { + try { + ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); + Bundle bundle = applicationInfo.metaData; + if (bundle == null) { + return false; + } + String prefix = "SDL_ENV."; + final int trimLength = prefix.length(); + for (String key : bundle.keySet()) { + if (key.startsWith(prefix)) { + String name = key.substring(trimLength); + String value = bundle.get(key).toString(); + nativeSetenv(name, value); } } + /* environment variables set! */ + return true; + } catch (Exception e) { + Log.v("SDL", "exception " + e.toString()); } - return results[0]; + return false; + } + + // This method is called by SDLControllerManager's API 26 Generic Motion Handler. + public static View getContentView() + { + return mSingleton.mLayout; } static class ShowTextInputTask implements Runnable { @@ -506,11 +980,12 @@ public ShowTextInputTask(int x, int y, int w, int h) { @Override public void run() { - AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams( - w, h + HEIGHT_PADDING, x, y); + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); + params.leftMargin = x; + params.topMargin = y; if (mTextEdit == null) { - mTextEdit = new DummyEdit(getContext()); + mTextEdit = new DummyEdit(SDL.getContext()); mLayout.addView(mTextEdit, params); } else { @@ -520,8 +995,10 @@ public void run() { mTextEdit.setVisibility(View.VISIBLE); mTextEdit.requestFocus(); - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mTextEdit, 0); + + mScreenKeyboardShown = true; } } @@ -533,102 +1010,26 @@ public static boolean showTextInput(int x, int y, int w, int h) { return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); } - /** - * This method is called by SDL using JNI. - */ - public static Surface getNativeSurface() { - return SDLActivity.mSurface.getNativeSurface(); - } - - // Audio - - /** - * This method is called by SDL using JNI. - */ - public static int audioInit(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) { - int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO; - int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT; - int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1); - - Log.v("SDL", "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer"); - - // Let the user pick a larger buffer if they really want -- but ye - // gods they probably shouldn't, the minimums are horrifyingly high - // latency already - desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize); - - if (mAudioTrack == null) { - mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, - channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); - - // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid - // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java - // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() - - if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { - Log.e("SDL", "Failed during initialization of Audio Track"); - mAudioTrack = null; - return -1; - } - - mAudioTrack.play(); + public static boolean isTextInputEvent(KeyEvent event) { + + // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT + if (Build.VERSION.SDK_INT >= 11) { + if (event.isCtrlPressed()) { + return false; + } } - Log.v("SDL", "SDL audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer"); - - return 0; + return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; } /** * This method is called by SDL using JNI. */ - public static void audioWriteShortBuffer(short[] buffer) { - for (int i = 0; i < buffer.length; ) { - int result = mAudioTrack.write(buffer, i, buffer.length - i); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom - } - } else { - Log.w("SDL", "SDL audio: error return from write(short)"); - return; - } - } - } - - /** - * This method is called by SDL using JNI. - */ - public static void audioWriteByteBuffer(byte[] buffer) { - for (int i = 0; i < buffer.length; ) { - int result = mAudioTrack.write(buffer, i, buffer.length - i); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom - } - } else { - Log.w("SDL", "SDL audio: error return from write(byte)"); - return; - } - } - } - - /** - * This method is called by SDL using JNI. - */ - public static void audioQuit() { - if (mAudioTrack != null) { - mAudioTrack.stop(); - mAudioTrack = null; + public static Surface getNativeSurface() { + if (SDLActivity.mSurface == null) { + return null; } + return SDLActivity.mSurface.getNativeSurface(); } // Input @@ -650,50 +1051,47 @@ public static int[] inputGetInputDeviceIds(int sources) { return Arrays.copyOf(filtered, used); } - // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance - public static boolean handleJoystickMotionEvent(MotionEvent event) { - return mJoystickHandler.handleMotionEvent(event); - } - - /** - * This method is called by SDL using JNI. - */ - public static void pollInputDevices() { - if (SDLActivity.mSDLThread != null) { - mJoystickHandler.pollInputDevices(); - SDLActivity.mSingleton.keepActive(); - } - } - - /** - * Trick needed for loading screen - */ - public void keepActive() { - } - - // APK extension files support + // APK expansion files support /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ - private Object expansionFile; + private static Object expansionFile; /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */ - private Method expansionFileMethod; + private static Method expansionFileMethod; /** * This method is called by SDL using JNI. + * @return an InputStream on success or null if no expansion file was used. + * @throws IOException on errors. Message is set for the SDL error message. */ - public InputStream openAPKExtensionInputStream(String fileName) throws IOException { + public static InputStream openAPKExpansionInputStream(String fileName) throws IOException { // Get a ZipResourceFile representing a merger of both the main and patch files if (expansionFile == null) { - Integer mainVersion = Integer.valueOf(nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION")); - Integer patchVersion = Integer.valueOf(nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION")); + String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION"); + if (mainHint == null) { + return null; // no expansion use if no main version was set + } + String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION"); + if (patchHint == null) { + return null; // no expansion use if no patch version was set + } + Integer mainVersion; + Integer patchVersion; try { - // To avoid direct dependency on Google APK extension library that is + mainVersion = Integer.valueOf(mainHint); + patchVersion = Integer.valueOf(patchHint); + } catch (NumberFormatException ex) { + ex.printStackTrace(); + throw new IOException("No valid file versions set for APK expansion files", ex); + } + + try { + // To avoid direct dependency on Google APK expansion library that is // not a part of Android SDK we access it using reflection expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport") .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class) - .invoke(null, this, mainVersion, patchVersion); + .invoke(null, SDL.getContext(), mainVersion, patchVersion); expansionFileMethod = expansionFile.getClass() .getMethod("getInputStream", String.class); @@ -701,6 +1099,7 @@ public InputStream openAPKExtensionInputStream(String fileName) throws IOExcepti ex.printStackTrace(); expansionFile = null; expansionFileMethod = null; + throw new IOException("Could not access APK expansion support library", ex); } } @@ -709,12 +1108,14 @@ public InputStream openAPKExtensionInputStream(String fileName) throws IOExcepti try { fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName); } catch (Exception ex) { + // calling "getInputStream" failed ex.printStackTrace(); - fileStream = null; + throw new IOException("Could not open stream from APK expansion file", ex); } if (fileStream == null) { - throw new IOException(); + // calling "getInputStream" was successful but null was returned + throw new IOException("Could not find path in APK expansion file"); } return fileStream; @@ -869,7 +1270,7 @@ public void onClick(View v) { mapping.put(KeyEvent.KEYCODE_ENTER, button); } if ((buttonFlags[i] & 0x00000002) != 0) { - mapping.put(111, button); /* API 11: KeyEvent.KEYCODE_ESCAPE */ + mapping.put(KeyEvent.KEYCODE_ESCAPE, button); /* API 11 */ } } button.setText(buttonTexts[i]); @@ -924,18 +1325,164 @@ public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) { return dialog; } + + private final Runnable rehideSystemUi = new Runnable() { + @Override + public void run() { + int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; + + SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags); + } + }; + + public void onSystemUiVisibilityChange(int visibility) { + if (SDLActivity.mFullscreenModeActive && (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { + + Handler handler = getWindow().getDecorView().getHandler(); + if (handler != null) { + handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop. + handler.postDelayed(rehideSystemUi, 2000); + } + + } + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean clipboardHasText() { + return mClipboardHandler.clipboardHasText(); + } + + /** + * This method is called by SDL using JNI. + */ + public static String clipboardGetText() { + return mClipboardHandler.clipboardGetText(); + } + + /** + * This method is called by SDL using JNI. + */ + public static void clipboardSetText(String string) { + mClipboardHandler.clipboardSetText(string); + } + + /** + * This method is called by SDL using JNI. + */ + public static int createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY) { + Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); + ++mLastCursorID; + // This requires API 24, so use reflection to implement this + try { + Class PointerIconClass = Class.forName("android.view.PointerIcon"); + Class[] arg_types = new Class[] { Bitmap.class, float.class, float.class }; + Method create = PointerIconClass.getMethod("create", arg_types); + mCursors.put(mLastCursorID, create.invoke(null, bitmap, hotSpotX, hotSpotY)); + } catch (Exception e) { + return 0; + } + return mLastCursorID; + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean setCustomCursor(int cursorID) { + // This requires API 24, so use reflection to implement this + try { + Class PointerIconClass = Class.forName("android.view.PointerIcon"); + Method setPointerIcon = SDLSurface.class.getMethod("setPointerIcon", PointerIconClass); + setPointerIcon.invoke(mSurface, mCursors.get(cursorID)); + } catch (Exception e) { + return false; + } + return true; + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean setSystemCursor(int cursorID) { + int cursor_type = 0; //PointerIcon.TYPE_NULL; + switch (cursorID) { + case SDL_SYSTEM_CURSOR_ARROW: + cursor_type = 1000; //PointerIcon.TYPE_ARROW; + break; + case SDL_SYSTEM_CURSOR_IBEAM: + cursor_type = 1008; //PointerIcon.TYPE_TEXT; + break; + case SDL_SYSTEM_CURSOR_WAIT: + cursor_type = 1004; //PointerIcon.TYPE_WAIT; + break; + case SDL_SYSTEM_CURSOR_CROSSHAIR: + cursor_type = 1007; //PointerIcon.TYPE_CROSSHAIR; + break; + case SDL_SYSTEM_CURSOR_WAITARROW: + cursor_type = 1004; //PointerIcon.TYPE_WAIT; + break; + case SDL_SYSTEM_CURSOR_SIZENWSE: + cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_SIZENESW: + cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_SIZEWE: + cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_SIZENS: + cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_SIZEALL: + cursor_type = 1020; //PointerIcon.TYPE_GRAB; + break; + case SDL_SYSTEM_CURSOR_NO: + cursor_type = 1012; //PointerIcon.TYPE_NO_DROP; + break; + case SDL_SYSTEM_CURSOR_HAND: + cursor_type = 1002; //PointerIcon.TYPE_HAND; + break; + } + // This requires API 24, so use reflection to implement this + try { + Class PointerIconClass = Class.forName("android.view.PointerIcon"); + Class[] arg_types = new Class[] { Context.class, int.class }; + Method getSystemIcon = PointerIconClass.getMethod("getSystemIcon", arg_types); + Method setPointerIcon = SDLSurface.class.getMethod("setPointerIcon", PointerIconClass); + setPointerIcon.invoke(mSurface, getSystemIcon.invoke(null, SDL.getContext(), cursor_type)); + } catch (Exception e) { + return false; + } + return true; + } } /** - Simple nativeInit() runnable + Simple runnable to start the SDL application */ class SDLMain implements Runnable { @Override public void run() { // Runs SDL_main() - SDLActivity.nativeInit(SDLActivity.mSingleton.getArguments()); + String library = SDLActivity.mSingleton.getMainSharedObject(); + String function = SDLActivity.mSingleton.getMainFunction(); + String[] arguments = SDLActivity.mSingleton.getArguments(); + + Log.v("SDL", "Running main function " + function + " from library " + library); + SDLActivity.nativeRunMain(library, function, arguments); + + Log.v("SDL", "Finished main function"); - //Log.v("SDL", "SDL thread terminated"); + // Native thread has finished, let's finish the Activity + if (!SDLActivity.mExitCalledFromJava) { + SDLActivity.handleNativeExit(); + } } } @@ -970,8 +1517,8 @@ public SDLSurface(Context context) { mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); - if(Build.VERSION.SDK_INT >= 12) { - setOnGenericMotionListener(new SDLGenericMotionListener_API12()); + if (Build.VERSION.SDK_INT >= 12) { + setOnGenericMotionListener(SDLActivity.getMotionListener()); } // Some arbitrary defaults to avoid a potential division by zero @@ -979,6 +1526,10 @@ public SDLSurface(Context context) { mHeight = 1.0f; } + public void handlePause() { + enableSensor(Sensor.TYPE_ACCELEROMETER, false); + } + public void handleResume() { setFocusable(true); setFocusableInTouchMode(true); @@ -1003,8 +1554,11 @@ public void surfaceCreated(SurfaceHolder holder) { @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.v("SDL", "surfaceDestroyed()"); - // Call this *before* setting mIsSurfaceReady to 'false' - SDLActivity.handlePause(); + + // Transition to pause, if needed + SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED; + SDLActivity.handleNativeState(); + SDLActivity.mIsSurfaceReady = false; SDLActivity.onNativeSurfaceDestroyed(); } @@ -1015,6 +1569,10 @@ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.v("SDL", "surfaceChanged()"); + if (SDLActivity.mSingleton == null) { + return; + } + int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default switch (format) { case PixelFormat.A_8: @@ -1062,70 +1620,101 @@ public void surfaceChanged(SurfaceHolder holder, mWidth = width; mHeight = height; - SDLActivity.onNativeResize(width, height, sdlFormat, mDisplay.getRefreshRate()); - Log.v("SDL", "Window size:" + width + "x"+height); + int nDeviceWidth = width; + int nDeviceHeight = height; + try + { + if (Build.VERSION.SDK_INT >= 17) { + android.util.DisplayMetrics realMetrics = new android.util.DisplayMetrics(); + mDisplay.getRealMetrics( realMetrics ); + nDeviceWidth = realMetrics.widthPixels; + nDeviceHeight = realMetrics.heightPixels; + } + } + catch ( java.lang.Throwable throwable ) {} - // Set mIsSurfaceReady to 'true' *before* making a call to handleResume - SDLActivity.mIsSurfaceReady = true; - SDLActivity.onNativeSurfaceChanged(); + synchronized(SDLActivity.getContext()) { + // In case we're waiting on a size change after going fullscreen, send a notification. + SDLActivity.getContext().notifyAll(); + } + Log.v("SDL", "Window size: " + width + "x" + height); + Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight); + SDLActivity.onNativeResize(width, height, nDeviceWidth, nDeviceHeight, sdlFormat, mDisplay.getRefreshRate()); - if (SDLActivity.mSDLThread == null) { - // This is the entry point to the C app. - // Start up the C app thread and enable sensor input for the first time + boolean skip = false; + int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation(); - final Thread sdlThread = new Thread(new SDLMain(), "SDLThread"); - enableSensor(Sensor.TYPE_ACCELEROMETER, true); - sdlThread.start(); + if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) + { + // Accept any + } + else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) + { + if (mWidth > mHeight) { + skip = true; + } + } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) { + if (mWidth < mHeight) { + skip = true; + } + } - // Set up a listener thread to catch when the native thread ends - SDLActivity.mSDLThread = new Thread(new Runnable(){ - @Override - public void run(){ - try { - sdlThread.join(); - } - catch(Exception e){} - finally{ - // Native thread has finished - if (! SDLActivity.mExitCalledFromJava) { - SDLActivity.handleNativeExit(); - } - } - } - }, "SDLThreadListener"); - SDLActivity.mSDLThread.start(); + // Special Patch for Square Resolution: Black Berry Passport + if (skip) { + double min = Math.min(mWidth, mHeight); + double max = Math.max(mWidth, mHeight); + + if (max / min < 1.20) { + Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution."); + skip = false; + } } - } - // unused - @Override - public void onDraw(Canvas canvas) {} + if (skip) { + Log.v("SDL", "Skip .. Surface is not ready."); + SDLActivity.mIsSurfaceReady = false; + return; + } + + /* Surface is ready */ + SDLActivity.mIsSurfaceReady = true; + /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */ + SDLActivity.onNativeSurfaceChanged(); + + SDLActivity.handleNativeState(); + } // Key events @Override public boolean onKey(View v, int keyCode, KeyEvent event) { // Dispatch the different events depending on where they come from - // Some SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD - // So, we try to process them as DPAD or GAMEPAD events first, if that fails we try them as KEYBOARD - - if ( (event.getSource() & InputDevice.SOURCE_GAMEPAD) != 0 || - (event.getSource() & InputDevice.SOURCE_DPAD) != 0 ) { + // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD + // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD + // + // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and + // SOURCE_JOYSTICK, while its key events arrive from the keyboard source + // So, retrieve the device itself and check all of its sources + if (SDLControllerManager.isDeviceSDLJoystick(event.getDeviceId())) { + // Note that we process events with specific key codes here if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLActivity.onNativePadDown(event.getDeviceId(), keyCode) == 0) { + if (SDLControllerManager.onNativePadDown(event.getDeviceId(), keyCode) == 0) { return true; } } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLActivity.onNativePadUp(event.getDeviceId(), keyCode) == 0) { + if (SDLControllerManager.onNativePadUp(event.getDeviceId(), keyCode) == 0) { return true; } } } - if( (event.getSource() & InputDevice.SOURCE_KEYBOARD) != 0) { + if ((event.getSource() & InputDevice.SOURCE_KEYBOARD) != 0) { if (event.getAction() == KeyEvent.ACTION_DOWN) { //Log.v("SDL", "key down: " + keyCode); + if (SDLActivity.isTextInputEvent(event)) { + SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); + } SDLActivity.onNativeKeyDown(keyCode); return true; } @@ -1136,6 +1725,20 @@ else if (event.getAction() == KeyEvent.ACTION_UP) { } } + if ((event.getSource() & InputDevice.SOURCE_MOUSE) != 0) { + // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses + // they are ignored here because sending them as mouse input to SDL is messy + if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + case KeyEvent.ACTION_UP: + // mark the event as handled or it will be handled by system + // handling KEYCODE_BACK by system will call onBackPressed() + return true; + } + } + } + return false; } @@ -1152,9 +1755,10 @@ public boolean onTouch(View v, MotionEvent event) { float x,y,p; // !!! FIXME: dump this SDK check after 2.0.4 ships and require API14. - if (event.getSource() == InputDevice.SOURCE_MOUSE && SDLActivity.mSeparateMouseAndTouch) { + // 12290 = Samsung DeX mode desktop mouse + if ((event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == 12290) && SDLActivity.mSeparateMouseAndTouch) { if (Build.VERSION.SDK_INT < 14) { - mouseButton = 1; // For Android==12 all mouse buttons are the left button + mouseButton = 1; // all mouse buttons are the left button } else { try { mouseButton = (Integer) event.getClass().getMethod("getButtonState").invoke(event); @@ -1162,7 +1766,14 @@ public boolean onTouch(View v, MotionEvent event) { mouseButton = 1; // oh well. } } - SDLActivity.onNativeMouse(mouseButton, action, event.getX(0), event.getY(0)); + + // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values + // if we are. We'll leverage our existing mouse motion listener + SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener(); + x = motionListener.getEventX(event); + y = motionListener.getEventY(event); + + SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode()); } else { switch(action) { case MotionEvent.ACTION_MOVE: @@ -1171,6 +1782,11 @@ public boolean onTouch(View v, MotionEvent event) { x = event.getX(i) / mWidth; y = event.getY(i) / mHeight; p = event.getPressure(i); + if (p > 1.0f) { + // may be larger than 1.0f on some devices + // see the documentation of getPressure(i) + p = 1.0f; + } SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p); } break; @@ -1190,6 +1806,11 @@ public boolean onTouch(View v, MotionEvent event) { x = event.getX(i) / mWidth; y = event.getY(i) / mHeight; p = event.getPressure(i); + if (p > 1.0f) { + // may be larger than 1.0f on some devices + // see the documentation of getPressure(i) + p = 1.0f; + } SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p); break; @@ -1199,6 +1820,11 @@ public boolean onTouch(View v, MotionEvent event) { x = event.getX(i) / mWidth; y = event.getY(i) / mHeight; p = event.getPressure(i); + if (p > 1.0f) { + // may be larger than 1.0f on some devices + // see the documentation of getPressure(i) + p = 1.0f; + } SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p); } break; @@ -1232,30 +1858,90 @@ public void onAccuracyChanged(Sensor sensor, int accuracy) { @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + + // Since we may have an orientation set, we won't receive onConfigurationChanged events. + // We thus should check here. + int newOrientation = SDLActivity.SDL_ORIENTATION_UNKNOWN; + float x, y; switch (mDisplay.getRotation()) { case Surface.ROTATION_90: x = -event.values[1]; y = event.values[0]; + newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE; break; case Surface.ROTATION_270: x = event.values[1]; y = -event.values[0]; + newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED; break; case Surface.ROTATION_180: x = -event.values[1]; y = -event.values[0]; + newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED; break; default: x = event.values[0]; y = event.values[1]; + newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT; break; } + + if (newOrientation != SDLActivity.mCurrentOrientation) { + SDLActivity.mCurrentOrientation = newOrientation; + SDLActivity.onNativeOrientationChanged(newOrientation); + } + SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH, y / SensorManager.GRAVITY_EARTH, - event.values[2] / SensorManager.GRAVITY_EARTH - 1); + event.values[2] / SensorManager.GRAVITY_EARTH); + + + } + } + + // Captured pointer events for API 26. + public boolean onCapturedPointerEvent(MotionEvent event) + { + int action = event.getActionMasked(); + + float x, y; + switch (action) { + case MotionEvent.ACTION_SCROLL: + x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); + y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); + SDLActivity.onNativeMouse(0, action, x, y, false); + return true; + + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_MOVE: + x = event.getX(0); + y = event.getY(0); + SDLActivity.onNativeMouse(0, action, x, y, true); + return true; + + case MotionEvent.ACTION_BUTTON_PRESS: + case MotionEvent.ACTION_BUTTON_RELEASE: + + // Change our action value to what SDL's code expects. + if (action == MotionEvent.ACTION_BUTTON_PRESS) { + action = MotionEvent.ACTION_DOWN; + } + else if (action == MotionEvent.ACTION_BUTTON_RELEASE) { + action = MotionEvent.ACTION_UP; + } + + x = event.getX(0); + y = event.getY(0); + int button = event.getButtonState(); + + SDLActivity.onNativeMouse(button, action, x, y, true); + return true; } + + return false; } + } /* This is a fake invisible editor view that receives the input and defines the @@ -1278,23 +1964,20 @@ public boolean onCheckIsTextEditor() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { - - // This handles the hardware keyboard input - if (event.isPrintingKey()) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { + /* + * This handles the hardware keyboard input + */ + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (SDLActivity.isTextInputEvent(event)) { ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); + return true; } - return true; - } - - if (event.getAction() == KeyEvent.ACTION_DOWN) { SDLActivity.onNativeKeyDown(keyCode); return true; } else if (event.getAction() == KeyEvent.ACTION_UP) { SDLActivity.onNativeKeyUp(keyCode); return true; } - return false; } @@ -1304,7 +1987,7 @@ public boolean onKeyPreIme (int keyCode, KeyEvent event) { // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639 // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not - // FIXME: A more effective solution would be to change our Layout from AbsoluteLayout to Relative or Linear + // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :) if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { @@ -1319,8 +2002,9 @@ public boolean onKeyPreIme (int keyCode, KeyEvent event) { public InputConnection onCreateInputConnection(EditorInfo outAttrs) { ic = new SDLInputConnection(this, true); + outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI - | 33554432 /* API 11: EditorInfo.IME_FLAG_NO_FULLSCREEN */; + | EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; return ic; } @@ -1335,30 +2019,43 @@ public SDLInputConnection(View targetView, boolean fullEditor) { @Override public boolean sendKeyEvent(KeyEvent event) { + /* + * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard) + * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses + * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys + * that still do, we empty this out. + */ /* - * This handles the keycodes from soft keyboard (and IME-translated - * input from hardkeyboard) + * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key + * as we do with physical keyboards, let's just use it to hide the keyboard. */ - int keyCode = event.getKeyCode(); - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (event.isPrintingKey()) { - commitText(String.valueOf((char) event.getUnicodeChar()), 1); - } - SDLActivity.onNativeKeyDown(keyCode); - return true; - } else if (event.getAction() == KeyEvent.ACTION_UP) { - SDLActivity.onNativeKeyUp(keyCode); - return true; + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + String imeHide = SDLActivity.nativeGetHint("SDL_RETURN_KEY_HIDES_IME"); + if ((imeHide != null) && imeHide.equals("1")) { + Context c = SDL.getContext(); + if (c instanceof SDLActivity) { + SDLActivity activity = (SDLActivity)c; + activity.sendCommand(SDLActivity.COMMAND_TEXTEDIT_HIDE, null); + return true; + } + } } + + return super.sendKeyEvent(event); } @Override public boolean commitText(CharSequence text, int newCursorPosition) { - nativeCommitText(text.toString(), newCursorPosition); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + nativeGenerateScancodeForUnichar(c); + } + + SDLInputConnection.nativeCommitText(text.toString(), newCursorPosition); return super.commitText(text, newCursorPosition); } @@ -1371,209 +2068,107 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { return super.setComposingText(text, newCursorPosition); } - public native void nativeCommitText(String text, int newCursorPosition); + public static native void nativeCommitText(String text, int newCursorPosition); + + public native void nativeGenerateScancodeForUnichar(char c); public native void nativeSetComposingText(String text, int newCursorPosition); @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection - if (beforeLength == 1 && afterLength == 0) { - // backspace - return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) - && super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); + // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 + if (beforeLength > 0 && afterLength == 0) { + boolean ret = true; + // backspace(s) + while (beforeLength-- > 0) { + boolean ret_key = sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) + && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); + ret = ret && ret_key; + } + return ret; } return super.deleteSurroundingText(beforeLength, afterLength); } } -/* A null joystick handler for API level < 12 devices (the accelerometer is handled separately) */ -class SDLJoystickHandler { +interface SDLClipboardHandler { - /** - * Handles given MotionEvent. - * @param event the event to be handled. - * @return if given event was processed. - */ - public boolean handleMotionEvent(MotionEvent event) { - return false; - } + public boolean clipboardHasText(); + public String clipboardGetText(); + public void clipboardSetText(String string); - /** - * Handles adding and removing of input devices. - */ - public void pollInputDevices() { - } } -/* Actual joystick functionality available for API >= 12 devices */ -class SDLJoystickHandler_API12 extends SDLJoystickHandler { - static class SDLJoystick { - public int device_id; - public String name; - public ArrayList axes; - public ArrayList hats; - } - static class RangeComparator implements Comparator { - @Override - public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { - return arg0.getAxis() - arg1.getAxis(); - } - } +class SDLClipboardHandler_API11 implements + SDLClipboardHandler, + android.content.ClipboardManager.OnPrimaryClipChangedListener { - private ArrayList mJoysticks; + protected android.content.ClipboardManager mClipMgr; - public SDLJoystickHandler_API12() { - - mJoysticks = new ArrayList(); + SDLClipboardHandler_API11() { + mClipMgr = (android.content.ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + mClipMgr.addPrimaryClipChangedListener(this); } @Override - public void pollInputDevices() { - int[] deviceIds = InputDevice.getDeviceIds(); - // It helps processing the device ids in reverse order - // For example, in the case of the XBox 360 wireless dongle, - // so the first controller seen by SDL matches what the receiver - // considers to be the first controller - - for(int i=deviceIds.length-1; i>-1; i--) { - SDLJoystick joystick = getJoystick(deviceIds[i]); - if (joystick == null) { - joystick = new SDLJoystick(); - InputDevice joystickDevice = InputDevice.getDevice(deviceIds[i]); - if( (joystickDevice.getSources() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { - joystick.device_id = deviceIds[i]; - joystick.name = joystickDevice.getName(); - joystick.axes = new ArrayList(); - joystick.hats = new ArrayList(); - - List ranges = joystickDevice.getMotionRanges(); - Collections.sort(ranges, new RangeComparator()); - for (InputDevice.MotionRange range : ranges ) { - if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ) { - if (range.getAxis() == MotionEvent.AXIS_HAT_X || - range.getAxis() == MotionEvent.AXIS_HAT_Y) { - joystick.hats.add(range); - } - else { - joystick.axes.add(range); - } - } - } - - mJoysticks.add(joystick); - SDLActivity.nativeAddJoystick(joystick.device_id, joystick.name, 0, -1, - joystick.axes.size(), joystick.hats.size()/2, 0); - } - } - } - - /* Check removed devices */ - ArrayList removedDevices = new ArrayList(); - for(int i=0; i < mJoysticks.size(); i++) { - int device_id = mJoysticks.get(i).device_id; - int j; - for (j=0; j < deviceIds.length; j++) { - if (device_id == deviceIds[j]) break; - } - if (j == deviceIds.length) { - removedDevices.add(Integer.valueOf(device_id)); - } - } - - for(int i=0; i < removedDevices.size(); i++) { - int device_id = removedDevices.get(i).intValue(); - SDLActivity.nativeRemoveJoystick(device_id); - for (int j=0; j < mJoysticks.size(); j++) { - if (mJoysticks.get(j).device_id == device_id) { - mJoysticks.remove(j); - break; - } - } - } + public boolean clipboardHasText() { + return mClipMgr.hasText(); } - protected SDLJoystick getJoystick(int device_id) { - for(int i=0; i < mJoysticks.size(); i++) { - if (mJoysticks.get(i).device_id == device_id) { - return mJoysticks.get(i); - } + @Override + public String clipboardGetText() { + CharSequence text; + text = mClipMgr.getText(); + if (text != null) { + return text.toString(); } return null; } @Override - public boolean handleMotionEvent(MotionEvent event) { - if ( (event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) { - int actionPointerIndex = event.getActionIndex(); - int action = event.getActionMasked(); - switch(action) { - case MotionEvent.ACTION_MOVE: - SDLJoystick joystick = getJoystick(event.getDeviceId()); - if ( joystick != null ) { - for (int i = 0; i < joystick.axes.size(); i++) { - InputDevice.MotionRange range = joystick.axes.get(i); - /* Normalize the value to -1...1 */ - float value = ( event.getAxisValue( range.getAxis(), actionPointerIndex) - range.getMin() ) / range.getRange() * 2.0f - 1.0f; - SDLActivity.onNativeJoy(joystick.device_id, i, value ); - } - for (int i = 0; i < joystick.hats.size(); i+=2) { - int hatX = Math.round(event.getAxisValue( joystick.hats.get(i).getAxis(), actionPointerIndex ) ); - int hatY = Math.round(event.getAxisValue( joystick.hats.get(i+1).getAxis(), actionPointerIndex ) ); - SDLActivity.onNativeHat(joystick.device_id, i/2, hatX, hatY ); - } - } - break; - default: - break; - } - } - return true; + public void clipboardSetText(String string) { + mClipMgr.removePrimaryClipChangedListener(this); + mClipMgr.setText(string); + mClipMgr.addPrimaryClipChangedListener(this); } -} - -class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener { - // Generic Motion (mouse hover, joystick...) events go here + @Override - public boolean onGenericMotion(View v, MotionEvent event) { - float x, y; - int mouseButton; - int action; - - switch ( event.getSource() ) { - case InputDevice.SOURCE_JOYSTICK: - case InputDevice.SOURCE_GAMEPAD: - case InputDevice.SOURCE_DPAD: - SDLActivity.handleJoystickMotionEvent(event); - return true; - - case InputDevice.SOURCE_MOUSE: - action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_SCROLL: - x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); - y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); - SDLActivity.onNativeMouse(0, action, x, y); - return true; + public void onPrimaryClipChanged() { + SDLActivity.onNativeClipboardChanged(); + } - case MotionEvent.ACTION_HOVER_MOVE: - x = event.getX(0); - y = event.getY(0); +} - SDLActivity.onNativeMouse(0, action, x, y); - return true; +class SDLClipboardHandler_Old implements + SDLClipboardHandler { + + protected android.text.ClipboardManager mClipMgrOld; + + SDLClipboardHandler_Old() { + mClipMgrOld = (android.text.ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + } - default: - break; - } + @Override + public boolean clipboardHasText() { + return mClipMgrOld.hasText(); + } - default: - break; - } + @Override + public String clipboardGetText() { + CharSequence text; + text = mClipMgrOld.getText(); + if (text != null) { + return text.toString(); + } + return null; + } - // Event was not managed - return false; + @Override + public void clipboardSetText(String string) { + mClipMgrOld.setText(string); } } + diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLAudioManager.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLAudioManager.java new file mode 100644 index 0000000000..bed0eb5c3c --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLAudioManager.java @@ -0,0 +1,368 @@ +package org.libsdl.app; + +import android.media.*; +import android.os.Build; +import android.util.Log; + +public class SDLAudioManager +{ + protected static final String TAG = "SDLAudio"; + + protected static AudioTrack mAudioTrack; + protected static AudioRecord mAudioRecord; + + public static void initialize() { + mAudioTrack = null; + mAudioRecord = null; + } + + // Audio + + protected static String getAudioFormatString(int audioFormat) { + switch (audioFormat) { + case AudioFormat.ENCODING_PCM_8BIT: + return "8-bit"; + case AudioFormat.ENCODING_PCM_16BIT: + return "16-bit"; + case AudioFormat.ENCODING_PCM_FLOAT: + return "float"; + default: + return Integer.toString(audioFormat); + } + } + + protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { + int channelConfig; + int sampleSize; + int frameSize; + + Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz"); + + /* On older devices let's use known good settings */ + if (Build.VERSION.SDK_INT < 21) { + if (desiredChannels > 2) { + desiredChannels = 2; + } + if (sampleRate < 8000) { + sampleRate = 8000; + } else if (sampleRate > 48000) { + sampleRate = 48000; + } + } + + if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) { + int minSDKVersion = (isCapture ? 23 : 21); + if (Build.VERSION.SDK_INT < minSDKVersion) { + audioFormat = AudioFormat.ENCODING_PCM_16BIT; + } + } + switch (audioFormat) + { + case AudioFormat.ENCODING_PCM_8BIT: + sampleSize = 1; + break; + case AudioFormat.ENCODING_PCM_16BIT: + sampleSize = 2; + break; + case AudioFormat.ENCODING_PCM_FLOAT: + sampleSize = 4; + break; + default: + Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT"); + audioFormat = AudioFormat.ENCODING_PCM_16BIT; + sampleSize = 2; + break; + } + + if (isCapture) { + switch (desiredChannels) { + case 1: + channelConfig = AudioFormat.CHANNEL_IN_MONO; + break; + case 2: + channelConfig = AudioFormat.CHANNEL_IN_STEREO; + break; + default: + Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); + desiredChannels = 2; + channelConfig = AudioFormat.CHANNEL_IN_STEREO; + break; + } + } else { + switch (desiredChannels) { + case 1: + channelConfig = AudioFormat.CHANNEL_OUT_MONO; + break; + case 2: + channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + break; + case 3: + channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; + break; + case 4: + channelConfig = AudioFormat.CHANNEL_OUT_QUAD; + break; + case 5: + channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; + break; + case 6: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + break; + case 7: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; + break; + case 8: + if (Build.VERSION.SDK_INT >= 23) { + channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; + } else { + Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround"); + desiredChannels = 6; + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + } + break; + default: + Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); + desiredChannels = 2; + channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + break; + } + +/* + Log.v(TAG, "Speaker configuration (and order of channels):"); + + if ((channelConfig & 0x00000004) != 0) { + Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT"); + } + if ((channelConfig & 0x00000008) != 0) { + Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT"); + } + if ((channelConfig & 0x00000010) != 0) { + Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER"); + } + if ((channelConfig & 0x00000020) != 0) { + Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY"); + } + if ((channelConfig & 0x00000040) != 0) { + Log.v(TAG, " CHANNEL_OUT_BACK_LEFT"); + } + if ((channelConfig & 0x00000080) != 0) { + Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT"); + } + if ((channelConfig & 0x00000100) != 0) { + Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER"); + } + if ((channelConfig & 0x00000200) != 0) { + Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER"); + } + if ((channelConfig & 0x00000400) != 0) { + Log.v(TAG, " CHANNEL_OUT_BACK_CENTER"); + } + if ((channelConfig & 0x00000800) != 0) { + Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT"); + } + if ((channelConfig & 0x00001000) != 0) { + Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT"); + } +*/ + } + frameSize = (sampleSize * desiredChannels); + + // Let the user pick a larger buffer if they really want -- but ye + // gods they probably shouldn't, the minimums are horrifyingly high + // latency already + int minBufferSize; + if (isCapture) { + minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); + } else { + minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); + } + desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize); + + int[] results = new int[4]; + + if (isCapture) { + if (mAudioRecord == null) { + mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate, + channelConfig, audioFormat, desiredFrames * frameSize); + + // see notes about AudioTrack state in audioOpen(), above. Probably also applies here. + if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) { + Log.e(TAG, "Failed during initialization of AudioRecord"); + mAudioRecord.release(); + mAudioRecord = null; + return null; + } + + mAudioRecord.startRecording(); + } + + results[0] = mAudioRecord.getSampleRate(); + results[1] = mAudioRecord.getAudioFormat(); + results[2] = mAudioRecord.getChannelCount(); + results[3] = desiredFrames; + + } else { + if (mAudioTrack == null) { + mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); + + // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid + // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java + // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() + if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { + /* Try again, with safer values */ + + Log.e(TAG, "Failed during initialization of Audio Track"); + mAudioTrack.release(); + mAudioTrack = null; + return null; + } + + mAudioTrack.play(); + } + + results[0] = mAudioTrack.getSampleRate(); + results[1] = mAudioTrack.getAudioFormat(); + results[2] = mAudioTrack.getChannelCount(); + results[3] = desiredFrames; + } + + Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz"); + + return results; + } + + /** + * This method is called by SDL using JNI. + */ + public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { + return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames); + } + + /** + * This method is called by SDL using JNI. + */ + public static void audioWriteFloatBuffer(float[] buffer) { + if (mAudioTrack == null) { + Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); + return; + } + + for (int i = 0; i < buffer.length;) { + int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING); + if (result > 0) { + i += result; + } else if (result == 0) { + try { + Thread.sleep(1); + } catch(InterruptedException e) { + // Nom nom + } + } else { + Log.w(TAG, "SDL audio: error return from write(float)"); + return; + } + } + } + + /** + * This method is called by SDL using JNI. + */ + public static void audioWriteShortBuffer(short[] buffer) { + if (mAudioTrack == null) { + Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); + return; + } + + for (int i = 0; i < buffer.length;) { + int result = mAudioTrack.write(buffer, i, buffer.length - i); + if (result > 0) { + i += result; + } else if (result == 0) { + try { + Thread.sleep(1); + } catch(InterruptedException e) { + // Nom nom + } + } else { + Log.w(TAG, "SDL audio: error return from write(short)"); + return; + } + } + } + + /** + * This method is called by SDL using JNI. + */ + public static void audioWriteByteBuffer(byte[] buffer) { + if (mAudioTrack == null) { + Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); + return; + } + + for (int i = 0; i < buffer.length; ) { + int result = mAudioTrack.write(buffer, i, buffer.length - i); + if (result > 0) { + i += result; + } else if (result == 0) { + try { + Thread.sleep(1); + } catch(InterruptedException e) { + // Nom nom + } + } else { + Log.w(TAG, "SDL audio: error return from write(byte)"); + return; + } + } + } + + /** + * This method is called by SDL using JNI. + */ + public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { + return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames); + } + + /** This method is called by SDL using JNI. */ + public static int captureReadFloatBuffer(float[] buffer, boolean blocking) { + return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); + } + + /** This method is called by SDL using JNI. */ + public static int captureReadShortBuffer(short[] buffer, boolean blocking) { + if (Build.VERSION.SDK_INT < 23) { + return mAudioRecord.read(buffer, 0, buffer.length); + } else { + return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); + } + } + + /** This method is called by SDL using JNI. */ + public static int captureReadByteBuffer(byte[] buffer, boolean blocking) { + if (Build.VERSION.SDK_INT < 23) { + return mAudioRecord.read(buffer, 0, buffer.length); + } else { + return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); + } + } + + /** This method is called by SDL using JNI. */ + public static void audioClose() { + if (mAudioTrack != null) { + mAudioTrack.stop(); + mAudioTrack.release(); + mAudioTrack = null; + } + } + + /** This method is called by SDL using JNI. */ + public static void captureClose() { + if (mAudioRecord != null) { + mAudioRecord.stop(); + mAudioRecord.release(); + mAudioRecord = null; + } + } + + public static native int nativeSetupJNI(); +} diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLControllerManager.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLControllerManager.java new file mode 100644 index 0000000000..e60023fa96 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/libsdl/app/SDLControllerManager.java @@ -0,0 +1,846 @@ +package org.libsdl.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.os.*; +import android.view.*; +import android.util.Log; + + +public class SDLControllerManager +{ + + public static native int nativeSetupJNI(); + + public static native int nativeAddJoystick(int device_id, String name, String desc, + int vendor_id, int product_id, + boolean is_accelerometer, int button_mask, + int naxes, int nhats, int nballs); + public static native int nativeRemoveJoystick(int device_id); + public static native int nativeAddHaptic(int device_id, String name); + public static native int nativeRemoveHaptic(int device_id); + public static native int onNativePadDown(int device_id, int keycode); + public static native int onNativePadUp(int device_id, int keycode); + public static native void onNativeJoy(int device_id, int axis, + float value); + public static native void onNativeHat(int device_id, int hat_id, + int x, int y); + + protected static SDLJoystickHandler mJoystickHandler; + protected static SDLHapticHandler mHapticHandler; + + private static final String TAG = "SDLControllerManager"; + + public static void initialize() { + if (mJoystickHandler == null) { + if (Build.VERSION.SDK_INT >= 19) { + mJoystickHandler = new SDLJoystickHandler_API19(); + } else if (Build.VERSION.SDK_INT >= 16) { + mJoystickHandler = new SDLJoystickHandler_API16(); + } else if (Build.VERSION.SDK_INT >= 12) { + mJoystickHandler = new SDLJoystickHandler_API12(); + } else { + mJoystickHandler = new SDLJoystickHandler(); + } + } + + if (mHapticHandler == null) { + if (Build.VERSION.SDK_INT >= 26) { + mHapticHandler = new SDLHapticHandler_API26(); + } else { + mHapticHandler = new SDLHapticHandler(); + } + } + } + + // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance + public static boolean handleJoystickMotionEvent(MotionEvent event) { + return mJoystickHandler.handleMotionEvent(event); + } + + /** + * This method is called by SDL using JNI. + */ + public static void pollInputDevices() { + mJoystickHandler.pollInputDevices(); + } + + /** + * This method is called by SDL using JNI. + */ + public static void pollHapticDevices() { + mHapticHandler.pollHapticDevices(); + } + + /** + * This method is called by SDL using JNI. + */ + public static void hapticRun(int device_id, float intensity, int length) { + mHapticHandler.run(device_id, intensity, length); + } + + /** + * This method is called by SDL using JNI. + */ + public static void hapticStop(int device_id) + { + mHapticHandler.stop(device_id); + } + + // Check if a given device is considered a possible SDL joystick + public static boolean isDeviceSDLJoystick(int deviceId) { + InputDevice device = InputDevice.getDevice(deviceId); + // We cannot use InputDevice.isVirtual before API 16, so let's accept + // only nonnegative device ids (VIRTUAL_KEYBOARD equals -1) + if ((device == null) || (deviceId < 0)) { + return false; + } + int sources = device.getSources(); + + /* This is called for every button press, so let's not spam the logs */ + /** + if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) { + Log.v(TAG, "Input device " + device.getName() + " is a joystick."); + } + if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) { + Log.v(TAG, "Input device " + device.getName() + " is a dpad."); + } + if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { + Log.v(TAG, "Input device " + device.getName() + " is a gamepad."); + } + **/ + + return (((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) || + ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) || + ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) + ); + } + +} + +/* A null joystick handler for API level < 12 devices (the accelerometer is handled separately) */ +class SDLJoystickHandler { + + /** + * Handles given MotionEvent. + * @param event the event to be handled. + * @return if given event was processed. + */ + public boolean handleMotionEvent(MotionEvent event) { + return false; + } + + /** + * Handles adding and removing of input devices. + */ + public void pollInputDevices() { + } +} + +/* Actual joystick functionality available for API >= 12 devices */ +class SDLJoystickHandler_API12 extends SDLJoystickHandler { + + static class SDLJoystick { + public int device_id; + public String name; + public String desc; + public ArrayList axes; + public ArrayList hats; + } + static class RangeComparator implements Comparator { + @Override + public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { + // Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL + int arg0Axis = arg0.getAxis(); + int arg1Axis = arg1.getAxis(); + if (arg0Axis == MotionEvent.AXIS_GAS) { + arg0Axis = MotionEvent.AXIS_BRAKE; + } else if (arg0Axis == MotionEvent.AXIS_BRAKE) { + arg0Axis = MotionEvent.AXIS_GAS; + } + if (arg1Axis == MotionEvent.AXIS_GAS) { + arg1Axis = MotionEvent.AXIS_BRAKE; + } else if (arg1Axis == MotionEvent.AXIS_BRAKE) { + arg1Axis = MotionEvent.AXIS_GAS; + } + + return arg0Axis - arg1Axis; + } + } + + private ArrayList mJoysticks; + + public SDLJoystickHandler_API12() { + + mJoysticks = new ArrayList(); + } + + @Override + public void pollInputDevices() { + int[] deviceIds = InputDevice.getDeviceIds(); + for(int i=0; i < deviceIds.length; ++i) { + SDLJoystick joystick = getJoystick(deviceIds[i]); + if (joystick == null) { + joystick = new SDLJoystick(); + InputDevice joystickDevice = InputDevice.getDevice(deviceIds[i]); + if (SDLControllerManager.isDeviceSDLJoystick(deviceIds[i])) { + joystick.device_id = deviceIds[i]; + joystick.name = joystickDevice.getName(); + joystick.desc = getJoystickDescriptor(joystickDevice); + joystick.axes = new ArrayList(); + joystick.hats = new ArrayList(); + + List ranges = joystickDevice.getMotionRanges(); + Collections.sort(ranges, new RangeComparator()); + for (InputDevice.MotionRange range : ranges ) { + if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { + if (range.getAxis() == MotionEvent.AXIS_HAT_X || + range.getAxis() == MotionEvent.AXIS_HAT_Y) { + joystick.hats.add(range); + } + else { + joystick.axes.add(range); + } + } + } + + mJoysticks.add(joystick); + SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, getVendorId(joystickDevice), getProductId(joystickDevice), false, getButtonMask(joystickDevice), joystick.axes.size(), joystick.hats.size()/2, 0); + } + } + } + + /* Check removed devices */ + ArrayList removedDevices = new ArrayList(); + for(int i=0; i < mJoysticks.size(); i++) { + int device_id = mJoysticks.get(i).device_id; + int j; + for (j=0; j < deviceIds.length; j++) { + if (device_id == deviceIds[j]) break; + } + if (j == deviceIds.length) { + removedDevices.add(Integer.valueOf(device_id)); + } + } + + for(int i=0; i < removedDevices.size(); i++) { + int device_id = removedDevices.get(i).intValue(); + SDLControllerManager.nativeRemoveJoystick(device_id); + for (int j=0; j < mJoysticks.size(); j++) { + if (mJoysticks.get(j).device_id == device_id) { + mJoysticks.remove(j); + break; + } + } + } + } + + protected SDLJoystick getJoystick(int device_id) { + for(int i=0; i < mJoysticks.size(); i++) { + if (mJoysticks.get(i).device_id == device_id) { + return mJoysticks.get(i); + } + } + return null; + } + + @Override + public boolean handleMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) { + int actionPointerIndex = event.getActionIndex(); + int action = event.getActionMasked(); + switch(action) { + case MotionEvent.ACTION_MOVE: + SDLJoystick joystick = getJoystick(event.getDeviceId()); + if ( joystick != null ) { + for (int i = 0; i < joystick.axes.size(); i++) { + InputDevice.MotionRange range = joystick.axes.get(i); + /* Normalize the value to -1...1 */ + float value = ( event.getAxisValue( range.getAxis(), actionPointerIndex) - range.getMin() ) / range.getRange() * 2.0f - 1.0f; + SDLControllerManager.onNativeJoy(joystick.device_id, i, value ); + } + for (int i = 0; i < joystick.hats.size(); i+=2) { + int hatX = Math.round(event.getAxisValue( joystick.hats.get(i).getAxis(), actionPointerIndex ) ); + int hatY = Math.round(event.getAxisValue( joystick.hats.get(i+1).getAxis(), actionPointerIndex ) ); + SDLControllerManager.onNativeHat(joystick.device_id, i/2, hatX, hatY ); + } + } + break; + default: + break; + } + } + return true; + } + + public String getJoystickDescriptor(InputDevice joystickDevice) { + return joystickDevice.getName(); + } + public int getProductId(InputDevice joystickDevice) { + return 0; + } + public int getVendorId(InputDevice joystickDevice) { + return 0; + } + public int getButtonMask(InputDevice joystickDevice) { + return -1; + } +} + +class SDLJoystickHandler_API16 extends SDLJoystickHandler_API12 { + + @Override + public String getJoystickDescriptor(InputDevice joystickDevice) { + String desc = joystickDevice.getDescriptor(); + + if (desc != null && !desc.isEmpty()) { + return desc; + } + + return super.getJoystickDescriptor(joystickDevice); + } +} + +class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 { + + @Override + public int getProductId(InputDevice joystickDevice) { + return joystickDevice.getProductId(); + } + + @Override + public int getVendorId(InputDevice joystickDevice) { + return joystickDevice.getVendorId(); + } + + @Override + public int getButtonMask(InputDevice joystickDevice) { + int button_mask = 0; + int[] keys = new int[] { + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BACK, + KeyEvent.KEYCODE_BUTTON_MODE, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_THUMBL, + KeyEvent.KEYCODE_BUTTON_THUMBR, + KeyEvent.KEYCODE_BUTTON_L1, + KeyEvent.KEYCODE_BUTTON_R1, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_DPAD_CENTER, + + // These don't map into any SDL controller buttons directly + KeyEvent.KEYCODE_BUTTON_L2, + KeyEvent.KEYCODE_BUTTON_R2, + KeyEvent.KEYCODE_BUTTON_C, + KeyEvent.KEYCODE_BUTTON_Z, + KeyEvent.KEYCODE_BUTTON_1, + KeyEvent.KEYCODE_BUTTON_2, + KeyEvent.KEYCODE_BUTTON_3, + KeyEvent.KEYCODE_BUTTON_4, + KeyEvent.KEYCODE_BUTTON_5, + KeyEvent.KEYCODE_BUTTON_6, + KeyEvent.KEYCODE_BUTTON_7, + KeyEvent.KEYCODE_BUTTON_8, + KeyEvent.KEYCODE_BUTTON_9, + KeyEvent.KEYCODE_BUTTON_10, + KeyEvent.KEYCODE_BUTTON_11, + KeyEvent.KEYCODE_BUTTON_12, + KeyEvent.KEYCODE_BUTTON_13, + KeyEvent.KEYCODE_BUTTON_14, + KeyEvent.KEYCODE_BUTTON_15, + KeyEvent.KEYCODE_BUTTON_16, + }; + int[] masks = new int[] { + (1 << 0), // A -> A + (1 << 1), // B -> B + (1 << 2), // X -> X + (1 << 3), // Y -> Y + (1 << 4), // BACK -> BACK + (1 << 5), // MODE -> GUIDE + (1 << 6), // START -> START + (1 << 7), // THUMBL -> LEFTSTICK + (1 << 8), // THUMBR -> RIGHTSTICK + (1 << 9), // L1 -> LEFTSHOULDER + (1 << 10), // R1 -> RIGHTSHOULDER + (1 << 11), // DPAD_UP -> DPAD_UP + (1 << 12), // DPAD_DOWN -> DPAD_DOWN + (1 << 13), // DPAD_LEFT -> DPAD_LEFT + (1 << 14), // DPAD_RIGHT -> DPAD_RIGHT + (1 << 4), // SELECT -> BACK + (1 << 0), // DPAD_CENTER -> A + (1 << 15), // L2 -> ?? + (1 << 16), // R2 -> ?? + (1 << 17), // C -> ?? + (1 << 18), // Z -> ?? + (1 << 20), // 1 -> ?? + (1 << 21), // 2 -> ?? + (1 << 22), // 3 -> ?? + (1 << 23), // 4 -> ?? + (1 << 24), // 5 -> ?? + (1 << 25), // 6 -> ?? + (1 << 26), // 7 -> ?? + (1 << 27), // 8 -> ?? + (1 << 28), // 9 -> ?? + (1 << 29), // 10 -> ?? + (1 << 30), // 11 -> ?? + (1 << 31), // 12 -> ?? + // We're out of room... + 0xFFFFFFFF, // 13 -> ?? + 0xFFFFFFFF, // 14 -> ?? + 0xFFFFFFFF, // 15 -> ?? + 0xFFFFFFFF, // 16 -> ?? + }; + boolean[] has_keys = joystickDevice.hasKeys(keys); + for (int i = 0; i < keys.length; ++i) { + if (has_keys[i]) { + button_mask |= masks[i]; + } + } + return button_mask; + } +} + +class SDLHapticHandler_API26 extends SDLHapticHandler { + @Override + public void run(int device_id, float intensity, int length) { + SDLHaptic haptic = getHaptic(device_id); + if (haptic != null) { + Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length); + if (intensity == 0.0f) { + stop(device_id); + return; + } + + int vibeValue = Math.round(intensity * 255); + + if (vibeValue > 255) { + vibeValue = 255; + } + if (vibeValue < 1) { + stop(device_id); + return; + } + try { + haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue)); + } + catch (Exception e) { + // Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if + // something went horribly wrong with the Android 8.0 APIs. + haptic.vib.vibrate(length); + } + } + } +} + +class SDLHapticHandler { + + class SDLHaptic { + public int device_id; + public String name; + public Vibrator vib; + } + + private ArrayList mHaptics; + + public SDLHapticHandler() { + mHaptics = new ArrayList(); + } + + public void run(int device_id, float intensity, int length) { + SDLHaptic haptic = getHaptic(device_id); + if (haptic != null) { + haptic.vib.vibrate(length); + } + } + + public void stop(int device_id) { + SDLHaptic haptic = getHaptic(device_id); + if (haptic != null) { + haptic.vib.cancel(); + } + } + + public void pollHapticDevices() { + + final int deviceId_VIBRATOR_SERVICE = 999999; + boolean hasVibratorService = false; + + int[] deviceIds = InputDevice.getDeviceIds(); + // It helps processing the device ids in reverse order + // For example, in the case of the XBox 360 wireless dongle, + // so the first controller seen by SDL matches what the receiver + // considers to be the first controller + + if (Build.VERSION.SDK_INT >= 16) + { + for (int i = deviceIds.length - 1; i > -1; i--) { + SDLHaptic haptic = getHaptic(deviceIds[i]); + if (haptic == null) { + InputDevice device = InputDevice.getDevice(deviceIds[i]); + Vibrator vib = device.getVibrator(); + if (vib.hasVibrator()) { + haptic = new SDLHaptic(); + haptic.device_id = deviceIds[i]; + haptic.name = device.getName(); + haptic.vib = vib; + mHaptics.add(haptic); + SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name); + } + } + } + } + + /* Check VIBRATOR_SERVICE */ + Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE); + if (vib != null) { + if (Build.VERSION.SDK_INT >= 11) { + hasVibratorService = vib.hasVibrator(); + } else { + hasVibratorService = true; + } + + if (hasVibratorService) { + SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE); + if (haptic == null) { + haptic = new SDLHaptic(); + haptic.device_id = deviceId_VIBRATOR_SERVICE; + haptic.name = "VIBRATOR_SERVICE"; + haptic.vib = vib; + mHaptics.add(haptic); + SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name); + } + } + } + + /* Check removed devices */ + ArrayList removedDevices = new ArrayList(); + for(int i=0; i < mHaptics.size(); i++) { + int device_id = mHaptics.get(i).device_id; + int j; + for (j=0; j < deviceIds.length; j++) { + if (device_id == deviceIds[j]) break; + } + + if (device_id == deviceId_VIBRATOR_SERVICE && hasVibratorService) { + // don't remove the vibrator if it is still present + } else if (j == deviceIds.length) { + removedDevices.add(device_id); + } + } + + for(int i=0; i < removedDevices.size(); i++) { + int device_id = removedDevices.get(i); + SDLControllerManager.nativeRemoveHaptic(device_id); + for (int j=0; j < mHaptics.size(); j++) { + if (mHaptics.get(j).device_id == device_id) { + mHaptics.remove(j); + break; + } + } + } + } + + protected SDLHaptic getHaptic(int device_id) { + for(int i=0; i < mHaptics.size(); i++) { + if (mHaptics.get(i).device_id == device_id) { + return mHaptics.get(i); + } + } + return null; + } +} + +class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener { + // Generic Motion (mouse hover, joystick...) events go here + @Override + public boolean onGenericMotion(View v, MotionEvent event) { + float x, y; + int action; + + switch ( event.getSource() ) { + case InputDevice.SOURCE_JOYSTICK: + case InputDevice.SOURCE_GAMEPAD: + case InputDevice.SOURCE_DPAD: + return SDLControllerManager.handleJoystickMotionEvent(event); + + case InputDevice.SOURCE_MOUSE: + if (!SDLActivity.mSeparateMouseAndTouch) { + break; + } + action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_SCROLL: + x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); + y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); + SDLActivity.onNativeMouse(0, action, x, y, false); + return true; + + case MotionEvent.ACTION_HOVER_MOVE: + x = event.getX(0); + y = event.getY(0); + + SDLActivity.onNativeMouse(0, action, x, y, false); + return true; + + default: + break; + } + break; + + default: + break; + } + + // Event was not managed + return false; + } + + public boolean supportsRelativeMouse() { + return false; + } + + public boolean inRelativeMode() { + return false; + } + + public boolean setRelativeMouseEnabled(boolean enabled) { + return false; + } + + public void reclaimRelativeMouseModeIfNeeded() + { + + } + + public float getEventX(MotionEvent event) { + return event.getX(0); + } + + public float getEventY(MotionEvent event) { + return event.getY(0); + } + +} + +class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 { + // Generic Motion (mouse hover, joystick...) events go here + + private boolean mRelativeModeEnabled; + + @Override + public boolean onGenericMotion(View v, MotionEvent event) { + float x, y; + int action; + + switch ( event.getSource() ) { + case InputDevice.SOURCE_JOYSTICK: + case InputDevice.SOURCE_GAMEPAD: + case InputDevice.SOURCE_DPAD: + return SDLControllerManager.handleJoystickMotionEvent(event); + + case InputDevice.SOURCE_MOUSE: + if (!SDLActivity.mSeparateMouseAndTouch) { + break; + } + action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_SCROLL: + x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); + y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); + SDLActivity.onNativeMouse(0, action, x, y, false); + return true; + + case MotionEvent.ACTION_HOVER_MOVE: + if (mRelativeModeEnabled) { + x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); + y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); + } + else { + x = event.getX(0); + y = event.getY(0); + } + + SDLActivity.onNativeMouse(0, action, x, y, mRelativeModeEnabled); + return true; + + default: + break; + } + break; + + default: + break; + } + + // Event was not managed + return false; + } + + @Override + public boolean supportsRelativeMouse() { + return true; + } + + @Override + public boolean inRelativeMode() { + return mRelativeModeEnabled; + } + + @Override + public boolean setRelativeMouseEnabled(boolean enabled) { + mRelativeModeEnabled = enabled; + return true; + } + + @Override + public float getEventX(MotionEvent event) { + if (mRelativeModeEnabled) { + return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); + } + else { + return event.getX(0); + } + } + + @Override + public float getEventY(MotionEvent event) { + if (mRelativeModeEnabled) { + return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); + } + else { + return event.getY(0); + } + } +} + + +class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 { + // Generic Motion (mouse hover, joystick...) events go here + private boolean mRelativeModeEnabled; + + @Override + public boolean onGenericMotion(View v, MotionEvent event) { + float x, y; + int action; + + switch ( event.getSource() ) { + case InputDevice.SOURCE_JOYSTICK: + case InputDevice.SOURCE_GAMEPAD: + case InputDevice.SOURCE_DPAD: + return SDLControllerManager.handleJoystickMotionEvent(event); + + case InputDevice.SOURCE_MOUSE: + case 12290: // DeX desktop mouse cursor is a separate non-standard input type. + if (!SDLActivity.mSeparateMouseAndTouch) { + break; + } + + action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_SCROLL: + x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); + y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); + SDLActivity.onNativeMouse(0, action, x, y, false); + return true; + + case MotionEvent.ACTION_HOVER_MOVE: + x = event.getX(0); + y = event.getY(0); + SDLActivity.onNativeMouse(0, action, x, y, false); + return true; + + default: + break; + } + break; + + case InputDevice.SOURCE_MOUSE_RELATIVE: + if (!SDLActivity.mSeparateMouseAndTouch) { + break; + } + action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_SCROLL: + x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); + y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); + SDLActivity.onNativeMouse(0, action, x, y, false); + return true; + + case MotionEvent.ACTION_HOVER_MOVE: + x = event.getX(0); + y = event.getY(0); + SDLActivity.onNativeMouse(0, action, x, y, true); + return true; + + default: + break; + } + break; + + default: + break; + } + + // Event was not managed + return false; + } + + @Override + public boolean supportsRelativeMouse() { + return (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)); + } + + @Override + public boolean inRelativeMode() { + return mRelativeModeEnabled; + } + + @Override + public boolean setRelativeMouseEnabled(boolean enabled) { + if (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)) { + if (enabled) { + SDLActivity.getContentView().requestPointerCapture(); + } + else { + SDLActivity.getContentView().releasePointerCapture(); + } + mRelativeModeEnabled = enabled; + return true; + } + else + { + return false; + } + } + + @Override + public void reclaimRelativeMouseModeIfNeeded() + { + if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) { + SDLActivity.getContentView().requestPointerCapture(); + } + } + + @Override + public float getEventX(MotionEvent event) { + // Relative mouse in capture mode will only have relative for X/Y + return event.getX(0); + } + + @Override + public float getEventY(MotionEvent event) { + // Relative mouse in capture mode will only have relative for X/Y + return event.getY(0); + } +} diff --git a/pythonforandroid/bootstraps/sdl2/build/src/patches/SDLActivity.java.patch b/pythonforandroid/bootstraps/sdl2/build/src/patches/SDLActivity.java.patch new file mode 100644 index 0000000000..27b97a7fbb --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2/build/src/patches/SDLActivity.java.patch @@ -0,0 +1,74 @@ +--- a/src/main/java/org/libsdl/app/SDLActivity.java ++++ b/src/main/java/org/libsdl/app/SDLActivity.java +@@ -196,6 +196,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() { ++ + // Load shared libraries + String errorMsgBrokenLib = ""; + try { +@@ -639,7 +648,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; +@@ -1051,6 +1061,21 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh + return Arrays.copyOf(filtered, used); + } + ++ /** ++ * 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() { ++ } ++ ++ + // APK expansion files support + + /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ +@@ -1341,14 +1366,13 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh + }; + + public void onSystemUiVisibilityChange(int visibility) { +- if (SDLActivity.mFullscreenModeActive && (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { +- ++ // SDL2 BUGFIX (see sdl bug #4424 ) - REMOVE WHEN FIXED IN UPSTREAM !! ++ if (SDLActivity.mFullscreenModeActive && ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)) { + Handler handler = getWindow().getDecorView().getHandler(); + if (handler != null) { + handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop. + handler.postDelayed(rehideSystemUi, 2000); + } +- + } + } + +@@ -1475,6 +1499,7 @@ class SDLMain implements Runnable { + String[] arguments = SDLActivity.mSingleton.getArguments(); + + Log.v("SDL", "Running main function " + function + " from library " + library); ++ SDLActivity.mSingleton.appConfirmedActive(); + SDLActivity.nativeRunMain(library, function, arguments); + + Log.v("SDL", "Finished main function"); diff --git a/pythonforandroid/bootstraps/service_only/__init__.py b/pythonforandroid/bootstraps/service_only/__init__.py index 3b10e8e783..2175f8b61f 100644 --- a/pythonforandroid/bootstraps/service_only/__init__.py +++ b/pythonforandroid/bootstraps/service_only/__init__.py @@ -9,7 +9,9 @@ class ServiceOnlyBootstrap(Bootstrap): name = 'service_only' - recipe_depends = ['genericndkbuild', ('python2', 'python3', 'python3crystax')] + recipe_depends = list( + set(Bootstrap.recipe_depends).union({'genericndkbuild'}) + ) def run_distribute(self): info_main('# Creating Android project from build and {} bootstrap'.format( diff --git a/pythonforandroid/bootstraps/service_only/build/jni/application/src/Android.mk b/pythonforandroid/bootstraps/service_only/build/jni/application/src/Android.mk index 6a8f1a65a2..0bc42bfb89 100644 --- a/pythonforandroid/bootstraps/service_only/build/jni/application/src/Android.mk +++ b/pythonforandroid/bootstraps/service_only/build/jni/application/src/Android.mk @@ -7,13 +7,13 @@ LOCAL_MODULE := main # Add your application source files here... LOCAL_SRC_FILES := start.c pyjniusjni.c -LOCAL_CFLAGS += -I$(LOCAL_PATH)/../../../../../other_builds/$(MK_PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS) +LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS) LOCAL_SHARED_LIBRARIES := python_shared LOCAL_LDLIBS := -llog $(EXTRA_LDLIBS) -LOCAL_LDFLAGS += -L$(LOCAL_PATH)/../../../../../other_builds/$(MK_PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) +LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) include $(BUILD_SHARED_LIBRARY) diff --git a/pythonforandroid/bootstraps/service_only/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/service_only/build/src/main/java/org/kivy/android/PythonActivity.java index 9ca6bd1ec8..3390806041 100644 --- a/pythonforandroid/bootstraps/service_only/build/src/main/java/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/service_only/build/src/main/java/org/kivy/android/PythonActivity.java @@ -74,6 +74,22 @@ public String getAppRoot() { return app_root; } + public String getEntryPoint(String search_dir) { + /* Get the main file (.pyc|.pyo|.py) depending on if we + * have a compiled version or not. + */ + List entryPoints = new ArrayList(); + entryPoints.add("main.pyo"); // python 2 compiled files + 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"; + } + public static void initialize() { // The static nature of the singleton and Android quirkiness force us to initialize everything here // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values @@ -142,16 +158,17 @@ public void onClick(DialogInterface dialog,int id) { // Set up the Python environment String app_root_dir = getAppRoot(); String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); + String entry_point = getEntryPoint(app_root_dir); Log.v(TAG, "Setting env vars for start.c and Python to use"); - PythonActivity.nativeSetEnv("ANDROID_ENTRYPOINT", "main.pyo"); - PythonActivity.nativeSetEnv("ANDROID_ARGUMENT", app_root_dir); - PythonActivity.nativeSetEnv("ANDROID_APP_PATH", app_root_dir); - PythonActivity.nativeSetEnv("ANDROID_PRIVATE", mFilesDirectory); - PythonActivity.nativeSetEnv("ANDROID_UNPACK", app_root_dir); - PythonActivity.nativeSetEnv("PYTHONHOME", app_root_dir); - PythonActivity.nativeSetEnv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); - PythonActivity.nativeSetEnv("PYTHONOPTIMIZE", "2"); + PythonActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point); + PythonActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir); + PythonActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir); + PythonActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory); + PythonActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir); + PythonActivity.nativeSetenv("PYTHONHOME", app_root_dir); + PythonActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); + PythonActivity.nativeSetenv("PYTHONOPTIMIZE", "2"); try { Log.v(TAG, "Access to our meta-data..."); @@ -368,9 +385,10 @@ public static void start_service(String serviceTitle, String serviceDescription, String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String filesDirectory = argument; 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/main.pyo"); + 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"); @@ -386,7 +404,7 @@ public static void stop_service() { } - public static native void nativeSetEnv(String j_name, String j_value); + public static native void nativeSetenv(String name, String value); public static native int nativeInit(Object arguments); } diff --git a/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java b/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java index 137181a7f9..622fbffa02 100644 --- a/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java +++ b/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java @@ -1,5 +1,7 @@ package {{ args.package }}; +import android.os.Binder; +import android.os.IBinder; import android.content.Intent; import android.content.Context; import org.kivy.android.PythonService; @@ -31,7 +33,7 @@ public boolean getStartForeground() { {% endif %} public static void start(Context ctx, String pythonServiceArgument) { - String argument = ctx.getFilesDir().getAbsolutePath(); + String argument = ctx.getFilesDir().getAbsolutePath() + "/app"; Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); intent.putExtra("androidPrivate", argument); intent.putExtra("androidArgument", argument); @@ -45,7 +47,7 @@ public static void start(Context ctx, String pythonServiceArgument) { intent.putExtra("pythonServiceArgument", pythonServiceArgument); ctx.startService(intent); } - + public static void stop(Context ctx) { Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); ctx.stopService(intent); @@ -58,7 +60,7 @@ public static void stop(Context ctx) { public IBinder onBind(Intent intent) { return mBinder; } - + /** * Class used for the client Binder. Because we know this service always * runs in the same process as its clients, we don't need to deal with IPC. diff --git a/pythonforandroid/bootstraps/webview/__init__.py b/pythonforandroid/bootstraps/webview/__init__.py index 21d0da5996..c59c158356 100644 --- a/pythonforandroid/bootstraps/webview/__init__.py +++ b/pythonforandroid/bootstraps/webview/__init__.py @@ -7,7 +7,9 @@ class WebViewBootstrap(Bootstrap): name = 'webview' - recipe_depends = ['genericndkbuild', ('python2', 'python3', 'python3crystax')] + recipe_depends = list( + set(Bootstrap.recipe_depends).union({'genericndkbuild'}) + ) def run_distribute(self): info_main('# Creating Android project from build and {} bootstrap'.format( diff --git a/pythonforandroid/bootstraps/webview/build/jni/application/src/Android.mk b/pythonforandroid/bootstraps/webview/build/jni/application/src/Android.mk index b1403ec110..20399573c9 100644 --- a/pythonforandroid/bootstraps/webview/build/jni/application/src/Android.mk +++ b/pythonforandroid/bootstraps/webview/build/jni/application/src/Android.mk @@ -9,13 +9,13 @@ LOCAL_MODULE := main # Add your application source files here... LOCAL_SRC_FILES := start.c pyjniusjni.c -LOCAL_CFLAGS += -I$(LOCAL_PATH)/../../../../../other_builds/$(MK_PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS) +LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS) LOCAL_SHARED_LIBRARIES := python_shared LOCAL_LDLIBS := -llog $(EXTRA_LDLIBS) -LOCAL_LDFLAGS += -L$(LOCAL_PATH)/../../../../../other_builds/$(MK_PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) +LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) include $(BUILD_SHARED_LIBRARY) diff --git a/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java index a62fc216c9..1a37bc672f 100644 --- a/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java @@ -81,6 +81,22 @@ public String getAppRoot() { return app_root; } + public String getEntryPoint(String search_dir) { + /* Get the main file (.pyc|.pyo|.py) depending on if we + * have a compiled version or not. + */ + List entryPoints = new ArrayList(); + entryPoints.add("main.pyo"); // python 2 compiled files + 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"; + } + public static void initialize() { // The static nature of the singleton and Android quirkyness force us to initialize everything here // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values @@ -170,16 +186,17 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { setContentView(mLayout); String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); + String entry_point = getEntryPoint(app_root_dir); Log.v(TAG, "Setting env vars for start.c and Python to use"); - PythonActivity.nativeSetEnv("ANDROID_ENTRYPOINT", "main.pyo"); - PythonActivity.nativeSetEnv("ANDROID_ARGUMENT", app_root_dir); - PythonActivity.nativeSetEnv("ANDROID_APP_PATH", app_root_dir); - PythonActivity.nativeSetEnv("ANDROID_PRIVATE", mFilesDirectory); - PythonActivity.nativeSetEnv("ANDROID_UNPACK", app_root_dir); - PythonActivity.nativeSetEnv("PYTHONHOME", app_root_dir); - PythonActivity.nativeSetEnv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); - PythonActivity.nativeSetEnv("PYTHONOPTIMIZE", "2"); + PythonActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point); + PythonActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir); + PythonActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir); + PythonActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory); + PythonActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir); + PythonActivity.nativeSetenv("PYTHONHOME", app_root_dir); + PythonActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); + PythonActivity.nativeSetenv("PYTHONOPTIMIZE", "2"); try { Log.v(TAG, "Access to our meta-data..."); @@ -425,9 +442,10 @@ public static void start_service(String serviceTitle, String serviceDescription, String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String filesDirectory = argument; 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/main.pyo"); + 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"); @@ -443,7 +461,7 @@ public static void stop_service() { } - public static native void nativeSetEnv(String j_name, String j_value); + public static native void nativeSetenv(String name, String value); public static native int nativeInit(Object arguments); } diff --git a/pythonforandroid/build.py b/pythonforandroid/build.py index 23a4392589..734103b20c 100644 --- a/pythonforandroid/build.py +++ b/pythonforandroid/build.py @@ -11,14 +11,16 @@ import sh import subprocess -from pythonforandroid.util import (ensure_dir, current_directory, BuildInterruptingException) +from pythonforandroid.util import ( + current_directory, ensure_dir, get_virtualenv_executable, + BuildInterruptingException +) from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint) from pythonforandroid.archs import ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64 from pythonforandroid.recipe import CythonRecipe, Recipe - -DEFAULT_ANDROID_API = 15 - -DEFAULT_NDK_API = 21 +from pythonforandroid.recommendations import ( + check_ndk_version, check_target_api, check_ndk_api, + RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API) class Context(object): @@ -140,19 +142,6 @@ def ndk_api(self): def ndk_api(self, value): self._ndk_api = value - @property - def ndk_ver(self): - '''The version of the NDK being used for compilation.''' - if self._ndk_ver is None: - raise ValueError('Tried to access ndk_ver but it has not ' - 'been set - this should not happen, something ' - 'went wrong!') - return self._ndk_ver - - @ndk_ver.setter - def ndk_ver(self, value): - self._ndk_ver = value - @property def sdk_dir(self): '''The path to the Android SDK.''' @@ -183,7 +172,6 @@ def prepare_build_environment(self, user_sdk_dir, user_ndk_dir, user_android_api, - user_ndk_ver, user_ndk_api): '''Checks that build dependencies exist and sets internal variables for the Android SDK etc. @@ -237,17 +225,12 @@ def prepare_build_environment(self, info('Found Android API target in $ANDROIDAPI: {}'.format(android_api)) else: info('Android API target was not set manually, using ' - 'the default of {}'.format(DEFAULT_ANDROID_API)) - android_api = DEFAULT_ANDROID_API + 'the default of {}'.format(RECOMMENDED_TARGET_API)) + android_api = RECOMMENDED_TARGET_API android_api = int(android_api) self.android_api = android_api - if self.android_api >= 21 and self.archs[0].arch == 'armeabi': - raise BuildInterruptingException( - 'Asked to build for armeabi architecture with API ' - '{}, but API 21 or greater does not support armeabi'.format( - self.android_api), - instructions='You probably want to build with --arch=armeabi-v7a instead') + check_target_api(android_api, self.archs[0].arch) if exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')): avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager')) @@ -306,47 +289,7 @@ def prepare_build_environment(self, raise BuildInterruptingException('Android NDK dir was not specified') self.ndk_dir = realpath(ndk_dir) - # Find the NDK version, and check it against what the NDK dir - # seems to report - ndk_ver = None - if user_ndk_ver: - ndk_ver = user_ndk_ver - if ndk_dir is not None: - info('Got NDK version from from user argument: {}'.format(ndk_ver)) - if ndk_ver is None: - ndk_ver = environ.get('ANDROIDNDKVER', None) - if ndk_ver is not None: - info('Got NDK version from $ANDROIDNDKVER: {}'.format(ndk_ver)) - - self.ndk = 'google' - - try: - with open(join(ndk_dir, 'RELEASE.TXT')) as fileh: - reported_ndk_ver = fileh.read().split(' ')[0].strip() - except IOError: - pass - else: - if reported_ndk_ver.startswith('crystax-ndk-'): - reported_ndk_ver = reported_ndk_ver[12:] - self.ndk = 'crystax' - if ndk_ver is None: - ndk_ver = reported_ndk_ver - info(('Got Android NDK version from the NDK dir: {}').format(ndk_ver)) - else: - if ndk_ver != reported_ndk_ver: - warning('NDK version was set as {}, but checking ' - 'the NDK dir claims it is {}.'.format( - ndk_ver, reported_ndk_ver)) - warning('The build will try to continue, but it may ' - 'fail and you should check ' - 'that your setting is correct.') - warning('If the NDK dir result is correct, you don\'t ' - 'need to manually set the NDK ver.') - if ndk_ver is None: - warning('Android NDK version could not be found. This probably' - 'won\'t cause any problems, but if necessary you can' - 'set it with `--ndk-version=...`.') - self.ndk_ver = ndk_ver + check_ndk_version(ndk_dir) ndk_api = None if user_ndk_api: @@ -356,29 +299,16 @@ def prepare_build_environment(self, ndk_api = environ.get('NDKAPI', None) info('Found Android API target in $NDKAPI') else: - ndk_api = min(self.android_api, DEFAULT_NDK_API) + ndk_api = min(self.android_api, RECOMMENDED_NDK_API) warning('NDK API target was not set manually, using ' 'the default of {} = min(android-api={}, default ndk-api={})'.format( - ndk_api, self.android_api, DEFAULT_NDK_API)) + ndk_api, self.android_api, RECOMMENDED_NDK_API)) ndk_api = int(ndk_api) self.ndk_api = ndk_api - if self.ndk_api > self.android_api: - raise BuildInterruptingException( - 'Target NDK API is {}, higher than the target Android API {}.'.format( - self.ndk_api, self.android_api), - instructions=('The NDK API is a minimum supported API number and must be lower ' - 'than the target Android API')) - - info('Using {} NDK {}'.format(self.ndk.capitalize(), self.ndk_ver)) + check_ndk_api(ndk_api, self.android_api) - virtualenv = None - if virtualenv is None: - virtualenv = sh.which('virtualenv2') - if virtualenv is None: - virtualenv = sh.which('virtualenv-2.7') - if virtualenv is None: - virtualenv = sh.which('virtualenv') + virtualenv = get_virtualenv_executable() if virtualenv is None: raise IOError('Couldn\'t find a virtualenv executable, ' 'you must install this to use p4a.') @@ -483,7 +413,6 @@ def __init__(self): self._ndk_dir = None self._android_api = None self._ndk_api = None - self._ndk_ver = None self.ndk = None self.toolchain_prefix = None @@ -577,7 +506,7 @@ def has_package(self, name, arch=None): # Try to look up recipe by name: try: recipe = Recipe.get_recipe(name, self) - except IOError: + except ValueError: pass else: name = getattr(recipe, 'site_packages_name', None) or name @@ -596,7 +525,6 @@ def not_has_package(self, name, arch=None): def build_recipes(build_order, python_modules, ctx): # Put recipes in correct build order - bs = ctx.bootstrap info_notify("Recipe build order is {}".format(build_order)) if python_modules: python_modules = sorted(set(python_modules)) @@ -687,14 +615,15 @@ def run_pymodules_install(ctx, modules): line = '{}\n'.format(module) fileh.write(line) + # Prepare base environment and upgrade pip: base_env = copy.copy(os.environ) base_env["PYTHONPATH"] = ctx.get_site_packages_dir() - info('Upgrade pip to latest version') shprint(sh.bash, '-c', ( "source venv/bin/activate && pip install -U pip" ), _env=copy.copy(base_env)) + # Install Cython in case modules need it to build: info('Install Cython in case one of the modules needs it to build') shprint(sh.bash, '-c', ( "venv/bin/pip install Cython" @@ -717,15 +646,17 @@ def run_pymodules_install(ctx, modules): 'changes / workarounds.') # Make sure our build package dir is available, and the virtualenv - # site packages come FIRST (for the proper pip version): + # site packages come FIRST (so the proper pip version is used): env["PYTHONPATH"] += ":" + ctx.get_site_packages_dir() env["PYTHONPATH"] = os.path.abspath(join( ctx.build_dir, "venv", "lib", "python" + ctx.python_recipe.major_minor_version_string, "site-packages")) + ":" + env["PYTHONPATH"] + + # Do actual install: shprint(sh.bash, '-c', ( - "source venv/bin/activate && " + - "pip install -v --target '{0}' --no-deps -r requirements.txt" + "venv/bin/pip " + + "install -v --target '{0}' --no-deps -r requirements.txt" ).format(ctx.get_site_packages_dir().replace("'", "'\"'\"'")), _env=copy.copy(env)) diff --git a/pythonforandroid/distribution.py b/pythonforandroid/distribution.py index 9aa10ee82f..9fa7b4c6b2 100644 --- a/pythonforandroid/distribution.py +++ b/pythonforandroid/distribution.py @@ -79,8 +79,6 @@ def get_distribution(cls, ctx, name=None, recipes=[], existing_dists = Distribution.get_distributions(ctx) - needs_build = True # whether the dist needs building, will be returned - possible_dists = existing_dists name_match_dist = None @@ -216,7 +214,9 @@ def save_info(self, dirn): 'bootstrap': self.ctx.bootstrap.name, 'archs': [arch.arch for arch in self.ctx.archs], 'ndk_api': self.ctx.ndk_api, - 'recipes': self.ctx.recipe_build_order + self.ctx.python_modules}, + 'recipes': self.ctx.recipe_build_order + self.ctx.python_modules, + 'hostpython': self.ctx.hostpython, + 'python_version': self.ctx.python_recipe.major_minor_version_string}, fileh) diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index 2207957769..2e98e8ccda 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -1,23 +1,37 @@ from copy import deepcopy from itertools import product -from pythonforandroid.logger import (info, warning) +from pythonforandroid.logger import info from pythonforandroid.recipe import Recipe from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.util import BuildInterruptingException -class RecipeOrder(dict): +def fix_deplist(deps): + """ Turn a dependency list into lowercase, and make sure all entries + that are just a string become a tuple of strings + """ + deps = [ + ((dep.lower(),) + if not isinstance(dep, (list, tuple)) + else tuple([dep_entry.lower() + for dep_entry in dep + ])) + for dep in deps + ] + return deps + +class RecipeOrder(dict): def __init__(self, ctx): self.ctx = ctx - def conflicts(self, name): + def conflicts(self): for name in self.keys(): try: recipe = Recipe.get_recipe(name, self.ctx) - conflicts = recipe.conflicts - except IOError: + conflicts = [dep.lower() for dep in recipe.conflicts] + except ValueError: conflicts = [] if any([c in self for c in conflicts]): @@ -25,26 +39,59 @@ def conflicts(self, name): return False -def recursively_collect_orders(name, ctx, orders=[]): +def get_dependency_tuple_list_for_recipe(recipe, blacklist=None): + """ Get the dependencies of a recipe with filtered out blacklist, and + turned into tuples with fix_deplist() + """ + if blacklist is None: + blacklist = set() + assert(type(blacklist) == set) + if recipe.depends is None: + dependencies = [] + else: + # Turn all dependencies into tuples so that product will work + dependencies = fix_deplist(recipe.depends) + + # Filter out blacklisted items and turn lowercase: + dependencies = [ + tuple(set(deptuple) - blacklist) + for deptuple in dependencies + if tuple(set(deptuple) - blacklist) + ] + return dependencies + + +def recursively_collect_orders( + name, ctx, all_inputs, orders=None, blacklist=None + ): '''For each possible recipe ordering, try to add the new recipe name to that order. Recursively do the same thing with all the dependencies of each recipe. ''' + name = name.lower() + if orders is None: + orders = [] + if blacklist is None: + blacklist = set() try: recipe = Recipe.get_recipe(name, ctx) - if recipe.depends is None: - dependencies = [] - else: - # make all dependencies into lists so that product will work - dependencies = [([dependency] if not isinstance( - dependency, (list, tuple)) - else dependency) for dependency in recipe.depends] + dependencies = get_dependency_tuple_list_for_recipe( + recipe, blacklist=blacklist + ) + + # handle opt_depends: these impose requirements on the build + # order only if already present in the list of recipes to build + dependencies.extend(fix_deplist( + [[d] for d in recipe.get_opt_depends_in_list(all_inputs) + if d.lower() not in blacklist] + )) + if recipe.conflicts is None: conflicts = [] else: - conflicts = recipe.conflicts - except IOError: + conflicts = [dep.lower() for dep in recipe.conflicts] + except ValueError: # The recipe does not exist, so we assume it can be installed # via pip with no extra dependencies dependencies = [] @@ -56,7 +103,7 @@ def recursively_collect_orders(name, ctx, orders=[]): if name in order: new_orders.append(deepcopy(order)) continue - if order.conflicts(name): + if order.conflicts(): continue if any([conflict in order for conflict in conflicts]): continue @@ -68,7 +115,9 @@ def recursively_collect_orders(name, ctx, orders=[]): dependency_new_orders = [new_order] for dependency in dependency_set: dependency_new_orders = recursively_collect_orders( - dependency, ctx, dependency_new_orders) + dependency, ctx, all_inputs, dependency_new_orders, + blacklist=blacklist + ) new_orders.extend(dependency_new_orders) @@ -94,22 +143,142 @@ def find_order(graph): bset.discard(result) -def get_recipe_order_and_bootstrap(ctx, names, bs=None): - recipes_to_load = set(names) +def obvious_conflict_checker(ctx, name_tuples, blacklist=None): + """ This is a pre-flight check function that will completely ignore + recipe order or choosing an actual value in any of the multiple + choice tuples/dependencies, and just do a very basic obvious + conflict check. + """ + deps_were_added_by = dict() + deps = set() + if blacklist is None: + blacklist = set() + + # Add dependencies for all recipes: + to_be_added = [(name_tuple, None) for name_tuple in name_tuples] + while len(to_be_added) > 0: + current_to_be_added = list(to_be_added) + to_be_added = [] + for (added_tuple, adding_recipe) in current_to_be_added: + assert(type(added_tuple) == tuple) + if len(added_tuple) > 1: + # No obvious commitment in what to add, don't check it itself + # but throw it into deps for later comparing against + # (Remember this function only catches obvious issues) + deps.add(added_tuple) + continue + + name = added_tuple[0] + recipe_conflicts = set() + recipe_dependencies = [] + try: + # Get recipe to add and who's ultimately adding it: + recipe = Recipe.get_recipe(name, ctx) + recipe_conflicts = {c.lower() for c in recipe.conflicts} + recipe_dependencies = get_dependency_tuple_list_for_recipe( + recipe, blacklist=blacklist + ) + except ValueError: + pass + adder_first_recipe_name = adding_recipe or name + + # Collect the conflicts: + triggered_conflicts = [] + for dep_tuple_list in deps: + # See if the new deps conflict with things added before: + if set(dep_tuple_list).intersection( + recipe_conflicts) == set(dep_tuple_list): + triggered_conflicts.append(dep_tuple_list) + continue + + # See if what was added before conflicts with the new deps: + if len(dep_tuple_list) > 1: + # Not an obvious commitment to a specific recipe/dep + # to be added, so we won't check. + # (remember this function only catches obvious issues) + continue + try: + dep_recipe = Recipe.get_recipe(dep_tuple_list[0], ctx) + except ValueError: + continue + conflicts = [c.lower() for c in dep_recipe.conflicts] + if name in conflicts: + triggered_conflicts.append(dep_tuple_list) + + # Throw error on conflict: + if triggered_conflicts: + # Get first conflict and see who added that one: + adder_second_recipe_name = "'||'".join(triggered_conflicts[0]) + second_recipe_original_adder = deps_were_added_by.get( + (adder_second_recipe_name,), None + ) + if second_recipe_original_adder: + adder_second_recipe_name = second_recipe_original_adder + + # Prompt error: + raise BuildInterruptingException( + "Conflict detected: '{}'" + " inducing dependencies {}, and '{}'" + " inducing conflicting dependencies {}".format( + adder_first_recipe_name, + (recipe.name,), + adder_second_recipe_name, + triggered_conflicts[0] + )) + + # Actually add it to our list: + deps.add(added_tuple) + deps_were_added_by[added_tuple] = adding_recipe + + # Schedule dependencies to be added + to_be_added += [ + (dep, adder_first_recipe_name or name) + for dep in recipe_dependencies + if dep not in deps + ] + # If we came here, then there were no obvious conflicts. + return None + + +def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=None): + # Get set of recipe/dependency names, clean up and add bootstrap deps: + names = set(names) if bs is not None and bs.recipe_depends: - recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) + names = names.union(set(bs.recipe_depends)) + names = fix_deplist([ + ([name] if not isinstance(name, (list, tuple)) else name) + for name in names + ]) + if blacklist is None: + blacklist = set() + blacklist = {bitem.lower() for bitem in blacklist} - possible_orders = [] + # Remove all values that are in the blacklist: + names_before_blacklist = list(names) + names = [] + for name in names_before_blacklist: + cleaned_up_tuple = tuple([ + item for item in name if item not in blacklist + ]) + if cleaned_up_tuple: + names.append(cleaned_up_tuple) + + # Do check for obvious conflicts (that would trigger in any order, and + # without comitting to any specific choice in a multi-choice tuple of + # dependencies): + obvious_conflict_checker(ctx, names, blacklist=blacklist) + # If we get here, no obvious conflicts! # get all possible order graphs, as names may include tuples/lists # of alternative dependencies - names = [([name] if not isinstance(name, (list, tuple)) else name) - for name in names] + possible_orders = [] for name_set in product(*names): new_possible_orders = [RecipeOrder(ctx)] for name in name_set: new_possible_orders = recursively_collect_orders( - name, ctx, orders=new_possible_orders) + name, ctx, name_set, orders=new_possible_orders, + blacklist=blacklist + ) possible_orders.extend(new_possible_orders) # turn each order graph into a linear list if possible @@ -121,20 +290,16 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): info('Circular dependency found in graph {}, skipping it.'.format( possible_order)) continue - except: - warning('Failed to import recipe named {}; the recipe exists ' - 'but appears broken.'.format(name)) - warning('Exception was:') - raise orders.append(list(order)) - # prefer python2 and SDL2 if available + # prefer python3 and SDL2 if available orders = sorted(orders, - key=lambda order: -('python2' in order) - ('sdl2' in order)) + key=lambda order: -('python3' in order) - ('sdl2' in order)) if not orders: raise BuildInterruptingException( - 'Didn\'t find any valid dependency graphs. This means that some of your ' + 'Didn\'t find any valid dependency graphs. ' + 'This means that some of your ' 'requirements pull in conflicting dependencies.') # It would be better to check against possible orders other @@ -151,8 +316,14 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): if bs is None: bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) + if bs is None: + # Note: don't remove this without thought, causes infinite loop + raise BuildInterruptingException( + "Could not find any compatible bootstrap!" + ) recipes, python_modules, bs = get_recipe_order_and_bootstrap( - ctx, chosen_order, bs=bs) + ctx, chosen_order, bs=bs, blacklist=blacklist + ) else: # check if each requirement has a recipe recipes = [] @@ -161,7 +332,7 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): try: recipe = Recipe.get_recipe(name, ctx) python_modules += recipe.python_depends - except IOError: + except ValueError: python_modules.append(name) else: recipes.append(name) diff --git a/pythonforandroid/logger.py b/pythonforandroid/logger.py index f293f0073c..d7914cee38 100644 --- a/pythonforandroid/logger.py +++ b/pythonforandroid/logger.py @@ -44,9 +44,9 @@ def format(self, record): logger = logging.getLogger('p4a') -if not hasattr(logger, 'touched'): # Necessary as importlib reloads - # this, which would add a second - # handler and reset the level +# Necessary as importlib reloads this, +# which would add a second handler and reset the level +if not hasattr(logger, 'touched'): logger.setLevel(logging.INFO) logger.touched = True ch = logging.StreamHandler(stderr) diff --git a/pythonforandroid/python.py b/pythonforandroid/python.py old mode 100644 new mode 100755 index 9ff532d89b..afddfe372b --- a/pythonforandroid/python.py +++ b/pythonforandroid/python.py @@ -4,8 +4,10 @@ ''' from os.path import dirname, exists, join +from multiprocessing import cpu_count from shutil import copy2 from os import environ +import subprocess import glob import sh @@ -13,7 +15,7 @@ from pythonforandroid.logger import logger, info, shprint from pythonforandroid.util import ( current_directory, ensure_dir, walk_valid_filens, - BuildInterruptingException) + BuildInterruptingException, build_platform) class GuestPythonRecipe(TargetPythonRecipe): @@ -71,7 +73,7 @@ class GuestPythonRecipe(TargetPythonRecipe): '''The directories that we want to omit for our python bundle''' stdlib_filen_blacklist = [ - '*.pyc', + '*.py', '*.exe', '*.whl', ] @@ -84,13 +86,23 @@ class GuestPythonRecipe(TargetPythonRecipe): '''The directories from site packages dir that we don't want to be included in our python bundle.''' - site_packages_filen_blacklist = [] + site_packages_filen_blacklist = [ + '*.py' + ] '''The file extensions from site packages dir that we don't want to be included in our python bundle.''' opt_depends = ['sqlite3', 'libffi', 'openssl'] '''The optional libraries which we would like to get our python linked''' + compiled_extension = '.pyc' + '''the default extension for compiled python files. + + .. note:: the default extension for compiled python files has been .pyo for + python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no + longer used and has been removed in favour of extension .pyc + ''' + def __init__(self, *args, **kwargs): self._ctx = None super(GuestPythonRecipe, self).__init__(*args, **kwargs) @@ -107,12 +119,12 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True): toolchain_prefix=self.ctx.toolchain_prefix, toolchain_version=self.ctx.toolchain_version) toolchain = join(self.ctx.ndk_dir, 'toolchains', - toolchain, 'prebuilt', 'linux-x86_64') + toolchain, 'prebuilt', build_platform) env['CC'] = ( '{clang} -target {target} -gcc-toolchain {toolchain}').format( clang=join(self.ctx.ndk_dir, 'toolchains', 'llvm', 'prebuilt', - 'linux-x86_64', 'bin', 'clang'), + build_platform, 'bin', 'clang'), target=arch.target, toolchain=toolchain) env['AR'] = join(toolchain, 'bin', android_host) + '-ar' @@ -178,9 +190,13 @@ def add_flags(include_flags, link_dirs, link_libs): if 'libffi' in self.ctx.recipe_build_order: info('Activating flags for libffi') recipe = Recipe.get_recipe('libffi', self.ctx) + # In order to force the correct linkage for our libffi library, we + # set the following variable to point where is our libffi.pc file, + # because the python build system uses pkg-config to configure it. + env['PKG_CONFIG_PATH'] = recipe.get_build_dir(arch.arch) add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)), - ' -L' + join(recipe.get_build_dir(arch.arch), - recipe.get_host(arch), '.libs'), ' -lffi') + ' -L' + join(recipe.get_build_dir(arch.arch), '.libs'), + ' -lffi') if 'openssl' in self.ctx.recipe_build_order: info('Activating flags for openssl') @@ -235,7 +251,7 @@ def build_arch(self, arch): py_version = self.major_minor_version_string if self.major_minor_version_string[0] == '3': py_version += 'm' - shprint(sh.make, 'all', + shprint(sh.make, 'all', '-j', str(cpu_count()), 'INSTSONAME=libpython{version}.so'.format( version=py_version), _env=env) @@ -249,26 +265,52 @@ def include_root(self, arch_name): def link_root(self, arch_name): return join(self.get_build_dir(arch_name), 'android-build') + def compile_python_files(self, dir): + ''' + Compile the python files (recursively) for the python files inside + a given folder. + + .. note:: python2 compiles the files into extension .pyo, but in + python3, and as of Python 3.5, the .pyo filename extension is no + longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488) + ''' + args = [self.ctx.hostpython] + if self.ctx.python_recipe.name == 'python3': + args += ['-OO', '-m', 'compileall', '-b', '-f', dir] + else: + args += ['-OO', '-m', 'compileall', '-f', dir] + subprocess.call(args) + def create_python_bundle(self, dirn, arch): """ Create a packaged python bundle in the target directory, by copying all the modules and standard library to the right place. """ - # Bundle compiled python modules to a folder - modules_dir = join(dirn, 'modules') - ensure_dir(modules_dir) # Todo: find a better way to find the build libs folder modules_build_dir = join( self.get_build_dir(arch.arch), 'android-build', 'build', - 'lib.linux{}-arm-{}'.format( + 'lib.linux{}-{}-{}'.format( '2' if self.version[0] == '2' else '', + arch.command_prefix.split('-')[0], self.major_minor_version_string )) + + # Compile to *.pyc/*.pyo the python modules + self.compile_python_files(modules_build_dir) + # Compile to *.pyc/*.pyo the standard python library + self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib')) + # Compile to *.pyc/*.pyo the other python packages (site-packages) + self.compile_python_files(self.ctx.get_python_install_dir()) + + # Bundle compiled python modules to a folder + modules_dir = join(dirn, 'modules') + c_ext = self.compiled_extension + ensure_dir(modules_dir) module_filens = (glob.glob(join(modules_build_dir, '*.so')) + - glob.glob(join(modules_build_dir, '*.py'))) + glob.glob(join(modules_build_dir, '*' + c_ext))) info("Copy {} files into the bundle".format(len(module_filens))) for filen in module_filens: info(" - copy {}".format(filen)) @@ -303,7 +345,7 @@ def create_python_bundle(self, dirn, arch): if self.major_minor_version_string[0] == '3': python_lib_name += 'm' shprint(sh.cp, join(python_build_dir, python_lib_name + '.so'), - 'libs/{}'.format(arch.arch)) + join(self.ctx.dist_dir, self.ctx.dist_name, 'libs', arch.arch)) info('Renaming .so files to reflect cross-compile') self.reduce_object_file_names(join(dirn, 'site-packages')) @@ -379,7 +421,7 @@ def build_arch(self, arch): shprint(sh.cp, join('Modules', 'Setup.dist'), join(build_dir, 'Modules', 'Setup')) - result = shprint(sh.make, '-C', build_dir) + shprint(sh.make, '-j', str(cpu_count()), '-C', build_dir) else: info('Skipping {name} ({version}) build, as it has already ' 'been completed'.format(name=self.name, version=self.version)) diff --git a/pythonforandroid/recipe.py b/pythonforandroid/recipe.py index 6b2c1a4f51..7b5ec9320d 100644 --- a/pythonforandroid/recipe.py +++ b/pythonforandroid/recipe.py @@ -12,6 +12,7 @@ import fnmatch from os import listdir, unlink, environ, mkdir, curdir, walk from sys import stdout +import time try: from urlparse import urlparse except ImportError: @@ -145,7 +146,19 @@ def report_hook(index, blksize, size): if exists(target): unlink(target) - urlretrieve(url, target, report_hook) + # Download item with multiple attempts (for bad connections): + attempts = 0 + while True: + try: + urlretrieve(url, target, report_hook) + except OSError as e: + attempts += 1 + if attempts >= 5: + raise e + stdout.write('Download failed retrying in a second...') + time.sleep(1) + continue + break return target elif parsed_url.scheme in ('git', 'git+file', 'git+ssh', 'git+http', 'git+https'): if isdir(target): @@ -166,14 +179,18 @@ def report_hook(index, blksize, size): shprint(sh.git, 'submodule', 'update', '--recursive') return target - def apply_patch(self, filename, arch): + def apply_patch(self, filename, arch, build_dir=None): """ Apply a patch from the current recipe directory into the current build directory. + + .. versionchanged:: 0.6.0 + Add ability to apply patch from any dir via kwarg `build_dir`''' """ info("Applying patch {}".format(filename)) + build_dir = build_dir if build_dir else self.get_build_dir(arch) filename = join(self.get_recipe_dir(), filename) - shprint(sh.patch, "-t", "-d", self.get_build_dir(arch), "-p1", + shprint(sh.patch, "-t", "-d", build_dir, "-p1", "-i", filename, _tail=10) def copy_file(self, filename, dest): @@ -224,6 +241,12 @@ def check_recipe_choices(self): recipes.append(recipe) return sorted(recipes) + def get_opt_depends_in_list(self, recipes): + '''Given a list of recipe names, returns those that are also in + self.opt_depends. + ''' + return [recipe for recipe in recipes if recipe in self.opt_depends] + def get_build_container_dir(self, arch): '''Given the arch name, returns the directory where it will be built. @@ -376,15 +399,10 @@ def unpack(self, arch): root_directory = fileh.filelist[0].filename.split('/')[0] if root_directory != basename(directory_name): shprint(sh.mv, root_directory, directory_name) - elif (extraction_filename.endswith('.tar.gz') or - extraction_filename.endswith('.tgz') or - extraction_filename.endswith('.tar.bz2') or - extraction_filename.endswith('.tbz2') or - extraction_filename.endswith('.tar.xz') or - extraction_filename.endswith('.txz')): + elif extraction_filename.endswith( + ('.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz')): sh.tar('xf', extraction_filename) - root_directory = shprint( - sh.tar, 'tf', extraction_filename).stdout.decode( + root_directory = sh.tar('tf', extraction_filename).stdout.decode( 'utf-8').split('\n')[0].split('/')[0] if root_directory != directory_name: shprint(sh.mv, root_directory, directory_name) @@ -428,8 +446,11 @@ def is_patched(self, arch): build_dir = self.get_build_dir(arch.arch) return exists(join(build_dir, '.patched')) - def apply_patches(self, arch): - '''Apply any patches for the Recipe.''' + def apply_patches(self, arch, build_dir=None): + '''Apply any patches for the Recipe. + + .. versionchanged:: 0.6.0 + Add ability to apply patches from any dir via kwarg `build_dir`''' if self.patches: info_main('Applying patches for {}[{}]' .format(self.name, arch.arch)) @@ -438,6 +459,7 @@ def apply_patches(self, arch): info_main('{} already patched, skipping'.format(self.name)) return + build_dir = build_dir if build_dir else self.get_build_dir(arch.arch) for patch in self.patches: if isinstance(patch, (tuple, list)): patch, patch_check = patch @@ -446,9 +468,9 @@ def apply_patches(self, arch): self.apply_patch( patch.format(version=self.version, arch=arch.arch), - arch.arch) + arch.arch, build_dir=build_dir) - shprint(sh.touch, join(self.get_build_dir(arch.arch), '.patched')) + shprint(sh.touch, join(build_dir, '.patched')) def should_build(self, arch): '''Should perform any necessary test and return True only if it needs @@ -552,6 +574,7 @@ def list_recipes(cls, ctx): @classmethod def get_recipe(cls, name, ctx): '''Returns the Recipe with the given name, if it exists.''' + name = name.lower() if not hasattr(cls, "recipes"): cls.recipes = {} if name in cls.recipes: @@ -559,20 +582,28 @@ def get_recipe(cls, name, ctx): recipe_file = None for recipes_dir in cls.recipe_dirs(ctx): - recipe_file = join(recipes_dir, name, '__init__.py') - if exists(recipe_file): + if not exists(recipes_dir): + continue + # Find matching folder (may differ in case): + for subfolder in listdir(recipes_dir): + if subfolder.lower() == name: + recipe_file = join(recipes_dir, subfolder, '__init__.py') + if exists(recipe_file): + name = subfolder # adapt to actual spelling + break + recipe_file = None + if recipe_file is not None: break - recipe_file = None if not recipe_file: - raise IOError('Recipe does not exist: {}'.format(name)) + raise ValueError('Recipe does not exist: {}'.format(name)) mod = import_recipe('pythonforandroid.recipes.{}'.format(name), recipe_file) if len(logger.handlers) > 1: logger.removeHandler(logger.handlers[1]) recipe = mod.recipe recipe.ctx = ctx - cls.recipes[name] = recipe + cls.recipes[name.lower()] = recipe return recipe @@ -599,9 +630,7 @@ class BootstrapNDKRecipe(Recipe): :class:`~pythonforandroid.recipe.NDKRecipe`. To link with python, call the method :meth:`get_recipe_env` - with the kwarg *with_python=True*. If recipe contains android's mk files - which should be linked with python, you may want to use the env variables - MK_PYTHON_INCLUDE_ROOT and MK_PYTHON_LINK_ROOT set in there. + with the kwarg *with_python=True*. ''' dir_name = None # The name of the recipe build folder in the jni dir @@ -630,16 +659,6 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=False): self.ctx.python_recipe.major_minor_version_string) if 'python3' in self.ctx.python_recipe.name: env['EXTRA_LDLIBS'] += 'm' - - # set some env variables that may be needed to build some bootstrap ndk - # recipes that needs linking with our python via mk files, like - # recipes: sdl2, genericndkbuild or sdl - other_builds = join(self.ctx.build_dir, 'other_builds') + '/' - env['MK_PYTHON_INCLUDE_ROOT'] = \ - self.ctx.python_recipe.include_root(arch.arch)[ - len(other_builds):] - env['MK_PYTHON_LINK_ROOT'] = \ - self.ctx.python_recipe.link_root(arch.arch)[len(other_builds):] return env @@ -754,6 +773,10 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True): env['PYTHONNOUSERSITE'] = '1' + # Set the LANG, this isn't usually important but is a better default + # as it occasionally matters how Python e.g. reads files + env['LANG'] = "en_GB.UTF-8" + if not self.call_hostpython_via_targetpython: # sets python headers/linkages...depending on python's recipe python_name = self.ctx.python_recipe.name diff --git a/pythonforandroid/recipes/android/__init__.py b/pythonforandroid/recipes/android/__init__.py index a8f6d2dd0a..6b7374ca9d 100644 --- a/pythonforandroid/recipes/android/__init__.py +++ b/pythonforandroid/recipes/android/__init__.py @@ -14,7 +14,8 @@ class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe): src_filename = 'src' - depends = [('pygame', 'sdl2', 'genericndkbuild')] + depends = [('pygame', 'sdl2', 'genericndkbuild'), + 'pyjnius'] config_env = {} diff --git a/pythonforandroid/recipes/android/src/android/loadingscreen.py b/pythonforandroid/recipes/android/src/android/loadingscreen.py new file mode 100644 index 0000000000..1dc1b670f5 --- /dev/null +++ b/pythonforandroid/recipes/android/src/android/loadingscreen.py @@ -0,0 +1,7 @@ + +from jnius import autoclass + + +def hide_loading_screen(): + python_activity = autoclass('org.kivy.android.PythonActivity') + python_activity.removeLoadingScreen() diff --git a/pythonforandroid/recipes/android/src/android/permissions.py b/pythonforandroid/recipes/android/src/android/permissions.py new file mode 100644 index 0000000000..2b3f516ccf --- /dev/null +++ b/pythonforandroid/recipes/android/src/android/permissions.py @@ -0,0 +1,434 @@ + +try: + from jnius import autoclass +except ImportError: + # To allow importing by build/manifest-creating code without + # pyjnius being present: + def autoclass(item): + raise RuntimeError("pyjnius not available") + + +class Permission: + ACCEPT_HANDOVER = "android.permission.ACCEPT_HANDOVER" + ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION" + ACCESS_LOCATION_EXTRA_COMMANDS = ( + "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" + ) + ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE" + ACCESS_NOTIFICATION_POLICY = ( + "android.permission.ACCESS_NOTIFICATION_POLICY" + ) + ACCESS_WIFI_STATE = "android.permission.ACCESS_WIFI_STATE" + ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL" + ANSWER_PHONE_CALLS = "android.permission.ANSWER_PHONE_CALLS" + BATTERY_STATS = "android.permission.BATTERY_STATS" + BIND_ACCESSIBILITY_SERVICE = ( + "android.permission.BIND_ACCESSIBILITY_SERVICE" + ) + BIND_AUTOFILL_SERVICE = "android.permission.BIND_AUTOFILL_SERVICE" + BIND_CARRIER_MESSAGING_SERVICE = ( # note: deprecated in api 23+ + "android.permission.BIND_CARRIER_MESSAGING_SERVICE" + ) + BIND_CARRIER_SERVICES = ( # replaces BIND_CARRIER_MESSAGING_SERVICE + "android.permission.BIND_CARRIER_SERVICES" + ) + BIND_CHOOSER_TARGET_SERVICE = ( + "android.permission.BIND_CHOOSER_TARGET_SERVICE" + ) + BIND_CONDITION_PROVIDER_SERVICE = ( + "android.permission.BIND_CONDITION_PROVIDER_SERVICE" + ) + BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN" + BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE" + BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE" + BIND_INPUT_METHOD = ( + "android.permission.BIND_INPUT_METHOD" + ) + BIND_MIDI_DEVICE_SERVICE = ( + "android.permission.BIND_MIDI_DEVICE_SERVICE" + ) + BIND_NFC_SERVICE = ( + "android.permission.BIND_NFC_SERVICE" + ) + BIND_NOTIFICATION_LISTENER_SERVICE = ( + "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" + ) + BIND_PRINT_SERVICE = ( + "android.permission.BIND_PRINT_SERVICE" + ) + BIND_QUICK_SETTINGS_TILE = ( + "android.permission.BIND_QUICK_SETTINGS_TILE" + ) + BIND_REMOTEVIEWS = ( + "android.permission.BIND_REMOTEVIEWS" + ) + BIND_SCREENING_SERVICE = ( + "android.permission.BIND_SCREENING_SERVICE" + ) + BIND_TELECOM_CONNECTION_SERVICE = ( + "android.permission.BIND_TELECOM_CONNECTION_SERVICE" + ) + BIND_TEXT_SERVICE = ( + "android.permission.BIND_TEXT_SERVICE" + ) + BIND_TV_INPUT = ( + "android.permission.BIND_TV_INPUT" + ) + BIND_VISUAL_VOICEMAIL_SERVICE = ( + "android.permission.BIND_VISUAL_VOICEMAIL_SERVICE" + ) + BIND_VOICE_INTERACTION = ( + "android.permission.BIND_VOICE_INTERACTION" + ) + BIND_VPN_SERVICE = ( + "android.permission.BIND_VPN_SERVICE" + ) + BIND_VR_LISTENER_SERVICE = ( + "android.permission.BIND_VR_LISTENER_SERVICE" + ) + BIND_WALLPAPER = ( + "android.permission.BIND_WALLPAPER" + ) + BLUETOOTH = ( + "android.permission.BLUETOOTH" + ) + BLUETOOTH_ADMIN = ( + "android.permission.BLUETOOTH_ADMIN" + ) + BODY_SENSORS = ( + "android.permission.BODY_SENSORS" + ) + BROADCAST_PACKAGE_REMOVED = ( + "android.permission.BROADCAST_PACKAGE_REMOVED" + ) + BROADCAST_STICKY = ( + "android.permission.BROADCAST_STICKY" + ) + CALL_PHONE = ( + "android.permission.CALL_PHONE" + ) + CALL_PRIVILEGED = ( + "android.permission.CALL_PRIVILEGED" + ) + CAMERA = ( + "android.permission.CAMERA" + ) + CAPTURE_AUDIO_OUTPUT = ( + "android.permission.CAPTURE_AUDIO_OUTPUT" + ) + CAPTURE_SECURE_VIDEO_OUTPUT = ( + "android.permission.CAPTURE_SECURE_VIDEO_OUTPUT" + ) + CAPTURE_VIDEO_OUTPUT = ( + "android.permission.CAPTURE_VIDEO_OUTPUT" + ) + CHANGE_COMPONENT_ENABLED_STATE = ( + "android.permission.CHANGE_COMPONENT_ENABLED_STATE" + ) + CHANGE_CONFIGURATION = ( + "android.permission.CHANGE_CONFIGURATION" + ) + CHANGE_NETWORK_STATE = ( + "android.permission.CHANGE_NETWORK_STATE" + ) + CHANGE_WIFI_MULTICAST_STATE = ( + "android.permission.CHANGE_WIFI_MULTICAST_STATE" + ) + CHANGE_WIFI_STATE = ( + "android.permission.CHANGE_WIFI_STATE" + ) + CLEAR_APP_CACHE = ( + "android.permission.CLEAR_APP_CACHE" + ) + CONTROL_LOCATION_UPDATES = ( + "android.permission.CONTROL_LOCATION_UPDATES" + ) + DELETE_CACHE_FILES = ( + "android.permission.DELETE_CACHE_FILES" + ) + DELETE_PACKAGES = ( + "android.permission.DELETE_PACKAGES" + ) + DIAGNOSTIC = ( + "android.permission.DIAGNOSTIC" + ) + DISABLE_KEYGUARD = ( + "android.permission.DISABLE_KEYGUARD" + ) + DUMP = ( + "android.permission.DUMP" + ) + EXPAND_STATUS_BAR = ( + "android.permission.EXPAND_STATUS_BAR" + ) + FACTORY_TEST = ( + "android.permission.FACTORY_TEST" + ) + FOREGROUND_SERVICE = ( + "android.permission.FOREGROUND_SERVICE" + ) + GET_ACCOUNTS = ( + "android.permission.GET_ACCOUNTS" + ) + GET_ACCOUNTS_PRIVILEGED = ( + "android.permission.GET_ACCOUNTS_PRIVILEGED" + ) + GET_PACKAGE_SIZE = ( + "android.permission.GET_PACKAGE_SIZE" + ) + GET_TASKS = ( + "android.permission.GET_TASKS" + ) + GLOBAL_SEARCH = ( + "android.permission.GLOBAL_SEARCH" + ) + INSTALL_LOCATION_PROVIDER = ( + "android.permission.INSTALL_LOCATION_PROVIDER" + ) + INSTALL_PACKAGES = ( + "android.permission.INSTALL_PACKAGES" + ) + INSTALL_SHORTCUT = ( + "com.android.launcher.permission.INSTALL_SHORTCUT" + ) + INSTANT_APP_FOREGROUND_SERVICE = ( + "android.permission.INSTANT_APP_FOREGROUND_SERVICE" + ) + INTERNET = ( + "android.permission.INTERNET" + ) + KILL_BACKGROUND_PROCESSES = ( + "android.permission.KILL_BACKGROUND_PROCESSES" + ) + LOCATION_HARDWARE = ( + "android.permission.LOCATION_HARDWARE" + ) + MANAGE_DOCUMENTS = ( + "android.permission.MANAGE_DOCUMENTS" + ) + MANAGE_OWN_CALLS = ( + "android.permission.MANAGE_OWN_CALLS" + ) + MASTER_CLEAR = ( + "android.permission.MASTER_CLEAR" + ) + MEDIA_CONTENT_CONTROL = ( + "android.permission.MEDIA_CONTENT_CONTROL" + ) + MODIFY_AUDIO_SETTINGS = ( + "android.permission.MODIFY_AUDIO_SETTINGS" + ) + MODIFY_PHONE_STATE = ( + "android.permission.MODIFY_PHONE_STATE" + ) + MOUNT_FORMAT_FILESYSTEMS = ( + "android.permission.MOUNT_FORMAT_FILESYSTEMS" + ) + MOUNT_UNMOUNT_FILESYSTEMS = ( + "android.permission.MOUNT_UNMOUNT_FILESYSTEMS" + ) + NFC = ( + "android.permission.NFC" + ) + NFC_TRANSACTION_EVENT = ( + "android.permission.NFC_TRANSACTION_EVENT" + ) + PACKAGE_USAGE_STATS = ( + "android.permission.PACKAGE_USAGE_STATS" + ) + PERSISTENT_ACTIVITY = ( + "android.permission.PERSISTENT_ACTIVITY" + ) + PROCESS_OUTGOING_CALLS = ( + "android.permission.PROCESS_OUTGOING_CALLS" + ) + READ_CALENDAR = ( + "android.permission.READ_CALENDAR" + ) + READ_CALL_LOG = ( + "android.permission.READ_CALL_LOG" + ) + READ_CONTACTS = ( + "android.permission.READ_CONTACTS" + ) + READ_EXTERNAL_STORAGE = ( + "android.permission.READ_EXTERNAL_STORAGE" + ) + READ_FRAME_BUFFER = ( + "android.permission.READ_FRAME_BUFFER" + ) + READ_INPUT_STATE = ( + "android.permission.READ_INPUT_STATE" + ) + READ_LOGS = ( + "android.permission.READ_LOGS" + ) + READ_PHONE_NUMBERS = ( + "android.permission.READ_PHONE_NUMBERS" + ) + READ_PHONE_STATE = ( + "android.permission.READ_PHONE_STATE" + ) + READ_SMS = ( + "android.permission.READ_SMS" + ) + READ_SYNC_SETTINGS = ( + "android.permission.READ_SYNC_SETTINGS" + ) + READ_SYNC_STATS = ( + "android.permission.READ_SYNC_STATS" + ) + READ_VOICEMAIL = ( + "com.android.voicemail.permission.READ_VOICEMAIL" + ) + REBOOT = ( + "android.permission.REBOOT" + ) + RECEIVE_BOOT_COMPLETED = ( + "android.permission.RECEIVE_BOOT_COMPLETED" + ) + RECEIVE_MMS = ( + "android.permission.RECEIVE_MMS" + ) + RECEIVE_SMS = ( + "android.permission.RECEIVE_SMS" + ) + RECEIVE_WAP_PUSH = ( + "android.permission.RECEIVE_WAP_PUSH" + ) + RECORD_AUDIO = ( + "android.permission.RECORD_AUDIO" + ) + REORDER_TASKS = ( + "android.permission.REORDER_TASKS" + ) + REQUEST_COMPANION_RUN_IN_BACKGROUND = ( + "android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" + ) + REQUEST_COMPANION_USE_DATA_IN_BACKGROUND = ( + "android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" + ) + REQUEST_DELETE_PACKAGES = ( + "android.permission.REQUEST_DELETE_PACKAGES" + ) + REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = ( + "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" + ) + REQUEST_INSTALL_PACKAGES = ( + "android.permission.REQUEST_INSTALL_PACKAGES" + ) + RESTART_PACKAGES = ( + "android.permission.RESTART_PACKAGES" + ) + SEND_RESPOND_VIA_MESSAGE = ( + "android.permission.SEND_RESPOND_VIA_MESSAGE" + ) + SEND_SMS = ( + "android.permission.SEND_SMS" + ) + SET_ALARM = ( + "com.android.alarm.permission.SET_ALARM" + ) + SET_ALWAYS_FINISH = ( + "android.permission.SET_ALWAYS_FINISH" + ) + SET_ANIMATION_SCALE = ( + "android.permission.SET_ANIMATION_SCALE" + ) + SET_DEBUG_APP = ( + "android.permission.SET_DEBUG_APP" + ) + SET_PREFERRED_APPLICATIONS = ( + "android.permission.SET_PREFERRED_APPLICATIONS" + ) + SET_PROCESS_LIMIT = ( + "android.permission.SET_PROCESS_LIMIT" + ) + SET_TIME = ( + "android.permission.SET_TIME" + ) + SET_TIME_ZONE = ( + "android.permission.SET_TIME_ZONE" + ) + SET_WALLPAPER = ( + "android.permission.SET_WALLPAPER" + ) + SET_WALLPAPER_HINTS = ( + "android.permission.SET_WALLPAPER_HINTS" + ) + SIGNAL_PERSISTENT_PROCESSES = ( + "android.permission.SIGNAL_PERSISTENT_PROCESSES" + ) + STATUS_BAR = ( + "android.permission.STATUS_BAR" + ) + SYSTEM_ALERT_WINDOW = ( + "android.permission.SYSTEM_ALERT_WINDOW" + ) + TRANSMIT_IR = ( + "android.permission.TRANSMIT_IR" + ) + UNINSTALL_SHORTCUT = ( + "com.android.launcher.permission.UNINSTALL_SHORTCUT" + ) + UPDATE_DEVICE_STATS = ( + "android.permission.UPDATE_DEVICE_STATS" + ) + USE_BIOMETRIC = ( + "android.permission.USE_BIOMETRIC" + ) + USE_FINGERPRINT = ( + "android.permission.USE_FINGERPRINT" + ) + USE_SIP = ( + "android.permission.USE_SIP" + ) + VIBRATE = ( + "android.permission.VIBRATE" + ) + WAKE_LOCK = ( + "android.permission.WAKE_LOCK" + ) + WRITE_APN_SETTINGS = ( + "android.permission.WRITE_APN_SETTINGS" + ) + WRITE_CALENDAR = ( + "android.permission.WRITE_CALENDAR" + ) + WRITE_CALL_LOG = ( + "android.permission.WRITE_CALL_LOG" + ) + WRITE_CONTACTS = ( + "android.permission.WRITE_CONTACTS" + ) + WRITE_EXTERNAL_STORAGE = ( + "android.permission.WRITE_EXTERNAL_STORAGE" + ) + WRITE_GSERVICES = ( + "android.permission.WRITE_GSERVICES" + ) + WRITE_SECURE_SETTINGS = ( + "android.permission.WRITE_SECURE_SETTINGS" + ) + WRITE_SETTINGS = ( + "android.permission.WRITE_SETTINGS" + ) + WRITE_SYNC_SETTINGS = ( + "android.permission.WRITE_SYNC_SETTINGS" + ) + WRITE_VOICEMAIL = ( + "com.android.voicemail.permission.WRITE_VOICEMAIL" + ) + + +def request_permission(permission): + python_activity = autoclass('org.kivy.android.PythonActivity') + python_activity.requestNewPermission(permission + "") + + +def check_permission(permission): + python_activity = autoclass('org.kivy.android.PythonActivity') + result = bool(python_activity.checkCurrentPermission( + permission + "" + )) + return result diff --git a/pythonforandroid/recipes/android/src/android/runnable.py b/pythonforandroid/recipes/android/src/android/runnable.py index 12c86a1ba6..8d2d1161d1 100644 --- a/pythonforandroid/recipes/android/src/android/runnable.py +++ b/pythonforandroid/recipes/android/src/android/runnable.py @@ -33,7 +33,7 @@ def __call__(self, *args, **kwargs): def run(self): try: self.func(*self.args, **self.kwargs) - except: + except: # noqa E722 import traceback traceback.print_exc() diff --git a/pythonforandroid/recipes/apsw/__init__.py b/pythonforandroid/recipes/apsw/__init__.py index 68e745961e..6098e4b98c 100644 --- a/pythonforandroid/recipes/apsw/__init__.py +++ b/pythonforandroid/recipes/apsw/__init__.py @@ -6,7 +6,7 @@ class ApswRecipe(PythonRecipe): version = '3.15.0-r1' url = 'https://github.com/rogerbinns/apsw/archive/{version}.tar.gz' - depends = ['sqlite3', 'hostpython2', 'python2', 'setuptools'] + depends = ['sqlite3', ('python2', 'python3'), 'setuptools'] call_hostpython_via_targetpython = False site_packages_name = 'apsw' @@ -24,14 +24,10 @@ def build_arch(self, arch): def get_recipe_env(self, arch): env = super(ApswRecipe, self).get_recipe_env(arch) - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() - env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' + \ - ' -I' + self.get_recipe('sqlite3', self.ctx).get_build_dir(arch.arch) - # Set linker to use the correct gcc - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' - env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + \ - ' -lpython2.7' + \ - ' -lsqlite3' + sqlite_recipe = self.get_recipe('sqlite3', self.ctx) + env['CFLAGS'] += ' -I' + sqlite_recipe.get_build_dir(arch.arch) + env['LDFLAGS'] += ' -L' + sqlite_recipe.get_lib_dir(arch) + env['LIBS'] = env.get('LIBS', '') + ' -lsqlite3' return env diff --git a/pythonforandroid/recipes/atom/__init__.py b/pythonforandroid/recipes/atom/__init__.py index 854911372e..51923d5487 100644 --- a/pythonforandroid/recipes/atom/__init__.py +++ b/pythonforandroid/recipes/atom/__init__.py @@ -5,7 +5,7 @@ class AtomRecipe(CppCompiledComponentsPythonRecipe): site_packages_name = 'atom' version = '0.3.10' url = 'https://github.com/nucleic/atom/archive/master.zip' - depends = ['python2', 'setuptools'] + depends = ['setuptools'] recipe = AtomRecipe() diff --git a/pythonforandroid/recipes/babel/__init__.py b/pythonforandroid/recipes/babel/__init__.py index dc1fad86f1..fc17f8e4b0 100644 --- a/pythonforandroid/recipes/babel/__init__.py +++ b/pythonforandroid/recipes/babel/__init__.py @@ -6,7 +6,7 @@ class BabelRecipe(PythonRecipe): version = '2.2.0' url = 'https://pypi.python.org/packages/source/B/Babel/Babel-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools', 'pytz'] + depends = ['setuptools', 'pytz'] call_hostpython_via_targetpython = False install_in_hostpython = True diff --git a/pythonforandroid/recipes/boost/__init__.py b/pythonforandroid/recipes/boost/__init__.py index 89109c3f7d..53d9388877 100644 --- a/pythonforandroid/recipes/boost/__init__.py +++ b/pythonforandroid/recipes/boost/__init__.py @@ -1,5 +1,6 @@ from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory from os.path import join, exists +from os import environ import sh """ @@ -9,11 +10,36 @@ class BoostRecipe(Recipe): - version = '1.60.0' - # Don't forget to change the URL when changing the version - url = 'http://downloads.sourceforge.net/project/boost/boost/{version}/boost_1_60_0.tar.bz2' - depends = ['python2'] - patches = ['disable-so-version.patch', 'use-android-libs.patch'] + # Todo: make recipe compatible with all p4a architectures + ''' + .. note:: This recipe can be built only against API 21+ and arch armeabi-v7a + + .. versionchanged:: 0.6.0 + Rewrote recipe to support clang's build. The following changes has + been made: + + - Bumped version number to 1.68.0 + - Better version handling for url + - Added python 3 compatibility + - Default compiler for ndk's toolchain set to clang + - Python version will be detected via user-config.jam + - Changed stl's lib from ``gnustl_shared`` to ``c++_shared`` + ''' + version = '1.68.0' + url = 'http://downloads.sourceforge.net/project/boost/' \ + 'boost/{version}/boost_{version_underscore}.tar.bz2' + depends = [('python2', 'python3')] + patches = ['disable-so-version.patch', + 'use-android-libs.patch', + 'fix-android-issues.patch'] + + @property + def versioned_url(self): + if self.url is None: + return None + return self.url.format( + version=self.version, + version_underscore=self.version.replace('.', '_')) def should_build(self, arch): return not exists(join(self.get_build_dir(arch.arch), 'b2')) @@ -28,9 +54,11 @@ def prebuild_arch(self, arch): shprint(bash, join(self.ctx.ndk_dir, 'build/tools/make-standalone-toolchain.sh'), '--arch=' + env['ARCH'], '--platform=android-' + str(self.ctx.android_api), - '--toolchain=' + env['CROSSHOST'] + '-' + env['TOOLCHAIN_VERSION'], + '--toolchain=' + env['CROSSHOST'] + '-' + self.ctx.toolchain_version + ':-llvm', + '--use-llvm', + '--stl=libc++', '--install-dir=' + env['CROSSHOME'] - ) + ) # Set custom configuration shutil.copyfile(join(self.get_recipe_dir(), 'user-config.jam'), join(env['BOOST_BUILD_PATH'], 'user-config.jam')) @@ -38,31 +66,38 @@ def prebuild_arch(self, arch): def build_arch(self, arch): super(BoostRecipe, self).build_arch(arch) env = self.get_recipe_env(arch) + env['PYTHON_HOST'] = self.ctx.hostpython with current_directory(self.get_build_dir(arch.arch)): # Compile Boost.Build engine with this custom toolchain bash = sh.Command('bash') - shprint(bash, 'bootstrap.sh', - '--with-python=' + join(env['PYTHON_ROOT'], 'bin/python.host'), - '--with-python-version=2.7', - '--with-python-root=' + env['PYTHON_ROOT'] - ) # Do not pass env + shprint(bash, 'bootstrap.sh') # Do not pass env # Install app stl - shutil.copyfile(join(env['CROSSHOME'], env['CROSSHOST'], 'lib/libgnustl_shared.so'), - join(self.ctx.get_libs_dir(arch.arch), 'libgnustl_shared.so')) + shutil.copyfile( + join(self.ctx.ndk_dir, 'sources/cxx-stl/llvm-libc++/libs/' + 'armeabi-v7a/libc++_shared.so'), + join(self.ctx.get_libs_dir(arch.arch), 'libc++_shared.so')) def select_build_arch(self, arch): return arch.arch.replace('eabi-v7a', '').replace('eabi', '') def get_recipe_env(self, arch): - env = super(BoostRecipe, self).get_recipe_env(arch) + # We don't use the normal env because we + # are building with a standalone toolchain + env = environ.copy() + env['BOOST_BUILD_PATH'] = self.get_build_dir(arch.arch) # find user-config.jam env['BOOST_ROOT'] = env['BOOST_BUILD_PATH'] # find boost source - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() + + env['PYTHON_ROOT'] = self.ctx.python_recipe.link_root(arch.arch) + env['PYTHON_INCLUDE'] = self.ctx.python_recipe.include_root(arch.arch) + env['PYTHON_MAJOR_MINOR'] = self.ctx.python_recipe.version[:3] + env['PYTHON_LINK_VERSION'] = self.ctx.python_recipe.major_minor_version_string + if 'python3' in self.ctx.python_recipe.name: + env['PYTHON_LINK_VERSION'] += 'm' + env['ARCH'] = self.select_build_arch(arch) - env['ANDROIDAPI'] = str(self.ctx.android_api) env['CROSSHOST'] = env['ARCH'] + '-linux-androideabi' env['CROSSHOME'] = join(env['BOOST_ROOT'], 'standalone-' + env['ARCH'] + '-toolchain') - env['TOOLCHAIN_PREFIX'] = join(env['CROSSHOME'], 'bin', env['CROSSHOST']) return env diff --git a/pythonforandroid/recipes/boost/fix-android-issues.patch b/pythonforandroid/recipes/boost/fix-android-issues.patch new file mode 100644 index 0000000000..54134800a1 --- /dev/null +++ b/pythonforandroid/recipes/boost/fix-android-issues.patch @@ -0,0 +1,68 @@ +diff -u -r boost_1_68_0.orig/boost/config/user.hpp boost_1_68_0/boost/config/user.hpp +--- boost_1_68_0.orig/boost/config/user.hpp 2018-08-01 22:50:46.000000000 +0200 ++++ boost_1_68_0/boost/config/user.hpp 2018-08-27 15:43:38.000000000 +0200 +@@ -13,6 +13,12 @@ + // configuration policy: + // + ++// Android defines ++// There is problem with std::atomic on android (and some other platforms). ++// See this link for more info: ++// https://code.google.com/p/android/issues/detail?id=42735#makechanges ++#define BOOST_ASIO_DISABLE_STD_ATOMIC 1 ++ + // define this to locate a compiler config file: + // #define BOOST_COMPILER_CONFIG + +diff -u -r boost_1_68_0.orig/boost/asio/detail/config.hpp boost_1_68_0/boost/asio/detail/config.hpp +--- boost_1_68_0.orig/boost/asio/detail/config.hpp 2018-08-01 22:50:46.000000000 +0200 ++++ boost_1_68_0/boost/asio/detail/config.hpp 2018-09-19 12:39:56.000000000 +0200 +@@ -804,7 +804,11 @@ + # if defined(__clang__) + # if (__cplusplus >= 201402) + # if __has_include() +-# define BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW 1 ++# if __clang_major__ >= 7 ++# undef BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW ++# else ++# define BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW 1 ++# endif // __clang_major__ >= 7 + # endif // __has_include() + # endif // (__cplusplus >= 201402) + # endif // defined(__clang__) +diff -u -r boost_1_68_0.orig/boost/system/error_code.hpp boost_1_68_0/boost/system/error_code.hpp +--- boost_1_68_0.orig/boost/system/error_code.hpp 2018-08-01 22:50:53.000000000 +0200 ++++ boost_1_68_0/boost/system/error_code.hpp 2018-08-27 15:44:29.000000000 +0200 +@@ -17,6 +17,7 @@ + #include + #include + #include ++#include + #include + #include + #include +diff -u -r boost_1_68_0.orig/libs/filesystem/src/operations.cpp boost_1_68_0/libs/filesystem/src/operations.cpp +--- boost_1_68_0.orig/libs/filesystem/src/operations.cpp 2018-08-01 22:50:47.000000000 +0200 ++++ boost_1_68_0/libs/filesystem/src/operations.cpp 2018-08-27 15:47:15.000000000 +0200 +@@ -232,6 +232,21 @@ + + # if defined(BOOST_POSIX_API) + ++# if defined(__ANDROID__) ++# define truncate libboost_truncate_wrapper ++// truncate() is present in Android libc only starting from ABI 21, so here's a simple wrapper ++static int libboost_truncate_wrapper(const char *path, off_t length) ++{ ++ int fd = open(path, O_WRONLY); ++ if (fd == -1) { ++ return -1; ++ } ++ int status = ftruncate(fd, length); ++ close(fd); ++ return status; ++} ++# endif ++ + typedef int err_t; + + // POSIX uses a 0 return to indicate success diff --git a/pythonforandroid/recipes/boost/user-config.jam b/pythonforandroid/recipes/boost/user-config.jam index 72643d8a06..e50b50afea 100644 --- a/pythonforandroid/recipes/boost/user-config.jam +++ b/pythonforandroid/recipes/boost/user-config.jam @@ -1,28 +1,61 @@ import os ; -local ANDROIDNDK = [ os.environ ANDROIDNDK ] ; -local ANDROIDAPI = [ os.environ ANDROIDAPI ] ; -local TOOLCHAIN_VERSION = [ os.environ TOOLCHAIN_VERSION ] ; -local TOOLCHAIN_PREFIX = [ os.environ TOOLCHAIN_PREFIX ] ; local ARCH = [ os.environ ARCH ] ; +local CROSSHOME = [ os.environ CROSSHOME ] ; +local PYTHON_HOST = [ os.environ PYTHON_HOST ] ; local PYTHON_ROOT = [ os.environ PYTHON_ROOT ] ; +local PYTHON_INCLUDE = [ os.environ PYTHON_INCLUDE ] ; +local PYTHON_LINK_VERSION = [ os.environ PYTHON_LINK_VERSION ] ; +local PYTHON_MAJOR_MINOR = [ os.environ PYTHON_MAJOR_MINOR ] ; -using gcc : $(ARCH) : $(TOOLCHAIN_PREFIX)-g++ : +using clang : $(ARCH) : $(CROSSHOME)/bin/arm-linux-androideabi-clang++ : +$(CROSSHOME)/bin/arm-linux-androideabi-ar +$(CROSSHOME)/sysroot $(ARCH) -$(TOOLCHAIN_PREFIX)-ar --DBOOST_SP_USE_PTHREADS --DBOOST_AC_USE_PTHREADS --DBOOST_SP_USE_PTHREADS --DBOOST_AC_USE_PTHREADS --frtti --fexceptions --I$(ANDROIDNDK)/platforms/android-$(ANDROIDAPI)/arch-$(ARCH)/usr/include --I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/include --I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH)/include --I$(PYTHON_ROOT)/include/python2.7 ---sysroot=$(ANDROIDNDK)/platforms/android-$(ANDROIDAPI)/arch-$(ARCH) --L$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH) --L$(PYTHON_ROOT)/lib --lgnustl_shared --lpython2.7 +-fexceptions +-frtti +-fpic +-ffunction-sections +-funwind-tables +-march=armv7-a +-msoft-float +-mfpu=neon +-mthumb +-march=armv7-a +-Wl,--fix-cortex-a8 +-Os +-fomit-frame-pointer +-fno-strict-aliasing +-DANDROID +-D__ANDROID__ +-DANDROID_TOOLCHAIN=clang +-DANDROID_ABI=armv7-a +-DANDROID_STL=c++_shared +-DBOOST_ALL_NO_LIB +#-DNDEBUG +-O2 +-g +-fvisibility=hidden +-fvisibility-inlines-hidden +-fdata-sections +-D__arm__ +-D_REENTRANT +-D_GLIBCXX__PTHREADS +-Wno-long-long +-Wno-missing-field-initializers +-Wno-unused-variable +-Wl,-z,relro +-Wl,-z,now +-lc++_shared +-L$(PYTHON_ROOT) +-lpython$(PYTHON_LINK_VERSION) +-Wl,-O1 +-Wl,-Bsymbolic-functions ; + +using python : $(PYTHON_MAJOR_MINOR) + : $(PYTHON_host) + : $(PYTHON_ROOT) $(PYTHON_INCLUDE) + : $(PYTHON_ROOT)/libpython$(PYTHON_LINK_VERSION).so + : #BOOST_ALL_DYN_LINK +; \ No newline at end of file diff --git a/pythonforandroid/recipes/cdecimal/__init__.py b/pythonforandroid/recipes/cdecimal/__init__.py index 6c4740bd3c..94929c7807 100644 --- a/pythonforandroid/recipes/cdecimal/__init__.py +++ b/pythonforandroid/recipes/cdecimal/__init__.py @@ -7,7 +7,7 @@ class CdecimalRecipe(CompiledComponentsPythonRecipe): version = '2.3' url = 'http://www.bytereef.org/software/mpdecimal/releases/cdecimal-{version}.tar.gz' - depends = ['python2'] + depends = [] patches = ['locale.patch', 'cross-compile.patch'] diff --git a/pythonforandroid/recipes/coverage/__init__.py b/pythonforandroid/recipes/coverage/__init__.py index d88ac6a652..95f08f1f4a 100644 --- a/pythonforandroid/recipes/coverage/__init__.py +++ b/pythonforandroid/recipes/coverage/__init__.py @@ -7,7 +7,7 @@ class CoverageRecipe(PythonRecipe): url = 'https://pypi.python.org/packages/2d/10/6136c8e10644c16906edf4d9f7c782c0f2e7ed47ff2f41f067384e432088/coverage-{version}.tar.gz' - depends = ['hostpython2', 'setuptools'] + depends = [('hostpython2', 'hostpython3'), 'setuptools'] patches = ['fallback-utf8.patch'] diff --git a/pythonforandroid/recipes/cryptography/__init__.py b/pythonforandroid/recipes/cryptography/__init__.py index 868ca74d14..c5b3e9eb49 100644 --- a/pythonforandroid/recipes/cryptography/__init__.py +++ b/pythonforandroid/recipes/cryptography/__init__.py @@ -3,7 +3,7 @@ class CryptographyRecipe(CompiledComponentsPythonRecipe): name = 'cryptography' - version = '2.4.2' + version = '2.5' url = 'https://github.com/pyca/cryptography/archive/{version}.tar.gz' depends = ['openssl', 'idna', 'asn1crypto', 'six', 'setuptools', 'enum34', 'ipaddress', 'cffi'] diff --git a/pythonforandroid/recipes/cymunk/__init__.py b/pythonforandroid/recipes/cymunk/__init__.py index d070a44dad..96d4169710 100644 --- a/pythonforandroid/recipes/cymunk/__init__.py +++ b/pythonforandroid/recipes/cymunk/__init__.py @@ -6,7 +6,7 @@ class CymunkRecipe(CythonRecipe): url = 'https://github.com/tito/cymunk/archive/{version}.zip' name = 'cymunk' - depends = [('python2', 'python3crystax')] + depends = [('python2', 'python3crystax', 'python3')] recipe = CymunkRecipe() diff --git a/pythonforandroid/recipes/dateutil/__init__.py b/pythonforandroid/recipes/dateutil/__init__.py index ace3c638a2..3367f8d145 100644 --- a/pythonforandroid/recipes/dateutil/__init__.py +++ b/pythonforandroid/recipes/dateutil/__init__.py @@ -6,7 +6,7 @@ class DateutilRecipe(PythonRecipe): version = '2.6.0' url = 'https://pypi.python.org/packages/51/fc/39a3fbde6864942e8bb24c93663734b74e281b984d1b8c4f95d64b0c21f6/python-dateutil-2.6.0.tar.gz' - depends = ['python2', "setuptools"] + depends = ["setuptools"] call_hostpython_via_targetpython = False install_in_hostpython = True diff --git a/pythonforandroid/recipes/decorator/__init__.py b/pythonforandroid/recipes/decorator/__init__.py index e97840c190..e1001dd6f3 100644 --- a/pythonforandroid/recipes/decorator/__init__.py +++ b/pythonforandroid/recipes/decorator/__init__.py @@ -5,7 +5,7 @@ class DecoratorPyRecipe(PythonRecipe): version = '4.2.1' url = 'https://pypi.python.org/packages/source/d/decorator/decorator-{version}.tar.gz' url = 'https://github.com/micheles/decorator/archive/{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] + depends = ['setuptools'] site_packages_name = 'decorator' call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/doubleratchet/__init__.py b/pythonforandroid/recipes/doubleratchet/__init__.py index f5d13d0a5f..8e93044bf1 100644 --- a/pythonforandroid/recipes/doubleratchet/__init__.py +++ b/pythonforandroid/recipes/doubleratchet/__init__.py @@ -6,7 +6,6 @@ class DoubleRatchetRecipe(PythonRecipe): version = '0.4.0' url = 'https://pypi.python.org/packages/source/D/DoubleRatchet/DoubleRatchet-{version}.tar.gz' depends = [ - ('python2', 'python3crystax'), 'setuptools', 'cryptography', ] diff --git a/pythonforandroid/recipes/enaml/__init__.py b/pythonforandroid/recipes/enaml/__init__.py index 81738e73aa..d2335206c8 100644 --- a/pythonforandroid/recipes/enaml/__init__.py +++ b/pythonforandroid/recipes/enaml/__init__.py @@ -4,9 +4,9 @@ class EnamlRecipe(CppCompiledComponentsPythonRecipe): site_packages_name = 'enaml' version = '0.9.8' - url = 'https://github.com/nucleic/enaml/archive/master.zip' + url = 'https://github.com/nucleic/enaml/archive/{version}.zip' patches = ['0001-Update-setup.py.patch'] # Remove PyQt dependency - depends = ['python2', 'setuptools', 'atom', 'kiwisolver'] + depends = ['setuptools', 'atom', 'kiwisolver'] recipe = EnamlRecipe() diff --git a/pythonforandroid/recipes/ethash/__init__.py b/pythonforandroid/recipes/ethash/__init__.py index 403513d89d..b65e10ad38 100644 --- a/pythonforandroid/recipes/ethash/__init__.py +++ b/pythonforandroid/recipes/ethash/__init__.py @@ -5,7 +5,7 @@ class EthashRecipe(PythonRecipe): url = 'https://github.com/ethereum/ethash/archive/master.zip' - depends = ['python2', 'setuptools'] + depends = ['setuptools'] recipe = EthashRecipe() diff --git a/pythonforandroid/recipes/evdev/__init__.py b/pythonforandroid/recipes/evdev/__init__.py index b4921dd76e..afd542e2a0 100644 --- a/pythonforandroid/recipes/evdev/__init__.py +++ b/pythonforandroid/recipes/evdev/__init__.py @@ -6,7 +6,7 @@ class EvdevRecipe(CompiledComponentsPythonRecipe): version = 'v0.4.7' url = 'https://github.com/gvalkov/python-evdev/archive/{version}.zip' - depends = [('python2', 'python3crystax')] + depends = [] build_cmd = 'build' diff --git a/pythonforandroid/recipes/ffmpeg/__init__.py b/pythonforandroid/recipes/ffmpeg/__init__.py index 818722c3f7..f8e3ec141f 100644 --- a/pythonforandroid/recipes/ffmpeg/__init__.py +++ b/pythonforandroid/recipes/ffmpeg/__init__.py @@ -4,7 +4,7 @@ class FFMpegRecipe(Recipe): - version = '3.4.1' + version = '3.4.5' url = 'http://ffmpeg.org/releases/ffmpeg-{version}.tar.bz2' depends = ['sdl2'] # Need this to build correct recipe order opts_depends = ['openssl', 'ffpyplayer_codecs'] diff --git a/pythonforandroid/recipes/gevent-websocket/__init__.py b/pythonforandroid/recipes/gevent-websocket/__init__.py index 8820a10b3b..598ca130c7 100644 --- a/pythonforandroid/recipes/gevent-websocket/__init__.py +++ b/pythonforandroid/recipes/gevent-websocket/__init__.py @@ -4,7 +4,7 @@ class GeventWebsocketRecipe(PythonRecipe): version = '0.9.5' url = 'https://pypi.python.org/packages/source/g/gevent-websocket/gevent-websocket-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] + depends = ['setuptools'] site_packages_name = 'geventwebsocket' call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/gevent/__init__.py b/pythonforandroid/recipes/gevent/__init__.py index efe08d45f4..5933fb3364 100644 --- a/pythonforandroid/recipes/gevent/__init__.py +++ b/pythonforandroid/recipes/gevent/__init__.py @@ -1,27 +1,31 @@ -import os -from pythonforandroid.recipe import CompiledComponentsPythonRecipe +import re +from pythonforandroid.logger import info +from pythonforandroid.recipe import CythonRecipe -class GeventRecipe(CompiledComponentsPythonRecipe): - version = '1.1.1' +class GeventRecipe(CythonRecipe): + version = '1.4.0' url = 'https://pypi.python.org/packages/source/g/gevent/gevent-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'greenlet'] - patches = ["gevent.patch"] + depends = ['librt', 'greenlet'] + patches = ["cross_compiling.patch"] def get_recipe_env(self, arch=None, with_flags_in_cc=True): + """ + - Moves all -I -D from CFLAGS to CPPFLAGS environment. + - Moves all -l from LDFLAGS to LIBS environment. + - Fixes linker name (use cross compiler) and flags (appends LIBS) + """ env = super(GeventRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # sets linker to use the correct gcc (cross compiler) - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' # CFLAGS may only be used to specify C compiler flags, for macro definitions use CPPFLAGS - env['CPPFLAGS'] = env['CFLAGS'] + ' -I{}/sources/python/3.5/include/python/'.format(self.ctx.ndk_dir) - env['CFLAGS'] = '' + regex = re.compile(r'(?:\s|^)-[DI][\S]+') + env['CPPFLAGS'] = ''.join(re.findall(regex, env['CFLAGS'])).strip() + env['CFLAGS'] = re.sub(regex, '', env['CFLAGS']) + info('Moved "{}" from CFLAGS to CPPFLAGS.'.format(env['CPPFLAGS'])) # LDFLAGS may only be used to specify linker flags, for libraries use LIBS - env['LDFLAGS'] = env['LDFLAGS'].replace('-lm', '').replace('-lcrystax', '') - env['LDFLAGS'] += ' -L{}'.format(os.path.join(self.ctx.bootstrap.build_dir, 'libs', arch.arch)) - env['LIBS'] = ' -lm' - if self.ctx.ndk == 'crystax': - env['LIBS'] += ' -lcrystax -lpython{}m'.format(self.ctx.python_recipe.version[0:3]) - env['LDSHARED'] += env['LIBS'] + regex = re.compile(r'(?:\s|^)-l[\w\.]+') + env['LIBS'] = ''.join(re.findall(regex, env['LDFLAGS'])).strip() + env['LDFLAGS'] = re.sub(regex, '', env['LDFLAGS']) + info('Moved "{}" from LDFLAGS to LIBS.'.format(env['LIBS'])) return env diff --git a/pythonforandroid/recipes/gevent/cross_compiling.patch b/pythonforandroid/recipes/gevent/cross_compiling.patch new file mode 100644 index 0000000000..01e55d8c00 --- /dev/null +++ b/pythonforandroid/recipes/gevent/cross_compiling.patch @@ -0,0 +1,26 @@ +diff --git a/_setupares.py b/_setupares.py +index dd184de6..bb16bebe 100644 +--- a/_setupares.py ++++ b/_setupares.py +@@ -43,7 +43,7 @@ else: + ares_configure_command = ' '.join([ + "(cd ", quoted_dep_abspath('c-ares'), + " && if [ -r ares_build.h ]; then cp ares_build.h ares_build.h.orig; fi ", +- " && sh ./configure --disable-dependency-tracking " + _m32 + "CONFIG_COMMANDS= ", ++ " && sh ./configure --host={} --disable-dependency-tracking ".format(os.environ['TOOLCHAIN_PREFIX']) + _m32 + "CONFIG_COMMANDS= ", + " && cp ares_config.h ares_build.h \"$OLDPWD\" ", + " && cat ares_build.h ", + " && if [ -r ares_build.h.orig ]; then mv ares_build.h.orig ares_build.h; fi)", +diff --git a/_setuplibev.py b/_setuplibev.py +index 2a5841bf..b6433c94 100644 +--- a/_setuplibev.py ++++ b/_setuplibev.py +@@ -31,7 +31,7 @@ LIBEV_EMBED = should_embed('libev') + # and the PyPy branch will clean it up. + libev_configure_command = ' '.join([ + "(cd ", quoted_dep_abspath('libev'), +- " && sh ./configure ", ++ " && sh ./configure --host={} ".format(os.environ['TOOLCHAIN_PREFIX']), + " && cp config.h \"$OLDPWD\"", + ")", + '> configure-output.txt' diff --git a/pythonforandroid/recipes/gevent/gevent.patch b/pythonforandroid/recipes/gevent/gevent.patch deleted file mode 100644 index 72c77cece2..0000000000 --- a/pythonforandroid/recipes/gevent/gevent.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff -Naur gevent-1.1.1/setup.py gevent-1.1.1_diff/setup.py ---- gevent-1.1.1/setup.py 2016-04-04 17:27:33.000000000 +0200 -+++ gevent-1.1.1_diff/setup.py 2016-05-10 10:10:39.145881610 +0200 -@@ -96,7 +96,7 @@ - # and the PyPy branch will clean it up. - libev_configure_command = ' '.join([ - "(cd ", _quoted_abspath('libev/'), -- " && /bin/sh ./configure ", -+ " && /bin/sh ./configure --host={}".format(os.environ['TOOLCHAIN_PREFIX']), - " && cp config.h \"$OLDPWD\"", - ")", - '> configure-output.txt' -@@ -112,7 +112,7 @@ - # Use -r, not -e, for support of old solaris. See https://github.com/gevent/gevent/issues/777 - ares_configure_command = ' '.join(["(cd ", _quoted_abspath('c-ares/'), - " && if [ -r ares_build.h ]; then cp ares_build.h ares_build.h.orig; fi ", -- " && /bin/sh ./configure " + _m32 + "CONFIG_COMMANDS= CONFIG_FILES= ", -+ " && /bin/sh ./configure --host={} ".format(os.environ['TOOLCHAIN_PREFIX']) + "CONFIG_COMMANDS= CONFIG_FILES= ", - " && cp ares_config.h ares_build.h \"$OLDPWD\" ", - " && mv ares_build.h.orig ares_build.h)", - "> configure-output.txt"]) diff --git a/pythonforandroid/recipes/greenlet/__init__.py b/pythonforandroid/recipes/greenlet/__init__.py index 80564cd284..3f2043d57d 100644 --- a/pythonforandroid/recipes/greenlet/__init__.py +++ b/pythonforandroid/recipes/greenlet/__init__.py @@ -1,10 +1,11 @@ -from pythonforandroid.recipe import PythonRecipe +from pythonforandroid.recipe import CompiledComponentsPythonRecipe -class GreenletRecipe(PythonRecipe): - version = '0.4.9' +class GreenletRecipe(CompiledComponentsPythonRecipe): + version = '0.4.15' url = 'https://pypi.python.org/packages/source/g/greenlet/greenlet-{version}.tar.gz' - depends = [('python2', 'python3crystax')] + depends = ['setuptools'] + call_hostpython_via_targetpython = False recipe = GreenletRecipe() diff --git a/pythonforandroid/recipes/groestlcoin_hash/__init__.py b/pythonforandroid/recipes/groestlcoin_hash/__init__.py index 6eb7333cda..43e7ab3686 100644 --- a/pythonforandroid/recipes/groestlcoin_hash/__init__.py +++ b/pythonforandroid/recipes/groestlcoin_hash/__init__.py @@ -4,7 +4,7 @@ class GroestlcoinHashRecipe(CythonRecipe): version = '1.0.1' url = 'https://github.com/Groestlcoin/groestlcoin-hash-python/archive/{version}.tar.gz' - depends = ['python3crystax'] + depends = [] call_hostpython_via_targetpython = True cythonize = False diff --git a/pythonforandroid/recipes/icu/__init__.py b/pythonforandroid/recipes/icu/__init__.py index 56914ff1a3..4bb2de0c99 100644 --- a/pythonforandroid/recipes/icu/__init__.py +++ b/pythonforandroid/recipes/icu/__init__.py @@ -11,7 +11,7 @@ class ICURecipe(NDKRecipe): version = '57.1' url = 'http://download.icu-project.org/files/icu4c/57.1/icu4c-57_1-src.tgz' - depends = [('python2', 'python3crystax'), 'hostpython2'] # installs in python + depends = [('hostpython2', 'hostpython3')] # installs in python generated_libraries = [ 'libicui18n.so', 'libicuuc.so', 'libicudata.so', 'libicule.so'] diff --git a/pythonforandroid/recipes/ifaddrs/__init__.py b/pythonforandroid/recipes/ifaddrs/__init__.py index 5d6d4a0b9e..47c0008fa5 100644 --- a/pythonforandroid/recipes/ifaddrs/__init__.py +++ b/pythonforandroid/recipes/ifaddrs/__init__.py @@ -8,20 +8,14 @@ class IFAddrRecipe(CompiledComponentsPythonRecipe): - version = 'master' - url = 'git+https://github.com/morristech/android-ifaddrs.git' - depends = [('hostpython2', 'hostpython3'), ('python2', 'python3crystax')] + version = '8f9a87c' + url = 'https://github.com/morristech/android-ifaddrs/archive/{version}.zip' + depends = [('hostpython2', 'hostpython3')] call_hostpython_via_targetpython = False site_packages_name = 'ifaddrs' generated_libraries = ['libifaddrs.so'] - def should_build(self, arch): - """It's faster to build than check""" - return not ( - exists(join(self.ctx.libs_dir, arch.arch, 'libifaddrs.so')) - and exists(join(self.ctx.get_python_install_dir(), 'lib' "libifaddrs.so"))) - def prebuild_arch(self, arch): """Make the build and target directories""" path = self.get_build_dir(arch.arch) @@ -39,30 +33,22 @@ def build_arch(self, arch): if not exists(path): info("creating {}".format(path)) shprint(sh.mkdir, '-p', path) - cli = env['CC'].split() - cc = sh.Command(cli[0]) + cli = env['CC'].split()[0] + # makes sure first CC command is the compiler rather than ccache, refs: + # https://github.com/kivy/python-for-android/issues/1398 + if 'ccache' in cli: + cli = env['CC'].split()[1] + cc = sh.Command(cli) with current_directory(self.get_build_dir(arch.arch)): cflags = env['CFLAGS'].split() cflags.extend(['-I.', '-c', '-l.', 'ifaddrs.c', '-I.']) shprint(cc, *cflags, _env=env) - cflags = env['CFLAGS'].split() cflags.extend(['-shared', '-I.', 'ifaddrs.o', '-o', 'libifaddrs.so']) cflags.extend(env['LDFLAGS'].split()) shprint(cc, *cflags, _env=env) - shprint(sh.cp, 'libifaddrs.so', self.ctx.get_libs_dir(arch.arch)) - shprint(sh.cp, "libifaddrs.so", join(self.ctx.get_python_install_dir(), 'lib')) - # drop header in to the Python include directory - python_version = self.ctx.python_recipe.version[0:3] - shprint(sh.cp, "ifaddrs.h", - join( - self.ctx.get_python_install_dir(), - 'include/python{}'.format(python_version)) - ) - include_path = join(self.ctx.python_recipe.get_build_dir(arch.arch), 'Include') - shprint(sh.cp, "ifaddrs.h", include_path) recipe = IFAddrRecipe() diff --git a/pythonforandroid/recipes/kivent_core/__init__.py b/pythonforandroid/recipes/kivent_core/__init__.py deleted file mode 100644 index f04152a6db..0000000000 --- a/pythonforandroid/recipes/kivent_core/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from pythonforandroid.recipe import CythonRecipe -from os.path import join - - -class KiventCoreRecipe(CythonRecipe): - version = 'master' - url = 'https://github.com/kivy/kivent/archive/{version}.zip' - name = 'kivent_core' - - depends = ['kivy'] - - subbuilddir = False - - def get_recipe_env(self, arch, with_flags_in_cc=True): - env = super(KiventCoreRecipe, self).get_recipe_env( - arch, with_flags_in_cc=with_flags_in_cc) - env['CYTHONPATH'] = self.get_recipe( - 'kivy', self.ctx).get_build_dir(arch.arch) - return env - - def get_build_dir(self, arch, sub=False): - builddir = super(KiventCoreRecipe, self).get_build_dir(arch) - if sub or self.subbuilddir: - return join(builddir, 'modules', 'core') - else: - return builddir - - def build_arch(self, arch): - self.subbuilddir = True - super(KiventCoreRecipe, self).build_arch(arch) - self.subbuilddir = False - - -recipe = KiventCoreRecipe() diff --git a/pythonforandroid/recipes/kivent_cymunk/__init__.py b/pythonforandroid/recipes/kivent_cymunk/__init__.py deleted file mode 100644 index 20031f16c2..0000000000 --- a/pythonforandroid/recipes/kivent_cymunk/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -from pythonforandroid.recipe import CythonRecipe -from os.path import join - - -class KiventCymunkRecipe(CythonRecipe): - name = 'kivent_cymunk' - - depends = ['kivent_core', 'cymunk'] - - subbuilddir = False - - def get_recipe_env(self, arch, with_flags_in_cc=True): - env = super(KiventCymunkRecipe, self).get_recipe_env( - arch, with_flags_in_cc=with_flags_in_cc) - cymunk = self.get_recipe('cymunk', self.ctx).get_build_dir(arch.arch) - env['PYTHONPATH'] = join(cymunk, 'cymunk', 'python') - kivy = self.get_recipe('kivy', self.ctx).get_build_dir(arch.arch) - kivent = self.get_recipe('kivent_core', - self.ctx).get_build_dir(arch.arch, sub=True) - env['CYTHONPATH'] = ':'.join((kivy, cymunk, kivent)) - return env - - def prepare_build_dir(self, arch): - '''No need to prepare, we'll use kivent_core''' - return - - def get_build_dir(self, arch): - builddir = self.get_recipe('kivent_core', self.ctx).get_build_dir(arch) - return join(builddir, 'modules', 'cymunk') - - -recipe = KiventCymunkRecipe() diff --git a/pythonforandroid/recipes/kivent_particles/__init__.py b/pythonforandroid/recipes/kivent_particles/__init__.py deleted file mode 100644 index 0ec8efeb5a..0000000000 --- a/pythonforandroid/recipes/kivent_particles/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -from pythonforandroid.recipe import CythonRecipe -from os.path import join - - -class KiventParticlesRecipe(CythonRecipe): - name = 'kivent_particles' - - depends = ['kivent_core'] - - subbuilddir = False - - def get_recipe_env(self, arch, with_flags_in_cc=True): - env = super(KiventParticlesRecipe, self).get_recipe_env( - arch, with_flags_in_cc=with_flags_in_cc) - kivy = self.get_recipe('kivy', self.ctx).get_build_dir(arch.arch) - kivent = self.get_recipe('kivent_core', - self.ctx).get_build_dir(arch.arch, sub=True) - env['CYTHONPATH'] = ':'.join((kivy, kivent)) - return env - - def prepare_build_dir(self, arch): - '''No need to prepare, we'll use kivent_core''' - return - - def get_build_dir(self, arch): - builddir = self.get_recipe('kivent_core', self.ctx).get_build_dir(arch) - return join(builddir, 'modules', 'particles') - - -recipe = KiventParticlesRecipe() diff --git a/pythonforandroid/recipes/kivent_polygen/__init__.py b/pythonforandroid/recipes/kivent_polygen/__init__.py deleted file mode 100644 index 96f14f40f8..0000000000 --- a/pythonforandroid/recipes/kivent_polygen/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -from pythonforandroid.recipe import CythonRecipe -from os.path import join - - -class KiventPolygenRecipe(CythonRecipe): - name = 'kivent_polygen' - - depends = ['kivent_core'] - - subbuilddir = False - - def get_recipe_env(self, arch, with_flags_in_cc=True): - env = super(KiventPolygenRecipe, self).get_recipe_env( - arch, with_flags_in_cc=with_flags_in_cc) - kivy = self.get_recipe('kivy', self.ctx).get_build_dir(arch.arch) - kivent = self.get_recipe('kivent_core', - self.ctx).get_build_dir(arch.arch, sub=True) - env['CYTHONPATH'] = ':'.join((kivy, kivent)) - return env - - def prepare_build_dir(self, arch): - '''No need to prepare, we'll use kivent_core''' - return - - def get_build_dir(self, arch): - builddir = self.get_recipe('kivent_core', self.ctx).get_build_dir(arch) - return join(builddir, 'modules', 'polygen') - - -recipe = KiventPolygenRecipe() diff --git a/pythonforandroid/recipes/kivy/__init__.py b/pythonforandroid/recipes/kivy/__init__.py index 70a5f5e69c..19c3a303f8 100644 --- a/pythonforandroid/recipes/kivy/__init__.py +++ b/pythonforandroid/recipes/kivy/__init__.py @@ -6,7 +6,8 @@ class KivyRecipe(CythonRecipe): - version = '1.10.1' + # post kivy==1.10.1, `fixes SDL2 image loading (jpg)` + version = 'a95d67f' url = 'https://github.com/kivy/kivy/archive/{version}.zip' name = 'kivy' diff --git a/pythonforandroid/recipes/libffi/__init__.py b/pythonforandroid/recipes/libffi/__init__.py index 62c452c10f..c1fd4a40c4 100644 --- a/pythonforandroid/recipes/libffi/__init__.py +++ b/pythonforandroid/recipes/libffi/__init__.py @@ -1,7 +1,7 @@ from os.path import exists, join from pythonforandroid.recipe import Recipe from pythonforandroid.logger import info, shprint -from pythonforandroid.util import current_directory +from pythonforandroid.util import current_directory, ensure_dir from glob import glob import sh @@ -17,22 +17,11 @@ class LibffiRecipe(Recipe): version = '3.2.1' url = 'https://github.com/libffi/libffi/archive/v{version}.tar.gz' - patches = ['remove-version-info.patch'] - - def get_host(self, arch): - with current_directory(self.get_build_dir(arch.arch)): - host = None - with open('Makefile') as f: - for line in f: - if line.startswith('host = '): - host = line.strip()[7:] - break - - if not host or not exists(host): - raise RuntimeError('failed to find build output! ({})' - .format(host)) - - return host + patches = ['remove-version-info.patch', + # This patch below is already included into libffi's master + # branch and included in the pre-release 3.3rc0...so we should + # remove this when we update the version number for libffi + 'fix-includedir.patch'] def should_build(self, arch): return not exists(join(self.ctx.get_libs_dir(arch.arch), 'libffi.so')) @@ -45,7 +34,8 @@ def build_arch(self, arch): shprint(sh.Command('autoreconf'), '-vif', _env=env) shprint(sh.Command('./configure'), '--host=' + arch.command_prefix, - '--prefix=' + self.ctx.get_python_install_dir(), + '--prefix=' + self.get_build_dir(arch.arch), + '--disable-builddir', '--enable-shared', _env=env) # '--with-sysroot={}'.format(self.ctx.ndk_platform), # '--target={}'.format(arch.toolchain_prefix), @@ -62,7 +52,7 @@ def build_arch(self, arch): info("make libffi.la failed as expected") cc = sh.Command(env['CC'].split()[0]) cflags = env['CC'].split()[1:] - host_build = join(self.get_build_dir(arch.arch), self.get_host(arch)) + host_build = self.get_build_dir(arch.arch) arch_flags = '' if '-march=' in env['CFLAGS']: @@ -87,12 +77,13 @@ def build_arch(self, arch): with current_directory(host_build): shprint(cc, *cflags, _env=env) - shprint(sh.cp, '-t', self.ctx.get_libs_dir(arch.arch), - join(host_build, '.libs', 'libffi.so')) + ensure_dir(self.ctx.get_libs_dir(arch.arch)) + shprint(sh.cp, + join(host_build, '.libs', 'libffi.so'), + self.ctx.get_libs_dir(arch.arch)) def get_include_dirs(self, arch): - return [join(self.get_build_dir(arch.arch), self.get_host(arch), - 'include')] + return [join(self.get_build_dir(arch.arch), 'include')] recipe = LibffiRecipe() diff --git a/pythonforandroid/recipes/libffi/fix-includedir.patch b/pythonforandroid/recipes/libffi/fix-includedir.patch new file mode 100644 index 0000000000..0dc35c70ca --- /dev/null +++ b/pythonforandroid/recipes/libffi/fix-includedir.patch @@ -0,0 +1,34 @@ +From 982b89c01aca99c7bc229914fc1521f96930919b Mon Sep 17 00:00:00 2001 +From: Yen Chi Hsuan +Date: Sun, 13 Nov 2016 19:17:19 +0800 +Subject: [PATCH] Install public headers in the standard path + +--- + include/Makefile.am | 3 +-- + libffi.pc.in | 2 +- + 2 files changed, 2 insertions(+), 3 deletions(-) + +diff --git a/include/Makefile.am b/include/Makefile.am +index bb241e88..c59df9fb 100644 +--- a/include/Makefile.am ++++ b/include/Makefile.am +@@ -6,5 +6,4 @@ DISTCLEANFILES=ffitarget.h + noinst_HEADERS=ffi_common.h ffi_cfi.h + EXTRA_DIST=ffi.h.in + +-includesdir = $(libdir)/@PACKAGE_NAME@-@PACKAGE_VERSION@/include +-nodist_includes_HEADERS = ffi.h ffitarget.h ++nodist_include_HEADERS = ffi.h ffitarget.h +diff --git a/libffi.pc.in b/libffi.pc.in +index edf6fde5..6fad83b4 100644 +--- a/libffi.pc.in ++++ b/libffi.pc.in +@@ -2,7 +2,7 @@ prefix=@prefix@ + exec_prefix=@exec_prefix@ + libdir=@libdir@ + toolexeclibdir=@toolexeclibdir@ +-includedir=${libdir}/@PACKAGE_NAME@-@PACKAGE_VERSION@/include ++includedir=@includedir@ + + Name: @PACKAGE_NAME@ + Description: Library supporting Foreign Function Interfaces diff --git a/pythonforandroid/recipes/libgeos/__init__.py b/pythonforandroid/recipes/libgeos/__init__.py index e89e3d5714..30786f8ea4 100644 --- a/pythonforandroid/recipes/libgeos/__init__.py +++ b/pythonforandroid/recipes/libgeos/__init__.py @@ -8,7 +8,7 @@ class LibgeosRecipe(Recipe): version = '3.5' # url = 'http://download.osgeo.org/geos/geos-{version}.tar.bz2' url = 'https://github.com/libgeos/libgeos/archive/svn-{version}.zip' - depends = ['python2'] + depends = [] def should_build(self, arch): super(LibgeosRecipe, self).should_build(arch) diff --git a/pythonforandroid/recipes/libglob/__init__.py b/pythonforandroid/recipes/libglob/__init__.py index aa5925883f..37fa94859d 100644 --- a/pythonforandroid/recipes/libglob/__init__.py +++ b/pythonforandroid/recipes/libglob/__init__.py @@ -21,7 +21,7 @@ class LibGlobRecipe(CompiledComponentsPythonRecipe): # and pushed in via patch name = 'libglob' - depends = [('hostpython2', 'hostpython3'), ('python2', 'python3crystax')] + depends = [('hostpython2', 'hostpython3')] patches = ['glob.patch'] def should_build(self, arch): diff --git a/pythonforandroid/recipes/libnacl/__init__.py b/pythonforandroid/recipes/libnacl/__init__.py index 6ebdc5ad82..3fc5da82f0 100644 --- a/pythonforandroid/recipes/libnacl/__init__.py +++ b/pythonforandroid/recipes/libnacl/__init__.py @@ -4,7 +4,7 @@ class LibNaClRecipe(PythonRecipe): version = '1.4.4' url = 'https://github.com/saltstack/libnacl/archive/v{version}.tar.gz' - depends = ['hostpython2', 'setuptools', 'libsodium'] + depends = [('hostpython2', 'hostpython3'), 'setuptools', 'libsodium'] site_packages_name = 'libnacl' call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/libpq/__init__.py b/pythonforandroid/recipes/libpq/__init__.py index dcfb541870..45c296a2a6 100644 --- a/pythonforandroid/recipes/libpq/__init__.py +++ b/pythonforandroid/recipes/libpq/__init__.py @@ -6,7 +6,7 @@ class LibpqRecipe(Recipe): version = '9.5.3' url = 'http://ftp.postgresql.org/pub/source/v{version}/postgresql-{version}.tar.bz2' - depends = [('python2', 'python3crystax')] + depends = [] def should_build(self, arch): return not os.path.isfile('{}/libpq.a'.format(self.ctx.get_libs_dir(arch.arch))) diff --git a/pythonforandroid/recipes/librt/__init__.py b/pythonforandroid/recipes/librt/__init__.py new file mode 100644 index 0000000000..9eb56b3b18 --- /dev/null +++ b/pythonforandroid/recipes/librt/__init__.py @@ -0,0 +1,55 @@ +from os import makedirs, remove +from os.path import exists, join +import sh + +from pythonforandroid.recipe import Recipe +from pythonforandroid.logger import shprint + + +class LibRt(Recipe): + ''' + This is a dumb recipe. We may need this because some recipes inserted some + flags `-lrt` without our control, case of: + + - :class:`~pythonforandroid.recipes.gevent.GeventRecipe` + - :class:`~pythonforandroid.recipes.lxml.LXMLRecipe` + + .. note:: the librt doesn't exist in android but it is integrated into + libc, so we create a symbolic link which we will remove when our build + finishes''' + + @property + def libc_path(self): + return join(self.ctx.ndk_platform, 'usr', 'lib', 'libc') + + def build_arch(self, arch): + # Create a temporary folder to add to link path with a fake librt.so: + fake_librt_temp_folder = join( + self.get_build_dir(arch.arch), + "p4a-librt-recipe-tempdir" + ) + if not exists(fake_librt_temp_folder): + makedirs(fake_librt_temp_folder) + + # Set symlinks, and make sure to update them on every build run: + if exists(join(fake_librt_temp_folder, "librt.so")): + remove(join(fake_librt_temp_folder, "librt.so")) + shprint(sh.ln, '-sf', + self.libc_path + '.so', + join(fake_librt_temp_folder, "librt.so"), + ) + if exists(join(fake_librt_temp_folder, "librt.a")): + remove(join(fake_librt_temp_folder, "librt.a")) + shprint(sh.ln, '-sf', + self.libc_path + '.a', + join(fake_librt_temp_folder, "librt.a"), + ) + + # Add folder as -L link option for all recipes if not done yet: + if fake_librt_temp_folder not in arch.extra_global_link_paths: + arch.extra_global_link_paths.append( + fake_librt_temp_folder + ) + + +recipe = LibRt() diff --git a/pythonforandroid/recipes/libsodium/__init__.py b/pythonforandroid/recipes/libsodium/__init__.py index ca7a1f367b..9911e36baa 100644 --- a/pythonforandroid/recipes/libsodium/__init__.py +++ b/pythonforandroid/recipes/libsodium/__init__.py @@ -6,7 +6,7 @@ class LibsodiumRecipe(Recipe): version = '1.0.16' url = 'https://github.com/jedisct1/libsodium/releases/download/{version}/libsodium-{version}.tar.gz' - depends = ['python2'] + depends = [] patches = ['size_max_fix.patch'] def should_build(self, arch): diff --git a/pythonforandroid/recipes/libtorrent/__init__.py b/pythonforandroid/recipes/libtorrent/__init__.py index f358f5166d..c73bb02962 100644 --- a/pythonforandroid/recipes/libtorrent/__init__.py +++ b/pythonforandroid/recipes/libtorrent/__init__.py @@ -1,5 +1,7 @@ from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory -from os.path import join +from multiprocessing import cpu_count +from os.path import join, basename +from os import listdir, walk import sh # This recipe builds libtorrent with Python bindings @@ -7,71 +9,128 @@ # which is all provided by the boost recipe +def get_lib_from(search_directory, lib_extension='.so'): + '''Scan directories recursively until find any file with the given + extension. The default extension to search is ``.so``.''' + for root, dirs, files in walk(search_directory): + for file in files: + if file.endswith(lib_extension): + print('get_lib_from: {}\n\t- {}'.format( + search_directory, join(root, file))) + return join(root, file) + return None + + class LibtorrentRecipe(Recipe): - version = '1.0.9' - # Don't forget to change the URL when changing the version - url = 'https://github.com/arvidn/libtorrent/archive/libtorrent-1_0_9.tar.gz' - depends = ['boost', 'python2'] + # Todo: make recipe compatible with all p4a architectures + ''' + .. note:: This recipe can be built only against API 21+ and arch armeabi-v7a + + .. versionchanged:: 0.6.0 + Rewrote recipe to support clang's build and boost 1.68. The following + changes has been made: + + - Bumped version number to 1.2.0 + - added python 3 compatibility + - new system to detect/copy generated libraries + ''' + version = '1_2_0' + url = 'https://github.com/arvidn/libtorrent/archive/libtorrent_{version}.tar.gz' + + depends = ['boost'] opt_depends = ['openssl'] - patches = ['disable-so-version.patch', 'use-soname-python.patch', 'setup-lib-name.patch'] + patches = ['disable-so-version.patch', + 'use-soname-python.patch', + 'setup-lib-name.patch'] + + # libtorrent.so is not included because is not a system library + generated_libraries = [ + 'boost_system', 'boost_python{py_version}', 'torrent_rasterbar'] def should_build(self, arch): - return not ( - self.has_libs(arch, 'libboost_python.so', 'libboost_system.so', 'libtorrent_rasterbar.so') - and self.ctx.has_package('libtorrent', arch.arch)) + python_version = self.ctx.python_recipe.version[:3].replace('.', '') + libs = ['lib' + lib_name.format(py_version=python_version) + + '.so' for lib_name in self.generated_libraries] + return not (self.has_libs(arch, *libs) and + self.ctx.has_package('libtorrent', arch.arch)) def prebuild_arch(self, arch): super(LibtorrentRecipe, self).prebuild_arch(arch) if 'openssl' in recipe.ctx.recipe_build_order: # Patch boost user-config.jam to use openssl - self.get_recipe('boost', self.ctx).apply_patch(join(self.get_recipe_dir(), 'user-config-openssl.patch'), arch.arch) + self.get_recipe('boost', self.ctx).apply_patch( + join(self.get_recipe_dir(), 'user-config-openssl.patch'), arch.arch) def build_arch(self, arch): super(LibtorrentRecipe, self).build_arch(arch) env = self.get_recipe_env(arch) - with current_directory(join(self.get_build_dir(arch.arch), 'bindings/python')): - # Compile libtorrent with boost libraries and python bindings + env['PYTHON_HOST'] = self.ctx.hostpython + + # Define build variables + build_dir = self.get_build_dir(arch.arch) + ctx_libs_dir = self.ctx.get_libs_dir(arch.arch) + encryption = 'openssl' if 'openssl' in recipe.ctx.recipe_build_order else 'built-in' + build_args = [ + '-q', + # '-a', # force build, useful to debug the build + '-j' + str(cpu_count()), + '--debug-configuration', # so we know if our python is detected + # '--deprecated-functions=off', + 'toolset=clang-arm', + 'abi=aapcs', + 'binary-format=elf', + 'cxxflags=-std=c++11', + 'target-os=android', + 'threading=multi', + 'link=shared', + 'boost-link=shared', + 'libtorrent-link=shared', + 'runtime-link=shared', + 'encryption={}'.format('on' if encryption == 'openssl' else 'off'), + 'crypto=' + encryption + ] + crypto_folder = 'encryption-off' + if encryption == 'openssl': + crypto_folder = 'crypto-openssl' + build_args.extend(['openssl-lib=' + env['OPENSSL_BUILD_PATH'], + 'openssl-include=' + env['OPENSSL_INCLUDE'] + ]) + build_args.append('release') + + # Compile libtorrent with boost libraries and python bindings + with current_directory(join(build_dir, 'bindings/python')): b2 = sh.Command(join(env['BOOST_ROOT'], 'b2')) - shprint(b2, - '-q', - '-j5', - 'toolset=gcc-' + env['ARCH'], - 'target-os=android', - 'threading=multi', - 'link=shared', - 'boost-link=shared', - 'boost=source', - 'encryption=openssl' if 'openssl' in recipe.ctx.recipe_build_order else '', - '--prefix=' + env['CROSSHOME'], - 'release', _env=env) - # Common build directories - build_subdirs = 'gcc-arm/release/boost-link-shared/boost-source' - if 'openssl' in recipe.ctx.recipe_build_order: - build_subdirs += '/encryption-openssl' - build_subdirs += '/libtorrent-python-pic-on/target-os-android/threading-multi/visibility-hidden' - # Copy the shared libraries into the libs folder - shutil.copyfile(join(env['BOOST_BUILD_PATH'], 'bin.v2/libs/python/build', build_subdirs, 'libboost_python.so'), - join(self.ctx.get_libs_dir(arch.arch), 'libboost_python.so')) - shutil.copyfile(join(env['BOOST_BUILD_PATH'], 'bin.v2/libs/system/build', build_subdirs, 'libboost_system.so'), - join(self.ctx.get_libs_dir(arch.arch), 'libboost_system.so')) - if 'openssl' in recipe.ctx.recipe_build_order: - shutil.copyfile( - join(env['BOOST_BUILD_PATH'], 'bin.v2/libs/date_time/build', build_subdirs, 'libboost_date_time.so'), - join(self.ctx.get_libs_dir(arch.arch), 'libboost_date_time.so')) - shutil.copyfile( - join(self.get_build_dir(arch.arch), 'bin', build_subdirs, 'libtorrent_rasterbar.so'), - join(self.ctx.get_libs_dir(arch.arch), 'libtorrent_rasterbar.so')) - shutil.copyfile( - join(self.get_build_dir(arch.arch), 'bindings/python/bin', build_subdirs, 'libtorrent.so'), - join(self.ctx.get_site_packages_dir(arch.arch), 'libtorrent.so')) + shprint(b2, *build_args, _env=env) + + # Copy only the boost shared libraries into the libs folder. Because + # boost build two boost_python libraries, we force to search the lib + # into the corresponding build path. + b2_build_dir = 'build/clang-linux-arm/release/{encryption}/' \ + 'lt-visibility-hidden/'.format(encryption=crypto_folder) + boost_libs_dir = join(env['BOOST_BUILD_PATH'], 'bin.v2/libs') + for boost_lib in listdir(boost_libs_dir): + lib_path = get_lib_from(join(boost_libs_dir, boost_lib, b2_build_dir)) + if lib_path: + lib_name = basename(lib_path) + shutil.copyfile(lib_path, join(ctx_libs_dir, lib_name)) + + # Copy libtorrent shared libraries into the right places + system_libtorrent = get_lib_from(join(build_dir, 'bin')) + if system_libtorrent: + shutil.copyfile(system_libtorrent, + join(ctx_libs_dir, 'libtorrent_rasterbar.so')) + + python_libtorrent = get_lib_from(join(build_dir, 'bindings/python/bin')) + shutil.copyfile(python_libtorrent, + join(self.ctx.get_site_packages_dir(arch.arch), 'libtorrent.so')) def get_recipe_env(self, arch): - env = super(LibtorrentRecipe, self).get_recipe_env(arch) - # Copy environment from boost recipe - env.update(self.get_recipe('boost', self.ctx).get_recipe_env(arch)) + # Use environment from boost recipe, cause we use b2 tool from boost + env = self.get_recipe('boost', self.ctx).get_recipe_env(arch) if 'openssl' in recipe.ctx.recipe_build_order: r = self.get_recipe('openssl', self.ctx) env['OPENSSL_BUILD_PATH'] = r.get_build_dir(arch.arch) + env['OPENSSL_INCLUDE'] = join(r.get_build_dir(arch.arch), 'include') env['OPENSSL_VERSION'] = r.version return env diff --git a/pythonforandroid/recipes/libtorrent/setup-lib-name.patch b/pythonforandroid/recipes/libtorrent/setup-lib-name.patch index ec3985af16..183705c839 100644 --- a/pythonforandroid/recipes/libtorrent/setup-lib-name.patch +++ b/pythonforandroid/recipes/libtorrent/setup-lib-name.patch @@ -1,20 +1,20 @@ ---- libtorrent/bindings/python/setup.py 2016-02-28 08:28:49.000000000 +0100 -+++ patch/bindings/python/setup.py 2016-07-12 12:03:05.256455888 +0200 -@@ -97,7 +97,7 @@ - source_list = os.listdir(os.path.join(os.path.dirname(__file__), "src")) - source_list = [os.path.join("src", s) for s in source_list if s.endswith(".cpp")] - -- ext = [Extension('libtorrent', -+ ext = [Extension('libtorrent_rasterbar', - sources = source_list, - language='c++', - include_dirs = parse_cmd(extra_cmd, '-I'), -@@ -107,7 +107,7 @@ - + target_specific(), - libraries = ['torrent-rasterbar'] + parse_cmd(extra_cmd, '-l'))] - --setup(name = 'python-libtorrent', -+setup(name = 'libtorrent', - version = '1.0.9', - author = 'Arvid Norberg', - author_email = 'arvid@libtorrent.org', +--- libtorrent/bindings/python/setup.py.orig 2018-11-26 22:21:48.772142135 +0100 ++++ libtorrent/bindings/python/setup.py 2018-11-26 22:23:23.092141235 +0100 +@@ -167,7 +167,7 @@ + extra_compile = flags.parse(extra_cmd) + + ext = [Extension( +- 'libtorrent', ++ 'libtorrent_rasterbar', + sources=sorted(source_list), + language='c++', + include_dirs=flags.include_dirs, +@@ -178,7 +178,7 @@ + ] + + setup( +- name='python-libtorrent', ++ name='libtorrent', + version='1.2.0', + author='Arvid Norberg', + author_email='arvid@libtorrent.org', diff --git a/pythonforandroid/recipes/libtorrent/use-soname-python.patch b/pythonforandroid/recipes/libtorrent/use-soname-python.patch index f78553d269..1456220712 100644 --- a/pythonforandroid/recipes/libtorrent/use-soname-python.patch +++ b/pythonforandroid/recipes/libtorrent/use-soname-python.patch @@ -1,11 +1,11 @@ ---- libtorrent/bindings/python/Jamfile 2016-01-17 23:52:45.000000000 +0100 -+++ libtorrent-patch/bindings/python/Jamfile 2016-02-09 17:11:44.261578000 +0100 -@@ -35,7 +35,7 @@ - - if ( gcc in $(properties) ) - { -- result += -Wl,-Bsymbolic ; -+ result += -Wl,-soname=libtorrent.so,-Bsymbolic ; - } - } +--- libtorrent/bindings/python/Jamfile.orig 2018-12-07 16:46:50.851838981 +0100 ++++ libtorrent/bindings/python/Jamfile 2018-12-07 16:49:09.099837663 +0100 +@@ -113,7 +113,7 @@ + + if ( gcc in $(properties) ) + { +- result += -Wl,-Bsymbolic ; ++ result += -Wl,-soname=libtorrent.so,-Bsymbolic ; + } + } diff --git a/pythonforandroid/recipes/libtorrent/user-config-openssl.patch b/pythonforandroid/recipes/libtorrent/user-config-openssl.patch index eea9b3f648..6a54071f43 100644 --- a/pythonforandroid/recipes/libtorrent/user-config-openssl.patch +++ b/pythonforandroid/recipes/libtorrent/user-config-openssl.patch @@ -1,25 +1,21 @@ ---- boost/user-config.jam 2016-03-02 14:31:41.280414820 +0100 -+++ boost-patch/user-config.jam 2016-03-02 14:32:08.904384741 +0100 -@@ -6,6 +6,7 @@ - local TOOLCHAIN_PREFIX = [ os.environ TOOLCHAIN_PREFIX ] ; - local ARCH = [ os.environ ARCH ] ; - local PYTHON_ROOT = [ os.environ PYTHON_ROOT ] ; +--- boost/user-config.jam.orig 2018-12-07 14:16:45.911924859 +0100 ++++ boost/user-config.jam 2018-12-07 14:20:16.243922853 +0100 +@@ -9,6 +9,8 @@ + local PYTHON_INCLUDE = [ os.environ PYTHON_INCLUDE ] ; + local PYTHON_LINK_VERSION = [ os.environ PYTHON_LINK_VERSION ] ; + local PYTHON_MAJOR_MINOR = [ os.environ PYTHON_MAJOR_MINOR ] ; +local OPENSSL_BUILD_PATH = [ os.environ OPENSSL_BUILD_PATH ] ; ++local OPENSSL_VERSION = [ os.environ OPENSSL_VERSION ] ; - using gcc : $(ARCH) : $(TOOLCHAIN_PREFIX)-g++ : - $(ARCH) -@@ -20,9 +21,14 @@ - -I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/include - -I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH)/include - -I$(PYTHON_ROOT)/include/python2.7 -+-I$(OPENSSL_BUILD_PATH)/include -+-I$(OPENSSL_BUILD_PATH)/include/openssl - --sysroot=$(ANDROIDNDK)/platforms/android-$(ANDROIDAPI)/arch-$(ARCH) - -L$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH) - -L$(PYTHON_ROOT)/lib + #using clang : $(ARCH) : $(ANDROID_BINARIES_PATH)/clang++ : + #$(ANDROID_BINARIES_PATH)/llvm-ar +@@ -56,6 +58,9 @@ + -Wl,-z,relro + -Wl,-z,now + -lc++_shared +-L$(OPENSSL_BUILD_PATH) - -lgnustl_shared - -lpython2.7 +-lcrypto$(OPENSSL_VERSION) +-lssl$(OPENSSL_VERSION) - ; + -L$(PYTHON_ROOT) + -lpython$(PYTHON_LINK_VERSION) + -Wl,-O1 diff --git a/pythonforandroid/recipes/libtribler/__init__.py b/pythonforandroid/recipes/libtribler/__init__.py index 6c64ecd145..1abb696892 100644 --- a/pythonforandroid/recipes/libtribler/__init__.py +++ b/pythonforandroid/recipes/libtribler/__init__.py @@ -14,7 +14,7 @@ class LibTriblerRecipe(PythonRecipe): url = 'git+https://github.com/Tribler/tribler.git' depends = ['apsw', 'cryptography', 'ffmpeg', 'libsodium', 'libtorrent', 'm2crypto', - 'netifaces', 'openssl', 'pil', 'pycrypto', 'pyleveldb', 'python2', 'twisted', + 'netifaces', 'openssl', 'pil', 'pycrypto', 'pyleveldb', 'twisted', ] python_depends = ['chardet', 'cherrypy', 'configobj', 'decorator', 'feedparser', diff --git a/pythonforandroid/recipes/libzbar/__init__.py b/pythonforandroid/recipes/libzbar/__init__.py index b9de0c828b..43ae34cc9d 100644 --- a/pythonforandroid/recipes/libzbar/__init__.py +++ b/pythonforandroid/recipes/libzbar/__init__.py @@ -11,22 +11,20 @@ class LibZBarRecipe(Recipe): url = 'https://github.com/ZBar/ZBar/archive/{version}.zip' - depends = ['hostpython2', 'python2', 'libiconv'] + depends = ['libiconv'] patches = ["werror.patch"] def should_build(self, arch): return not os.path.exists( - os.path.join(self.ctx.get_libs_dir(arch.arch), 'libzbar.so')) + os.path.join(self.ctx.get_libs_dir(arch.arch), 'libzbar.so')) def get_recipe_env(self, arch=None, with_flags_in_cc=True): env = super(LibZBarRecipe, self).get_recipe_env(arch, with_flags_in_cc) libiconv = self.get_recipe('libiconv', self.ctx) libiconv_dir = libiconv.get_build_dir(arch.arch) env['CFLAGS'] += ' -I' + os.path.join(libiconv_dir, 'include') - env['LDSHARED'] = env['CC'] + \ - ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' - env['LDFLAGS'] += " -landroid -liconv" + env['LIBS'] = env.get('LIBS', '') + ' -landroid -liconv' return env def build_arch(self, arch): diff --git a/pythonforandroid/recipes/libzmq/__init__.py b/pythonforandroid/recipes/libzmq/__init__.py index 0dea2052ed..b33f3ac6cf 100644 --- a/pythonforandroid/recipes/libzmq/__init__.py +++ b/pythonforandroid/recipes/libzmq/__init__.py @@ -7,7 +7,7 @@ class LibZMQRecipe(Recipe): version = '4.1.4' url = 'http://download.zeromq.org/zeromq-{version}.tar.gz' - depends = ['python2'] + depends = [] def should_build(self, arch): super(LibZMQRecipe, self).should_build(arch) diff --git a/pythonforandroid/recipes/lxml/__init__.py b/pythonforandroid/recipes/lxml/__init__.py index 2b782ef059..6d4b91c25b 100644 --- a/pythonforandroid/recipes/lxml/__init__.py +++ b/pythonforandroid/recipes/lxml/__init__.py @@ -1,14 +1,12 @@ from pythonforandroid.recipe import Recipe, CompiledComponentsPythonRecipe -from pythonforandroid.logger import shprint from os.path import exists, join from os import uname -import sh class LXMLRecipe(CompiledComponentsPythonRecipe): version = '4.2.5' url = 'https://pypi.python.org/packages/source/l/lxml/lxml-{version}.tar.gz' # noqa - depends = ['libxml2', 'libxslt', 'setuptools'] + depends = ['librt', 'libxml2', 'libxslt', 'setuptools'] name = 'lxml' call_hostpython_via_targetpython = False # Due to setuptools @@ -25,21 +23,6 @@ def should_build(self, arch): return not all([exists(join(build_dir, lib)) for lib in py_libs]) - def build_compiled_components(self, arch): - # Hack to make it link properly to librt, inserted automatically by the - # installer (Note: the librt doesn't exist in android but it is - # integrated into libc, so we create a symbolic link which we will - # remove when our build finishes) - link_c = join(self.ctx.ndk_platform, 'usr', 'lib', 'libc') - link_rt = join(self.ctx.ndk_platform, 'usr', 'lib', 'librt') - shprint(sh.ln, '-sf', link_c + '.so', link_rt + '.so') - shprint(sh.ln, '-sf', link_c + '.a', link_rt + '.a') - - super(LXMLRecipe, self).build_compiled_components(arch) - - shprint(sh.rm, '-r', link_rt + '.so') - shprint(sh.rm, '-r', link_rt + '.a') - def get_recipe_env(self, arch): env = super(LXMLRecipe, self).get_recipe_env(arch) diff --git a/pythonforandroid/recipes/m2crypto/__init__.py b/pythonforandroid/recipes/m2crypto/__init__.py index 399cdbeb48..653eeca85c 100644 --- a/pythonforandroid/recipes/m2crypto/__init__.py +++ b/pythonforandroid/recipes/m2crypto/__init__.py @@ -6,10 +6,9 @@ class M2CryptoRecipe(CompiledComponentsPythonRecipe): - version = '0.24.0' + version = '0.30.1' url = 'https://pypi.python.org/packages/source/M/M2Crypto/M2Crypto-{version}.tar.gz' - # md5sum = '89557730e245294a6cab06de8ad4fb42' - depends = ['openssl', 'hostpython2', 'python2', 'setuptools'] + depends = ['openssl', 'setuptools'] site_packages_name = 'M2Crypto' call_hostpython_via_targetpython = False @@ -35,8 +34,6 @@ def build_compiled_components(self, arch): def get_recipe_env(self, arch): env = super(M2CryptoRecipe, self).get_recipe_env(arch) env['OPENSSL_BUILD_PATH'] = self.get_recipe('openssl', self.ctx).get_build_dir(arch.arch) - # Set linker to use the correct gcc - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' return env diff --git a/pythonforandroid/recipes/msgpack-python/__init__.py b/pythonforandroid/recipes/msgpack-python/__init__.py index f005c338c3..cdd024b922 100644 --- a/pythonforandroid/recipes/msgpack-python/__init__.py +++ b/pythonforandroid/recipes/msgpack-python/__init__.py @@ -4,7 +4,7 @@ class MsgPackRecipe(CythonRecipe): version = '0.4.7' url = 'https://pypi.python.org/packages/source/m/msgpack-python/msgpack-python-{version}.tar.gz' - depends = [('python2', 'python3crystax'), "setuptools"] + depends = ["setuptools"] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/mysqldb/__init__.py b/pythonforandroid/recipes/mysqldb/__init__.py index f04d56683c..f08458567b 100644 --- a/pythonforandroid/recipes/mysqldb/__init__.py +++ b/pythonforandroid/recipes/mysqldb/__init__.py @@ -8,7 +8,7 @@ class MysqldbRecipe(CompiledComponentsPythonRecipe): url = 'https://pypi.python.org/packages/source/M/MySQL-python/MySQL-python-{version}.zip' site_packages_name = 'MySQLdb' - depends = ['python2', 'setuptools', 'libmysqlclient'] + depends = ['setuptools', 'libmysqlclient'] patches = ['override-mysql-config.patch', 'disable-zip.patch'] diff --git a/pythonforandroid/recipes/netifaces/__init__.py b/pythonforandroid/recipes/netifaces/__init__.py index 6c9361a80b..8ad1382025 100644 --- a/pythonforandroid/recipes/netifaces/__init__.py +++ b/pythonforandroid/recipes/netifaces/__init__.py @@ -3,7 +3,7 @@ class NetifacesRecipe(CompiledComponentsPythonRecipe): - version = '0.10.7' + version = '0.10.9' url = 'https://files.pythonhosted.org/packages/source/n/netifaces/netifaces-{version}.tar.gz' diff --git a/pythonforandroid/recipes/numpy/__init__.py b/pythonforandroid/recipes/numpy/__init__.py index 1357689c36..6b6e6b3907 100644 --- a/pythonforandroid/recipes/numpy/__init__.py +++ b/pythonforandroid/recipes/numpy/__init__.py @@ -1,4 +1,5 @@ from pythonforandroid.recipe import CompiledComponentsPythonRecipe +from multiprocessing import cpu_count from os.path import join @@ -7,7 +8,6 @@ class NumpyRecipe(CompiledComponentsPythonRecipe): version = '1.15.1' url = 'https://pypi.python.org/packages/source/n/numpy/numpy-{version}.zip' site_packages_name = 'numpy' - depends = [('python2', 'python3', 'python3crystax')] patches = [ @@ -18,6 +18,16 @@ class NumpyRecipe(CompiledComponentsPythonRecipe): join('patches', 'python-fixes.patch') ] + def build_compiled_components(self, arch): + self.setup_extra_args = ['-j', str(cpu_count())] + super(NumpyRecipe, self).build_compiled_components(arch) + self.setup_extra_args = [] + + def rebuild_compiled_components(self, arch, env): + self.setup_extra_args = ['-j', str(cpu_count())] + super(NumpyRecipe, self).rebuild_compiled_components(arch, env) + self.setup_extra_args = [] + def get_recipe_env(self, arch): env = super(NumpyRecipe, self).get_recipe_env(arch) @@ -44,8 +54,5 @@ def get_recipe_env(self, arch): env['LD'] += flags + ' -shared' return env - def prebuild_arch(self, arch): - super(NumpyRecipe, self).prebuild_arch(arch) - recipe = NumpyRecipe() diff --git a/pythonforandroid/recipes/omemo-backend-signal/__init__.py b/pythonforandroid/recipes/omemo-backend-signal/__init__.py index ba01eec530..c87034ce7d 100644 --- a/pythonforandroid/recipes/omemo-backend-signal/__init__.py +++ b/pythonforandroid/recipes/omemo-backend-signal/__init__.py @@ -7,7 +7,6 @@ class OmemoBackendSignalRecipe(PythonRecipe): url = 'https://pypi.python.org/packages/source/o/omemo-backend-signal/omemo-backend-signal-{version}.tar.gz' site_packages_name = 'omemo-backend-signal' depends = [ - ('python2', 'python3crystax'), 'setuptools', 'protobuf_cpp', 'x3dh', diff --git a/pythonforandroid/recipes/omemo/__init__.py b/pythonforandroid/recipes/omemo/__init__.py index 425ffd0d10..a940105a59 100644 --- a/pythonforandroid/recipes/omemo/__init__.py +++ b/pythonforandroid/recipes/omemo/__init__.py @@ -7,7 +7,6 @@ class OmemoRecipe(PythonRecipe): url = 'https://pypi.python.org/packages/source/O/OMEMO/OMEMO-{version}.tar.gz' site_packages_name = 'omemo' depends = [ - ('python2', 'python3crystax'), 'setuptools', 'x3dh', 'cryptography', diff --git a/pythonforandroid/recipes/opencv/__init__.py b/pythonforandroid/recipes/opencv/__init__.py index 0e51450991..6932bc225c 100644 --- a/pythonforandroid/recipes/opencv/__init__.py +++ b/pythonforandroid/recipes/opencv/__init__.py @@ -1,4 +1,4 @@ -import os +from os.path import join import sh from pythonforandroid.recipe import NDKRecipe from pythonforandroid.toolchain import ( @@ -9,45 +9,127 @@ class OpenCVRecipe(NDKRecipe): - version = '2.4.10.1' - url = 'https://github.com/Itseez/opencv/archive/{version}.zip' - # md5sum = '2ddfa98e867e6611254040df841186dc' + ''' + .. versionchanged:: 0.7.1 + rewrote recipe to support the python bindings (cv2.so) and enable the + build of most of the libraries of the opencv's package, so we can + process images, videos, objects, photos... + ''' + version = '4.0.1' + url = 'https://github.com/opencv/opencv/archive/{version}.zip' depends = ['numpy'] - patches = ['patches/p4a_build-2.4.10.1.patch'] - generated_libraries = ['cv2.so'] + patches = ['patches/p4a_build.patch'] + generated_libraries = [ + 'libopencv_features2d.so', + 'libopencv_imgproc.so', + 'libopencv_stitching.so', + 'libopencv_calib3d.so', + 'libopencv_flann.so', + 'libopencv_ml.so', + 'libopencv_videoio.so', + 'libopencv_core.so', + 'libopencv_highgui.so', + 'libopencv_objdetect.so', + 'libopencv_video.so', + 'libopencv_dnn.so', + 'libopencv_imgcodecs.so', + 'libopencv_photo.so' + ] - def prebuild_arch(self, arch): - self.apply_patches(arch) + def get_lib_dir(self, arch): + return join(self.get_build_dir(arch.arch), 'build', 'lib', arch.arch) def get_recipe_env(self, arch): env = super(OpenCVRecipe, self).get_recipe_env(arch) - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() env['ANDROID_NDK'] = self.ctx.ndk_dir env['ANDROID_SDK'] = self.ctx.sdk_dir - env['SITEPACKAGES_PATH'] = self.ctx.get_site_packages_dir() return env def build_arch(self, arch): - with current_directory(self.get_build_dir(arch.arch)): + build_dir = join(self.get_build_dir(arch.arch), 'build') + shprint(sh.mkdir, '-p', build_dir) + with current_directory(build_dir): env = self.get_recipe_env(arch) - cvsrc = self.get_build_dir(arch.arch) - lib_dir = os.path.join(self.ctx.get_python_install_dir(), "lib") + + python_major = self.ctx.python_recipe.version[0] + python_include_root = self.ctx.python_recipe.include_root(arch.arch) + python_site_packages = self.ctx.get_site_packages_dir() + python_link_root = self.ctx.python_recipe.link_root(arch.arch) + python_link_version = self.ctx.python_recipe.major_minor_version_string + if 'python3' in self.ctx.python_recipe.name: + python_link_version += 'm' + python_library = join(python_link_root, + 'libpython{}.so'.format(python_link_version)) + python_include_numpy = join(python_site_packages, + 'numpy', 'core', 'include') shprint(sh.cmake, - '-DP4A=ON', '-DANDROID_ABI={}'.format(arch.arch), - '-DCMAKE_TOOLCHAIN_FILE={}/platforms/android/android.toolchain.cmake'.format(cvsrc), - '-DPYTHON_INCLUDE_PATH={}/include/python2.7'.format(env['PYTHON_ROOT']), - '-DPYTHON_LIBRARY={}/lib/libpython2.7.so'.format(env['PYTHON_ROOT']), - '-DPYTHON_NUMPY_INCLUDE_DIR={}/numpy/core/include'.format(env['SITEPACKAGES_PATH']), + '-DP4A=ON', + '-DANDROID_ABI={}'.format(arch.arch), + '-DANDROID_STANDALONE_TOOLCHAIN={}'.format(self.ctx.ndk_dir), + '-DANDROID_NATIVE_API_LEVEL={}'.format(self.ctx.ndk_api), '-DANDROID_EXECUTABLE={}/tools/android'.format(env['ANDROID_SDK']), - '-DBUILD_TESTS=OFF', '-DBUILD_PERF_TESTS=OFF', - '-DBUILD_EXAMPLES=OFF', '-DBUILD_ANDROID_EXAMPLES=OFF', - '-DPYTHON_PACKAGES_PATH={}'.format(env['SITEPACKAGES_PATH']), - cvsrc, + + '-DCMAKE_TOOLCHAIN_FILE={}'.format( + join(self.ctx.ndk_dir, 'build', 'cmake', + 'android.toolchain.cmake')), + # Make the linkage with our python library, otherwise we + # will get dlopen error when trying to import cv2's module. + '-DCMAKE_SHARED_LINKER_FLAGS=-L{path} -lpython{version}'.format( + path=python_link_root, + version=python_link_version), + + '-DBUILD_WITH_STANDALONE_TOOLCHAIN=ON', + # Force to build as shared libraries the cv2's dependant + # libs or we will not be able to link with our python + '-DBUILD_SHARED_LIBS=ON', + '-DBUILD_STATIC_LIBS=OFF', + + # Disable some opencv's features + '-DBUILD_opencv_java=OFF', + '-DBUILD_opencv_java_bindings_generator=OFF', + # '-DBUILD_opencv_highgui=OFF', + # '-DBUILD_opencv_imgproc=OFF', + # '-DBUILD_opencv_flann=OFF', + '-DBUILD_TESTS=OFF', + '-DBUILD_PERF_TESTS=OFF', + '-DENABLE_TESTING=OFF', + '-DBUILD_EXAMPLES=OFF', + '-DBUILD_ANDROID_EXAMPLES=OFF', + + # Force to only build our version of python + '-DBUILD_OPENCV_PYTHON{major}=ON'.format(major=python_major), + '-DBUILD_OPENCV_PYTHON{major}=OFF'.format( + major='2' if python_major == '3' else '3'), + + # Force to install the `cv2.so` library directly into + # python's site packages (otherwise the cv2's loader fails + # on finding the cv2.so library) + '-DOPENCV_SKIP_PYTHON_LOADER=ON', + '-DOPENCV_PYTHON{major}_INSTALL_PATH={site_packages}'.format( + major=python_major, site_packages=python_site_packages), + + # Define python's paths for: exe, lib, includes, numpy... + '-DPYTHON_DEFAULT_EXECUTABLE={}'.format(self.ctx.hostpython), + '-DPYTHON{major}_EXECUTABLE={host_python}'.format( + major=python_major, host_python=self.ctx.hostpython), + '-DPYTHON{major}_INCLUDE_PATH={include_path}'.format( + major=python_major, include_path=python_include_root), + '-DPYTHON{major}_LIBRARIES={python_lib}'.format( + major=python_major, python_lib=python_library), + '-DPYTHON{major}_NUMPY_INCLUDE_DIRS={numpy_include}'.format( + major=python_major, numpy_include=python_include_numpy), + '-DPYTHON{major}_PACKAGES_PATH={site_packages}'.format( + major=python_major, site_packages=python_site_packages), + + self.get_build_dir(arch.arch), _env=env) - shprint(sh.make, '-j', str(cpu_count()), 'opencv_python') + shprint(sh.make, '-j' + str(cpu_count()), 'opencv_python' + python_major) + # Install python bindings (cv2.so) shprint(sh.cmake, '-DCOMPONENT=python', '-P', './cmake_install.cmake') - sh.cp('-a', sh.glob('./lib/{}/lib*.so'.format(arch.arch)), lib_dir) + # Copy third party shared libs that we need in our final apk + sh.cp('-a', sh.glob('./lib/{}/lib*.so'.format(arch.arch)), + self.ctx.get_libs_dir(arch.arch)) recipe = OpenCVRecipe() diff --git a/pythonforandroid/recipes/opencv/patches/p4a_build-2.4.10.1.patch b/pythonforandroid/recipes/opencv/patches/p4a_build-2.4.10.1.patch deleted file mode 100644 index a7a60aa3b3..0000000000 --- a/pythonforandroid/recipes/opencv/patches/p4a_build-2.4.10.1.patch +++ /dev/null @@ -1,66 +0,0 @@ -diff --git a/cmake/OpenCVDetectPython.cmake b/cmake/OpenCVDetectPython.cmake -index 31c2c1e..c890917 100644 ---- a/cmake/OpenCVDetectPython.cmake -+++ b/cmake/OpenCVDetectPython.cmake -@@ -36,7 +36,7 @@ if(PYTHON_EXECUTABLE) - unset(PYTHON_VERSION_FULL) - endif() - -- if(NOT ANDROID AND NOT IOS) -+ if(P4A OR NOT ANDROID AND NOT IOS) - ocv_check_environment_variables(PYTHON_LIBRARY PYTHON_INCLUDE_DIR) - if(CMAKE_CROSSCOMPILING) - find_host_package(PythonLibs ${PYTHON_VERSION_MAJOR_MINOR}) -@@ -51,7 +51,7 @@ if(PYTHON_EXECUTABLE) - endif() - endif() - -- if(NOT ANDROID AND NOT IOS) -+ if(P4A OR NOT ANDROID AND NOT IOS) - if(CMAKE_HOST_UNIX) - execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "from distutils.sysconfig import *; print get_python_lib()" - RESULT_VARIABLE PYTHON_CVPY_PROCESS -@@ -117,7 +117,7 @@ if(PYTHON_EXECUTABLE) - OUTPUT_STRIP_TRAILING_WHITESPACE) - endif() - endif() -- endif(NOT ANDROID AND NOT IOS) -+ endif(P4A OR NOT ANDROID AND NOT IOS) - - if(BUILD_DOCS) - find_host_program(SPHINX_BUILD sphinx-build) -diff --git a/modules/python/CMakeLists.txt b/modules/python/CMakeLists.txt -index 3c0f2fd..7ba234a 100644 ---- a/modules/python/CMakeLists.txt -+++ b/modules/python/CMakeLists.txt -@@ -5,7 +5,7 @@ - if(WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Debug") - ocv_module_disable(python) - endif() --if(ANDROID OR IOS OR NOT PYTHONLIBS_FOUND OR NOT PYTHON_USE_NUMPY) -+if(ANDROID AND NOT P4A OR IOS OR NOT PYTHONLIBS_FOUND OR NOT PYTHON_USE_NUMPY) - ocv_module_disable(python) - endif() - -diff --git a/modules/androidcamera/src/camera_activity.cpp b/modules/androidcamera/src/camera_activity.cpp -index 84db3e1..4222526 100644 ---- a/modules/androidcamera/src/camera_activity.cpp -+++ b/modules/androidcamera/src/camera_activity.cpp -@@ -7,6 +7,7 @@ - #include - #include - #include -+#include - #include - #include "camera_activity.hpp" - #include "camera_wrapper.h" -@@ -342,6 +343,8 @@ std::string CameraWrapperConnector::getPathLibFolder() - - char* pathEnd = strrchr(pathBegin, '/'); - pathEnd[1] = 0; -+ pathBegin = realpath((std::string(pathBegin)+"../../../../lib").c_str(), lineBuf); -+ pathBegin = strcat(pathBegin, "/"); - - LOGD("Libraries folder found: %s", pathBegin); - - diff --git a/pythonforandroid/recipes/opencv/patches/p4a_build.patch b/pythonforandroid/recipes/opencv/patches/p4a_build.patch new file mode 100644 index 0000000000..fd60c01d38 --- /dev/null +++ b/pythonforandroid/recipes/opencv/patches/p4a_build.patch @@ -0,0 +1,33 @@ +This patch allow that the opencv's build command correctly detects our version +of python, so we can successfully build the python bindings (cv2.so) +--- opencv-4.0.1/cmake/OpenCVDetectPython.cmake.orig 2018-12-22 08:03:30.000000000 +0100 ++++ opencv-4.0.1/cmake/OpenCVDetectPython.cmake 2019-01-31 11:33:10.896502978 +0100 +@@ -175,7 +175,7 @@ if(NOT ${found}) + endif() + endif() + +- if(NOT ANDROID AND NOT IOS) ++ if(P4A OR NOT ANDROID AND NOT IOS) + if(CMAKE_HOST_UNIX) + execute_process(COMMAND ${_executable} -c "from distutils.sysconfig import *; print(get_python_lib())" + RESULT_VARIABLE _cvpy_process +@@ -244,7 +244,7 @@ if(NOT ${found}) + OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() + endif() +- endif(NOT ANDROID AND NOT IOS) ++ endif(P4A OR NOT ANDROID AND NOT IOS) + endif() + + # Export return values +--- opencv-4.0.1/modules/python/CMakeLists.txt.orig 2018-12-22 08:03:30.000000000 +0100 ++++ opencv-4.0.1/modules/python/CMakeLists.txt 2019-01-31 11:47:17.100494908 +0100 +@@ -3,7 +3,7 @@ + # ---------------------------------------------------------------------------- + if(DEFINED OPENCV_INITIAL_PASS) # OpenCV build + +-if(ANDROID OR APPLE_FRAMEWORK OR WINRT) ++if(ANDROID AND NOT P4A OR APPLE_FRAMEWORK OR WINRT) + ocv_module_disable_(python2) + ocv_module_disable_(python3) + return() diff --git a/pythonforandroid/recipes/pil/__init__.py b/pythonforandroid/recipes/pil/__init__.py index 1b99dabfb4..f3ad2f42ef 100644 --- a/pythonforandroid/recipes/pil/__init__.py +++ b/pythonforandroid/recipes/pil/__init__.py @@ -8,7 +8,7 @@ class PILRecipe(CompiledComponentsPythonRecipe): name = 'pil' version = '1.1.7' url = 'http://effbot.org/downloads/Imaging-{version}.tar.gz' - depends = [('python2', 'python2legacy'), 'png', 'jpeg', 'setuptools'] + depends = ['png', 'jpeg', 'setuptools'] opt_depends = ['freetype'] site_packages_name = 'PIL' diff --git a/pythonforandroid/recipes/preppy/__init__.py b/pythonforandroid/recipes/preppy/__init__.py index 495a58dd2c..40afd681ba 100644 --- a/pythonforandroid/recipes/preppy/__init__.py +++ b/pythonforandroid/recipes/preppy/__init__.py @@ -4,7 +4,7 @@ class PreppyRecipe(PythonRecipe): version = '27b7085' url = 'https://bitbucket.org/rptlab/preppy/get/{version}.tar.gz' - depends = [('python2', 'python3crystax')] + depends = [] patches = ['fix-setup.patch'] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/protobuf_cpp/__init__.py b/pythonforandroid/recipes/protobuf_cpp/__init__.py index 6cfa674b57..d82fcf7206 100644 --- a/pythonforandroid/recipes/protobuf_cpp/__init__.py +++ b/pythonforandroid/recipes/protobuf_cpp/__init__.py @@ -1,7 +1,7 @@ from pythonforandroid.recipe import PythonRecipe from pythonforandroid.logger import shprint, info_notify from pythonforandroid.util import current_directory, shutil -from os.path import exists, join, dirname +from os.path import exists, join import sh from multiprocessing import cpu_count from pythonforandroid.toolchain import info @@ -11,7 +11,7 @@ class ProtobufCppRecipe(PythonRecipe): name = 'protobuf_cpp' - version = '3.5.1' + version = '3.6.1' url = 'https://github.com/google/protobuf/releases/download/v{version}/protobuf-python-{version}.tar.gz' call_hostpython_via_targetpython = False depends = ['cffi', 'setuptools'] @@ -20,6 +20,12 @@ class ProtobufCppRecipe(PythonRecipe): def prebuild_arch(self, arch): super(ProtobufCppRecipe, self).prebuild_arch(arch) + + patch_mark = join(self.get_build_dir(arch.arch), '.protobuf-patched') + if self.ctx.python_recipe.name == 'python3' and not exists(patch_mark): + self.apply_patch('fix-python3-compatibility.patch', arch.arch) + shprint(sh.touch, patch_mark) + # During building, host needs to transpile .proto files to .py # ideally with the same version as protobuf runtime, or with an older one. # Because protoc is compiled for target (i.e. Android), we need an other binary @@ -100,34 +106,18 @@ def install_python_package(self, arch): with current_directory(join(self.get_build_dir(arch.arch), 'python')): hostpython = sh.Command(self.hostpython_location) - if self.ctx.python_recipe.from_crystax: - hpenv = env.copy() - shprint(hostpython, 'setup.py', 'install', '-O2', - '--root={}'.format(self.ctx.get_python_install_dir()), - '--install-lib=.', - '--cpp_implementation', - _env=hpenv, *self.setup_extra_args) - else: - hppath = join(dirname(self.hostpython_location), 'Lib', - 'site-packages') - hpenv = env.copy() - if 'PYTHONPATH' in hpenv: - hpenv['PYTHONPATH'] = ':'.join([hppath] + - hpenv['PYTHONPATH'].split(':')) - else: - hpenv['PYTHONPATH'] = hppath - shprint(hostpython, 'setup.py', 'install', '-O2', - '--root={}'.format(self.ctx.get_python_install_dir()), - '--install-lib=lib/python2.7/site-packages', - '--cpp_implementation', - _env=hpenv, *self.setup_extra_args) + hpenv = env.copy() + shprint(hostpython, 'setup.py', 'install', '-O2', + '--root={}'.format(self.ctx.get_python_install_dir()), + '--install-lib=.', + '--cpp_implementation', + _env=hpenv, *self.setup_extra_args) def get_recipe_env(self, arch): env = super(ProtobufCppRecipe, self).get_recipe_env(arch) if self.protoc_dir is not None: # we need protoc with binary for host platform env['PROTOC'] = join(self.protoc_dir, 'bin', 'protoc') - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() env['TARGET_OS'] = 'OS_ANDROID_CROSSCOMPILE' env['CFLAGS'] += ( ' -I' + self.ctx.ndk_dir + '/platforms/android-' + @@ -136,17 +126,17 @@ def get_recipe_env(self, arch): ' -I' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/include' + ' -I' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + - self.ctx.toolchain_version + '/libs/' + arch.arch + '/include' + - ' -I' + env['PYTHON_ROOT'] + '/include/python2.7') + self.ctx.toolchain_version + '/libs/' + arch.arch + '/include') + env['CFLAGS'] += ' -std=gnu++11' env['CXXFLAGS'] = env['CFLAGS'] env['CXXFLAGS'] += ' -frtti' env['CXXFLAGS'] += ' -fexceptions' env['LDFLAGS'] += ( ' -L' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + - '/libs/' + arch.arch + ' -lgnustl_shared -lpython2.7 -landroid -llog') + '/libs/' + arch.arch) + env['LIBS'] = env.get('LIBS', '') + ' -lgnustl_shared -landroid -llog' - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' return env diff --git a/pythonforandroid/recipes/protobuf_cpp/fix-python3-compatibility.patch b/pythonforandroid/recipes/protobuf_cpp/fix-python3-compatibility.patch new file mode 100644 index 0000000000..e77debaa61 --- /dev/null +++ b/pythonforandroid/recipes/protobuf_cpp/fix-python3-compatibility.patch @@ -0,0 +1,91 @@ +From 539bc017a62f91bdf7c547b58948cb5a2f59d918 Mon Sep 17 00:00:00 2001 +From: Ben Webb +Date: Thu, 12 Jul 2018 10:58:10 -0700 +Subject: [PATCH] Add Python 3.7 compatibility (#4862) + +Compilation of Python wrappers fails with Python 3.7 because +the Python folks changed their C API such that +PyUnicode_AsUTF8AndSize() now returns a const char* rather +than a char*. Add a patch to work around. Relates #4086. +--- + python/google/protobuf/pyext/descriptor.cc | 2 +- + python/google/protobuf/pyext/descriptor_containers.cc | 2 +- + python/google/protobuf/pyext/descriptor_pool.cc | 2 +- + python/google/protobuf/pyext/extension_dict.cc | 2 +- + python/google/protobuf/pyext/message.cc | 4 ++-- + 5 files changed, 6 insertions(+), 6 deletions(-) + +diff --git a/python/google/protobuf/pyext/descriptor.cc b/python/google/protobuf/pyext/descriptor.cc +index 8af0cb1289..19a1c38a62 100644 +--- a/python/google/protobuf/pyext/descriptor.cc ++++ b/python/google/protobuf/pyext/descriptor.cc +@@ -56,7 +56,7 @@ + #endif + #define PyString_AsStringAndSize(ob, charpp, sizep) \ + (PyUnicode_Check(ob)? \ +- ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ ++ ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ + PyBytes_AsStringAndSize(ob, (charpp), (sizep))) + #endif + +diff --git a/python/google/protobuf/pyext/descriptor_containers.cc b/python/google/protobuf/pyext/descriptor_containers.cc +index bc007f7efa..0153664f50 100644 +--- a/python/google/protobuf/pyext/descriptor_containers.cc ++++ b/python/google/protobuf/pyext/descriptor_containers.cc +@@ -66,7 +66,7 @@ + #endif + #define PyString_AsStringAndSize(ob, charpp, sizep) \ + (PyUnicode_Check(ob)? \ +- ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ ++ ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ + PyBytes_AsStringAndSize(ob, (charpp), (sizep))) + #endif + +diff --git a/python/google/protobuf/pyext/descriptor_pool.cc b/python/google/protobuf/pyext/descriptor_pool.cc +index 95882aeb35..962accc6e9 100644 +--- a/python/google/protobuf/pyext/descriptor_pool.cc ++++ b/python/google/protobuf/pyext/descriptor_pool.cc +@@ -48,7 +48,7 @@ + #endif + #define PyString_AsStringAndSize(ob, charpp, sizep) \ + (PyUnicode_Check(ob)? \ +- ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ ++ ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ + PyBytes_AsStringAndSize(ob, (charpp), (sizep))) + #endif + +diff --git a/python/google/protobuf/pyext/extension_dict.cc b/python/google/protobuf/pyext/extension_dict.cc +index 018b5c2c49..174c5470c2 100644 +--- a/python/google/protobuf/pyext/extension_dict.cc ++++ b/python/google/protobuf/pyext/extension_dict.cc +@@ -53,7 +53,7 @@ + #endif + #define PyString_AsStringAndSize(ob, charpp, sizep) \ + (PyUnicode_Check(ob)? \ +- ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ ++ ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ + PyBytes_AsStringAndSize(ob, (charpp), (sizep))) + #endif + +diff --git a/python/google/protobuf/pyext/message.cc b/python/google/protobuf/pyext/message.cc +index 5893533adf..31094b7e10 100644 +--- a/python/google/protobuf/pyext/message.cc ++++ b/python/google/protobuf/pyext/message.cc +@@ -79,7 +79,7 @@ + (PyUnicode_Check(ob)? PyUnicode_AsUTF8(ob): PyBytes_AsString(ob)) + #define PyString_AsStringAndSize(ob, charpp, sizep) \ + (PyUnicode_Check(ob)? \ +- ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ ++ ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ + PyBytes_AsStringAndSize(ob, (charpp), (sizep))) + #endif + #endif +@@ -1529,7 +1529,7 @@ PyObject* HasField(CMessage* self, PyObject* arg) { + return NULL; + } + #else +- field_name = PyUnicode_AsUTF8AndSize(arg, &size); ++ field_name = const_cast(PyUnicode_AsUTF8AndSize(arg, &size)); + if (!field_name) { + return NULL; + } diff --git a/pythonforandroid/recipes/psycopg2/__init__.py b/pythonforandroid/recipes/psycopg2/__init__.py index f26f79109d..75a07374e5 100644 --- a/pythonforandroid/recipes/psycopg2/__init__.py +++ b/pythonforandroid/recipes/psycopg2/__init__.py @@ -4,10 +4,14 @@ class Psycopg2Recipe(PythonRecipe): + """ + Requires `libpq-dev` system dependency e.g. for `pg_config` binary. + """ version = 'latest' url = 'http://initd.org/psycopg/tarballs/psycopg2-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'libpq'] + depends = ['libpq'] site_packages_name = 'psycopg2' + call_hostpython_via_targetpython = False def prebuild_arch(self, arch): libdir = self.ctx.get_libs_dir(arch.arch) diff --git a/pythonforandroid/recipes/pyaml/__init__.py b/pythonforandroid/recipes/pyaml/__init__.py index ee24eb8f9c..8440175707 100644 --- a/pythonforandroid/recipes/pyaml/__init__.py +++ b/pythonforandroid/recipes/pyaml/__init__.py @@ -4,7 +4,7 @@ class PyamlRecipe(PythonRecipe): version = "15.8.2" url = 'https://pypi.python.org/packages/source/p/pyaml/pyaml-{version}.tar.gz' - depends = [('python2', 'python3crystax'), "setuptools"] + depends = ["setuptools"] site_packages_name = 'yaml' call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/pyasn1/__init__.py b/pythonforandroid/recipes/pyasn1/__init__.py index 64b007d8ce..dec21e8010 100644 --- a/pythonforandroid/recipes/pyasn1/__init__.py +++ b/pythonforandroid/recipes/pyasn1/__init__.py @@ -5,7 +5,7 @@ class PyASN1Recipe(PythonRecipe): version = '0.1.8' url = 'https://pypi.python.org/packages/source/p/pyasn1/pyasn1-{version}.tar.gz' - depends = [('python2', 'python3crystax')] + depends = [] recipe = PyASN1Recipe() diff --git a/pythonforandroid/recipes/pycryptodome/__init__.py b/pythonforandroid/recipes/pycryptodome/__init__.py index 43e28fc72f..9418600a29 100644 --- a/pythonforandroid/recipes/pycryptodome/__init__.py +++ b/pythonforandroid/recipes/pycryptodome/__init__.py @@ -2,15 +2,9 @@ class PycryptodomeRecipe(PythonRecipe): - version = '3.4.6' + version = '3.6.3' url = 'https://github.com/Legrandin/pycryptodome/archive/v{version}.tar.gz' depends = ['setuptools', 'cffi'] - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(PycryptodomeRecipe, self).get_recipe_env(arch, with_flags_in_cc) - # sets linker to use the correct gcc (cross compiler) - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' - return env - recipe = PycryptodomeRecipe() diff --git a/pythonforandroid/recipes/pyethereum/__init__.py b/pythonforandroid/recipes/pyethereum/__init__.py index f08c073308..d18ad8ea0f 100644 --- a/pythonforandroid/recipes/pyethereum/__init__.py +++ b/pythonforandroid/recipes/pyethereum/__init__.py @@ -6,7 +6,7 @@ class PyethereumRecipe(PythonRecipe): url = 'https://github.com/ethereum/pyethereum/archive/{version}.tar.gz' depends = [ - 'python2', 'setuptools', 'pycryptodome', 'pysha3', 'ethash', 'scrypt' + 'setuptools', 'pycryptodome', 'pysha3', 'ethash', 'scrypt' ] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/pygame/__init__.py b/pythonforandroid/recipes/pygame/__init__.py index 82fac2eac5..981fa445fb 100644 --- a/pythonforandroid/recipes/pygame/__init__.py +++ b/pythonforandroid/recipes/pygame/__init__.py @@ -68,7 +68,6 @@ def build_arch(self, arch): shprint(sh.find, build_lib[0], '-name', '*.o', '-exec', env['STRIP'], '{}', ';') - python_install_path = join(self.ctx.build_dir, 'python-install') warning('Should remove pygame tests etc. here, but skipping for now') diff --git a/pythonforandroid/recipes/pygame_bootstrap_components/__init__.py b/pythonforandroid/recipes/pygame_bootstrap_components/__init__.py index f6682d2b3a..f18dd5bd26 100644 --- a/pythonforandroid/recipes/pygame_bootstrap_components/__init__.py +++ b/pythonforandroid/recipes/pygame_bootstrap_components/__init__.py @@ -9,9 +9,10 @@ class PygameJNIComponentsRecipe(BootstrapNDKRecipe): version = 'master' url = 'https://github.com/kivy/p4a-pygame-bootstrap-components/archive/{version}.zip' dir_name = 'bootstrap_components' + patches = ['jpeg-ndk15-plus.patch'] def prebuild_arch(self, arch): - super(PygameJNIComponentsRecipe, self).postbuild_arch(arch) + super(PygameJNIComponentsRecipe, self).prebuild_arch(arch) info('Unpacking pygame bootstrap JNI dir components') with current_directory(self.get_build_container_dir(arch)): @@ -25,11 +26,9 @@ def prebuild_arch(self, arch): info('Unpacking was successful, deleting original container dir') shprint(sh.rm, '-rf', self.get_build_dir(arch)) - info('Applying jpeg assembler patch') - ndk_15_plus_patch = join(self.get_recipe_dir(), 'jpeg-ndk15-plus.patch') - shprint(sh.patch, '-t', '-d', - join(self.get_build_container_dir(arch), 'jpeg'), '-p1', - '-i', ndk_15_plus_patch, _tail=10) + def apply_patches(self, arch, build_dir=None): + super(PygameJNIComponentsRecipe, self).apply_patches( + arch, build_dir=self.get_build_container_dir(arch.arch)) recipe = PygameJNIComponentsRecipe() diff --git a/pythonforandroid/recipes/pygame_bootstrap_components/jpeg-ndk15-plus.patch b/pythonforandroid/recipes/pygame_bootstrap_components/jpeg-ndk15-plus.patch index e12942223b..9992084fc4 100644 --- a/pythonforandroid/recipes/pygame_bootstrap_components/jpeg-ndk15-plus.patch +++ b/pythonforandroid/recipes/pygame_bootstrap_components/jpeg-ndk15-plus.patch @@ -2,8 +2,8 @@ The distributed jpeg has troubles to be build with newer ndks, starting from the introduction of the `unified headers` (ndk > 15). This patch allow us to build the distributed `external jpeg` in sdl package, got the solution in here: https://github.com/oNaiPs/droidVncServer/issues/53 ---- jpeg/Android.mk.orig 2015-06-21 15:14:54.000000000 +0200 -+++ jpeg/Android.mk 2019-01-14 10:57:06.384806168 +0100 +--- jni/jpeg/Android.mk.orig 2015-06-21 15:14:54.000000000 +0200 ++++ jni/jpeg/Android.mk 2019-01-14 10:57:06.384806168 +0100 @@ -20,7 +20,7 @@ endif @@ -13,8 +13,8 @@ https://github.com/oNaiPs/droidVncServer/issues/53 ifeq ($(strip $(ANDROID_JPEG_NO_ASSEMBLER)),true) LOCAL_SRC_FILES += jidctint.c jidctfst.c - --- jpeg/jidctfst.S.orig 2019-01-14 11:00:38.000000000 +0100 -+++ jpeg/jidctfst.S 2019-01-14 11:00:56.844803970 +0100 +--- jni/jpeg/jidctfst.S.orig 2019-01-14 11:00:38.000000000 +0100 ++++ jni/jpeg/jidctfst.S 2019-01-14 11:00:56.844803970 +0100 @@ -63,7 +63,7 @@ diff --git a/pythonforandroid/recipes/pyicu/__init__.py b/pythonforandroid/recipes/pyicu/__init__.py index 65a7d69fe0..98ec7b7979 100644 --- a/pythonforandroid/recipes/pyicu/__init__.py +++ b/pythonforandroid/recipes/pyicu/__init__.py @@ -8,7 +8,7 @@ class PyICURecipe(CompiledComponentsPythonRecipe): version = '1.9.2' url = 'https://pypi.python.org/packages/source/P/PyICU/PyICU-{version}.tar.gz' - depends = [('python2', 'python3crystax'), "icu"] + depends = ["icu"] patches = ['locale.patch', 'icu.patch'] def get_recipe_env(self, arch): diff --git a/pythonforandroid/recipes/pyleveldb/__init__.py b/pythonforandroid/recipes/pyleveldb/__init__.py index f0913b0662..65f17cebe5 100644 --- a/pythonforandroid/recipes/pyleveldb/__init__.py +++ b/pythonforandroid/recipes/pyleveldb/__init__.py @@ -6,13 +6,12 @@ class PyLevelDBRecipe(CompiledComponentsPythonRecipe): version = '0.193' url = 'https://pypi.python.org/packages/source/l/leveldb/leveldb-{version}.tar.gz' - depends = ['snappy', 'leveldb', 'hostpython2', 'python2', 'setuptools'] + depends = ['snappy', 'leveldb', ('hostpython2', 'hostpython3'), 'setuptools'] patches = ['bindings-only.patch'] call_hostpython_via_targetpython = False # Due to setuptools site_packages_name = 'leveldb' def build_arch(self, arch): - env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): # Remove source in this pypi package sh.rm('-rf', 'leveldb', 'leveldb.egg-info', 'snappy') diff --git a/pythonforandroid/recipes/pymunk/__init__.py b/pythonforandroid/recipes/pymunk/__init__.py index 7aeb366c05..b72b85b09b 100644 --- a/pythonforandroid/recipes/pymunk/__init__.py +++ b/pythonforandroid/recipes/pymunk/__init__.py @@ -4,8 +4,8 @@ class PymunkRecipe(CompiledComponentsPythonRecipe): name = "pymunk" - version = '5.2.0' - url = 'https://pypi.python.org/packages/5e/bd/e67edcffdee3d0a1e3ebf0050bb9746a61d616f5502ceedddf0f7fd0a896/pymunk-5.2.0.zip' + version = '5.3.2' + url = 'https://pypi.python.org/packages/source/p/pymunk/pymunk-{version}.zip' depends = ['cffi', 'setuptools'] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/pynacl/__init__.py b/pythonforandroid/recipes/pynacl/__init__.py index bde45a6b81..eb9ca2df50 100644 --- a/pythonforandroid/recipes/pynacl/__init__.py +++ b/pythonforandroid/recipes/pynacl/__init__.py @@ -7,7 +7,7 @@ class PyNaCLRecipe(CompiledComponentsPythonRecipe): version = '1.3.0' url = 'https://pypi.python.org/packages/source/P/PyNaCl/PyNaCl-{version}.tar.gz' - depends = ['hostpython2', 'python2', 'six', 'setuptools', 'cffi', 'libsodium'] + depends = [('hostpython2', 'hostpython3'), 'six', 'setuptools', 'cffi', 'libsodium'] call_hostpython_via_targetpython = False def get_recipe_env(self, arch): diff --git a/pythonforandroid/recipes/pyogg/__init__.py b/pythonforandroid/recipes/pyogg/__init__.py index 340785f1db..70ea435c7b 100644 --- a/pythonforandroid/recipes/pyogg/__init__.py +++ b/pythonforandroid/recipes/pyogg/__init__.py @@ -5,7 +5,7 @@ class PyOggRecipe(PythonRecipe): version = '0.6.4a1' url = 'https://files.pythonhosted.org/packages/source/p/pyogg/PyOgg-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'libogg', 'libvorbis', 'setuptools'] + depends = ['libogg', 'libvorbis', 'setuptools'] patches = [join('patches', 'fix-find-lib.patch')] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/pyopenal/__init__.py b/pythonforandroid/recipes/pyopenal/__init__.py index 2c2dac45c8..c42cd09652 100644 --- a/pythonforandroid/recipes/pyopenal/__init__.py +++ b/pythonforandroid/recipes/pyopenal/__init__.py @@ -5,7 +5,7 @@ class PyOpenALRecipe(PythonRecipe): version = '0.7.3a1' url = 'https://files.pythonhosted.org/packages/source/p/pyopenal/PyOpenAL-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'openal', 'numpy', 'setuptools'] + depends = ['openal', 'numpy', 'setuptools'] patches = [join('patches', 'fix-find-lib.patch')] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/pyopenssl/__init__.py b/pythonforandroid/recipes/pyopenssl/__init__.py index 8e43753176..65be3080ef 100644 --- a/pythonforandroid/recipes/pyopenssl/__init__.py +++ b/pythonforandroid/recipes/pyopenssl/__init__.py @@ -5,7 +5,7 @@ class PyOpenSSLRecipe(PythonRecipe): version = '16.0.0' url = 'https://pypi.python.org/packages/source/p/pyOpenSSL/pyOpenSSL-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'openssl', 'setuptools'] + depends = ['openssl', 'setuptools'] site_packages_name = 'OpenSSL' call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/pyproj/__init__.py b/pythonforandroid/recipes/pyproj/__init__.py index e8d8800191..71b272d136 100644 --- a/pythonforandroid/recipes/pyproj/__init__.py +++ b/pythonforandroid/recipes/pyproj/__init__.py @@ -4,7 +4,7 @@ class PyProjRecipe(CythonRecipe): version = '1.9.5.1' url = 'https://github.com/jswhit/pyproj/archive/master.zip' - depends = ['python2', 'setuptools'] + depends = ['setuptools'] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/pyrxp/__init__.py b/pythonforandroid/recipes/pyrxp/__init__.py index b0796ac643..09b1804a83 100644 --- a/pythonforandroid/recipes/pyrxp/__init__.py +++ b/pythonforandroid/recipes/pyrxp/__init__.py @@ -4,7 +4,7 @@ class PyRXPURecipe(CompiledComponentsPythonRecipe): version = '2a02cecc87b9' url = 'https://bitbucket.org/rptlab/pyrxp/get/{version}.tar.gz' - depends = [('python2', 'python3crystax')] + depends = [] patches = [] diff --git a/pythonforandroid/recipes/pysha3/__init__.py b/pythonforandroid/recipes/pysha3/__init__.py index df002fd147..35cfff84a8 100644 --- a/pythonforandroid/recipes/pysha3/__init__.py +++ b/pythonforandroid/recipes/pysha3/__init__.py @@ -6,14 +6,16 @@ class Pysha3Recipe(PythonRecipe): version = '1.0.2' url = 'https://github.com/tiran/pysha3/archive/{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] + depends = ['setuptools'] + call_hostpython_via_targetpython = False def get_recipe_env(self, arch=None, with_flags_in_cc=True): env = super(Pysha3Recipe, self).get_recipe_env(arch, with_flags_in_cc) - # sets linker to use the correct gcc (cross compiler) - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' # CFLAGS may only be used to specify C compiler flags, for macro definitions use CPPFLAGS - env['CPPFLAGS'] = env['CFLAGS'] + ' -I{}/sources/python/3.5/include/python/'.format(self.ctx.ndk_dir) + env['CPPFLAGS'] = env['CFLAGS'] + if self.ctx.ndk == 'crystax': + env['CPPFLAGS'] += ' -I{}/sources/python/{}/include/python/'.format( + self.ctx.ndk_dir, self.ctx.python_recipe.version[0:3]) env['CFLAGS'] = '' # LDFLAGS may only be used to specify linker flags, for libraries use LIBS env['LDFLAGS'] = env['LDFLAGS'].replace('-lm', '').replace('-lcrystax', '') diff --git a/pythonforandroid/recipes/python2/__init__.py b/pythonforandroid/recipes/python2/__init__.py index 064c6609f5..beba2b65a0 100644 --- a/pythonforandroid/recipes/python2/__init__.py +++ b/pythonforandroid/recipes/python2/__init__.py @@ -47,6 +47,8 @@ class Python2Recipe(GuestPythonRecipe): '--prefix={prefix}', '--exec-prefix={exec_prefix}') + compiled_extension = '.pyo' + def prebuild_arch(self, arch): super(Python2Recipe, self).prebuild_arch(arch) patch_mark = join(self.get_build_dir(arch.arch), '.openssl-patched') @@ -56,6 +58,11 @@ def prebuild_arch(self, arch): def set_libs_flags(self, env, arch): env = super(Python2Recipe, self).set_libs_flags(env, arch) + if 'libffi' in self.ctx.recipe_build_order: + # For python2 we need to tell configure that we want to use our + # compiled libffi, this step is not necessary for python3. + self.configure_args += ('--with-system-ffi',) + if 'openssl' in self.ctx.recipe_build_order: recipe = Recipe.get_recipe('openssl', self.ctx) openssl_build = recipe.get_build_dir(arch.arch) diff --git a/pythonforandroid/recipes/python3/__init__.py b/pythonforandroid/recipes/python3/__init__.py index bf29a23537..e7a9e5403f 100644 --- a/pythonforandroid/recipes/python3/__init__.py +++ b/pythonforandroid/recipes/python3/__init__.py @@ -21,7 +21,9 @@ class Python3Recipe(GuestPythonRecipe): url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz' name = 'python3' - depends = ['hostpython3'] + patches = ["patches/fix-ctypes-util-find-library.patch"] + + depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi'] conflicts = ['python3crystax', 'python2', 'python2legacy'] configure_args = ( diff --git a/pythonforandroid/recipes/python3/patches/fix-ctypes-util-find-library.patch b/pythonforandroid/recipes/python3/patches/fix-ctypes-util-find-library.patch new file mode 100644 index 0000000000..ac75c83919 --- /dev/null +++ b/pythonforandroid/recipes/python3/patches/fix-ctypes-util-find-library.patch @@ -0,0 +1,23 @@ +diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py +--- a/Lib/ctypes/util.py ++++ b/Lib/ctypes/util.py +@@ -67,4 +67,19 @@ + return fname + return None + ++# This patch overrides the find_library to look in the right places on ++# Android ++if True: ++ def find_library(name): ++ # Check the user app libs and system libraries directory: ++ app_root = os.path.normpath(os.path.abspath('../../')) ++ lib_search_dirs = [os.path.join(app_root, 'lib'), "/system/lib"] ++ for lib_dir in lib_search_dirs: ++ for filename in os.listdir(lib_dir): ++ if filename.endswith('.so') and ( ++ filename.startswith("lib" + name + ".") or ++ filename.startswith(name + ".")): ++ return os.path.join(lib_dir, filename) ++ return None ++ + elif os.name == "posix" and sys.platform == "darwin": diff --git a/pythonforandroid/recipes/pytz/__init__.py b/pythonforandroid/recipes/pytz/__init__.py index 6d45cc6f0e..12133bc98e 100644 --- a/pythonforandroid/recipes/pytz/__init__.py +++ b/pythonforandroid/recipes/pytz/__init__.py @@ -6,7 +6,7 @@ class PytzRecipe(PythonRecipe): version = '2015.7' url = 'https://pypi.python.org/packages/source/p/pytz/pytz-{version}.tar.bz2' - depends = [('python2', 'python3crystax')] + depends = [] call_hostpython_via_targetpython = False install_in_hostpython = True diff --git a/pythonforandroid/recipes/pyusb/__init__.py b/pythonforandroid/recipes/pyusb/__init__.py index 3ef7a88073..0a0fbc72b4 100644 --- a/pythonforandroid/recipes/pyusb/__init__.py +++ b/pythonforandroid/recipes/pyusb/__init__.py @@ -5,7 +5,7 @@ class PyusbRecipe(PythonRecipe): name = 'pyusb' version = '1.0.0b1' url = 'https://pypi.python.org/packages/source/p/pyusb/pyusb-{version}.tar.gz' - depends = [('python2', 'python3crystax')] + depends = [] site_packages_name = 'usb' patches = ['fix-android.patch'] diff --git a/pythonforandroid/recipes/pyyaml/__init__.py b/pythonforandroid/recipes/pyyaml/__init__.py index 4ad827964e..fcd15d329f 100644 --- a/pythonforandroid/recipes/pyyaml/__init__.py +++ b/pythonforandroid/recipes/pyyaml/__init__.py @@ -4,7 +4,7 @@ class PyYamlRecipe(PythonRecipe): version = "3.12" url = 'http://pyyaml.org/download/pyyaml/PyYAML-{version}.tar.gz' - depends = [('python2', 'python3crystax'), "setuptools"] + depends = ["setuptools"] site_packages_name = 'pyyaml' diff --git a/pythonforandroid/recipes/pyzbar/__init__.py b/pythonforandroid/recipes/pyzbar/__init__.py new file mode 100644 index 0000000000..ccfcd9b2ea --- /dev/null +++ b/pythonforandroid/recipes/pyzbar/__init__.py @@ -0,0 +1,26 @@ +from os.path import join +from pythonforandroid.recipe import PythonRecipe + + +class PyZBarRecipe(PythonRecipe): + + version = '0.1.7' + + url = 'https://github.com/NaturalHistoryMuseum/pyzbar/archive/v{version}.tar.gz' # noqa + + call_hostpython_via_targetpython = False + + depends = ['setuptools', 'libzbar'] + + def get_recipe_env(self, arch=None, with_flags_in_cc=True): + env = super(PyZBarRecipe, self).get_recipe_env(arch, with_flags_in_cc) + libzbar = self.get_recipe('libzbar', self.ctx) + libzbar_dir = libzbar.get_build_dir(arch.arch) + env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() + env['CFLAGS'] += ' -I' + join(libzbar_dir, 'include') + env['LDFLAGS'] += ' -L' + join(libzbar_dir, 'zbar', '.libs') + env['LIBS'] = env.get('LIBS', '') + ' -landroid -lzbar' + return env + + +recipe = PyZBarRecipe() diff --git a/pythonforandroid/recipes/pyzmq/__init__.py b/pythonforandroid/recipes/pyzmq/__init__.py index c4a79ffa82..5f9614ddb8 100644 --- a/pythonforandroid/recipes/pyzmq/__init__.py +++ b/pythonforandroid/recipes/pyzmq/__init__.py @@ -13,7 +13,7 @@ class PyZMQRecipe(CythonRecipe): version = 'master' url = 'https://github.com/zeromq/pyzmq/archive/{version}.zip' site_packages_name = 'zmq' - depends = ['python2', 'libzmq'] + depends = ['libzmq'] cython_args = ['-Izmq/utils', '-Izmq/backend/cython', '-Izmq/devices'] diff --git a/pythonforandroid/recipes/regex/__init__.py b/pythonforandroid/recipes/regex/__init__.py index 13f8c5c086..9533905303 100644 --- a/pythonforandroid/recipes/regex/__init__.py +++ b/pythonforandroid/recipes/regex/__init__.py @@ -6,7 +6,7 @@ class RegexRecipe(CompiledComponentsPythonRecipe): version = '2017.07.28' url = 'https://pypi.python.org/packages/d1/23/5fa829706ee1d4452552eb32e0bfc1039553e01f50a8754c6f7152e85c1b/regex-{version}.tar.gz' - depends = ['python2', 'setuptools'] + depends = ['setuptools'] recipe = RegexRecipe() diff --git a/pythonforandroid/recipes/reportlab/__init__.py b/pythonforandroid/recipes/reportlab/__init__.py index da07e58156..d5e8001e46 100644 --- a/pythonforandroid/recipes/reportlab/__init__.py +++ b/pythonforandroid/recipes/reportlab/__init__.py @@ -9,6 +9,7 @@ class ReportLabRecipe(CompiledComponentsPythonRecipe): version = 'c088826211ca' url = 'https://bitbucket.org/rptlab/reportlab/get/{version}.tar.gz' depends = ['freetype'] + call_hostpython_via_targetpython = False def prebuild_arch(self, arch): if not self.is_patched(arch): diff --git a/pythonforandroid/recipes/scrypt/__init__.py b/pythonforandroid/recipes/scrypt/__init__.py index 17a4ef5bc0..26b8048a05 100644 --- a/pythonforandroid/recipes/scrypt/__init__.py +++ b/pythonforandroid/recipes/scrypt/__init__.py @@ -1,4 +1,3 @@ -import os from pythonforandroid.recipe import CythonRecipe @@ -6,7 +5,7 @@ class ScryptRecipe(CythonRecipe): version = '0.8.6' url = 'https://bitbucket.org/mhallin/py-scrypt/get/v{version}.zip' - depends = [('python2', 'python3crystax'), 'setuptools', 'openssl'] + depends = ['setuptools', 'openssl'] call_hostpython_via_targetpython = False patches = ["remove_librt.patch"] @@ -15,23 +14,12 @@ def get_recipe_env(self, arch, with_flags_in_cc=True): Adds openssl recipe to include and library path. """ env = super(ScryptRecipe, self).get_recipe_env(arch, with_flags_in_cc) - openssl_build_dir = self.get_recipe( - 'openssl', self.ctx).get_build_dir(arch.arch) - env['CFLAGS'] += ' -I{}'.format(os.path.join(openssl_build_dir, 'include')) - env['LDFLAGS'] += ' -L{}'.format( - self.ctx.get_libs_dir(arch.arch) + - '-L{}'.format(self.ctx.libs_dir)) + ' -L{}'.format( - openssl_build_dir) - # required additional library and path for Crystax - if self.ctx.ndk == 'crystax': - # only keeps major.minor (discards patch) - python_version = self.ctx.python_recipe.version[0:3] - ndk_dir_python = os.path.join(self.ctx.ndk_dir, 'sources/python/', python_version) - env['LDFLAGS'] += ' -L{}'.format(os.path.join(ndk_dir_python, 'libs', arch.arch)) - env['LDFLAGS'] += ' -lpython{}m'.format(python_version) - # until `pythonforandroid/archs.py` gets merged upstream: - # https://github.com/kivy/python-for-android/pull/1250/files#diff-569e13021e33ced8b54385f55b49cbe6 - env['CFLAGS'] += ' -I{}/include/python/'.format(ndk_dir_python) + openssl_recipe = self.get_recipe('openssl', self.ctx) + env['CFLAGS'] += openssl_recipe.include_flags(arch) + env['LDFLAGS'] += ' -L{}'.format(self.ctx.get_libs_dir(arch.arch)) + env['LDFLAGS'] += ' -L{}'.format(self.ctx.libs_dir) + env['LDFLAGS'] += openssl_recipe.link_dirs_flags(arch) + env['LIBS'] = env.get('LIBS', '') + openssl_recipe.link_libs_flags() return env diff --git a/pythonforandroid/recipes/sdl2/__init__.py b/pythonforandroid/recipes/sdl2/__init__.py index 48b7515c0e..bbfadc2c04 100644 --- a/pythonforandroid/recipes/sdl2/__init__.py +++ b/pythonforandroid/recipes/sdl2/__init__.py @@ -4,17 +4,15 @@ class LibSDL2Recipe(BootstrapNDKRecipe): - version = "2.0.4" + version = "2.0.9" url = "https://www.libsdl.org/release/SDL2-{version}.tar.gz" - md5sum = '44fc4a023349933e7f5d7a582f7b886e' + md5sum = 'f2ecfba915c54f7200f504d8b48a5dfe' dir_name = 'SDL' depends = ['sdl2_image', 'sdl2_mixer', 'sdl2_ttf'] conflicts = ['sdl', 'pygame', 'pygame_bootstrap_components'] - patches = ['add_nativeSetEnv.patch'] - def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=True): env = super(LibSDL2Recipe, self).get_recipe_env( arch=arch, with_flags_in_cc=with_flags_in_cc, with_python=with_python) diff --git a/pythonforandroid/recipes/sdl2/add_nativeSetEnv.patch b/pythonforandroid/recipes/sdl2/add_nativeSetEnv.patch deleted file mode 100644 index 2262f1690f..0000000000 --- a/pythonforandroid/recipes/sdl2/add_nativeSetEnv.patch +++ /dev/null @@ -1,22 +0,0 @@ ---- orig/src/core/android/SDL_android.c 2016-01-02 20:56:31.000000000 +0100 -+++ patch/src/core/android/SDL_android.c 2016-04-15 22:21:13.985708267 +0200 -@@ -188,6 +188,19 @@ - Android_OnHat(device_id, hat_id, x, y); - } - -+/* Patched in env var setter for python-for-android */ -+JNIEXPORT void JNICALL Java_org_libsdl_app_SDLActivity_nativeSetEnv( -+ JNIEnv* env, jclass jcls, -+ jstring j_name, jstring j_value) -+{ -+ jboolean iscopy; -+ const char *name = (*env)->GetStringUTFChars(env, j_name, &iscopy); -+ const char *value = (*env)->GetStringUTFChars(env, j_value, &iscopy); -+ setenv(name, value, 1); -+ (*env)->ReleaseStringUTFChars(env, j_name, name); -+ (*env)->ReleaseStringUTFChars(env, j_value, value); -+} -+ - - JNIEXPORT jint JNICALL Java_org_libsdl_app_SDLActivity_nativeAddJoystick( - JNIEnv* env, jclass jcls, diff --git a/pythonforandroid/recipes/sdl2_image/__init__.py b/pythonforandroid/recipes/sdl2_image/__init__.py index d611899b42..9ecfc388d0 100644 --- a/pythonforandroid/recipes/sdl2_image/__init__.py +++ b/pythonforandroid/recipes/sdl2_image/__init__.py @@ -3,14 +3,14 @@ class LibSDL2Image(BootstrapNDKRecipe): - version = '2.0.1' + version = '2.0.4' url = 'https://www.libsdl.org/projects/SDL_image/release/SDL2_image-{version}.tar.gz' dir_name = 'SDL2_image' patches = ['toggle_jpg_png_webp.patch', ('disable_jpg.patch', is_arch('x86')), 'extra_cflags.patch', - 'fix_with_ndk_15_plus.patch'] + ] recipe = LibSDL2Image() diff --git a/pythonforandroid/recipes/sdl2_image/extra_cflags.patch b/pythonforandroid/recipes/sdl2_image/extra_cflags.patch index f8f26b73e2..c2b875bbe4 100644 --- a/pythonforandroid/recipes/sdl2_image/extra_cflags.patch +++ b/pythonforandroid/recipes/sdl2_image/extra_cflags.patch @@ -1,11 +1,11 @@ ---- orig/Android.mk 2016-01-03 06:52:28.000000000 +0100 -+++ patch/Android.mk 2016-04-15 21:03:18.547379710 +0200 -@@ -25,7 +25,7 @@ - LOCAL_C_INCLUDES := $(LOCAL_PATH) +--- SDL2_image-2.0.4/Android.mk.orig 2018-10-31 15:58:52.000000000 +0100 ++++ SDL2_image-2.0.4/Android.mk 2019-02-07 21:57:29.552365123 +0100 +@@ -61,6 +61,8 @@ LOCAL_SRC_FILES := \ + LOCAL_CFLAGS := -DLOAD_BMP -DLOAD_GIF -DLOAD_LBM -DLOAD_PCX -DLOAD_PNM \ - -DLOAD_TGA -DLOAD_XCF -DLOAD_XPM -DLOAD_XV --LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays -+LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays $(EXTRA_CFLAGS) - - LOCAL_SRC_FILES := $(notdir $(filter-out %/showimage.c, $(wildcard $(LOCAL_PATH)/*.c))) - + -DLOAD_SVG -DLOAD_TGA -DLOAD_XCF -DLOAD_XPM -DLOAD_XV ++LOCAL_CFLAGS += $(EXTRA_CFLAGS) ++ + LOCAL_LDLIBS := + LOCAL_STATIC_LIBRARIES := + LOCAL_SHARED_LIBRARIES := SDL2 diff --git a/pythonforandroid/recipes/sdl2_image/fix_with_ndk_15_plus.patch b/pythonforandroid/recipes/sdl2_image/fix_with_ndk_15_plus.patch deleted file mode 100644 index a6d42b8dc4..0000000000 --- a/pythonforandroid/recipes/sdl2_image/fix_with_ndk_15_plus.patch +++ /dev/null @@ -1,50 +0,0 @@ -diff --git a/Android.mk b/Android.mk -index 97a96c7..2e724c0 100644 ---- a/Android.mk -+++ b/Android.mk -@@ -79,6 +79,7 @@ ifeq ($(SUPPORT_JPG),true) - $(JPG_LIBRARY_PATH)/jfdctfst.c \ - $(JPG_LIBRARY_PATH)/jfdctint.c \ - $(JPG_LIBRARY_PATH)/jidctflt.c \ -+ $(JPG_LIBRARY_PATH)/jidctfst.c \ - $(JPG_LIBRARY_PATH)/jidctint.c \ - $(JPG_LIBRARY_PATH)/jquant1.c \ - $(JPG_LIBRARY_PATH)/jquant2.c \ -@@ -86,12 +87,6 @@ ifeq ($(SUPPORT_JPG),true) - $(JPG_LIBRARY_PATH)/jmemmgr.c \ - $(JPG_LIBRARY_PATH)/jmem-android.c - -- # assembler support is available for arm -- ifeq ($(TARGET_ARCH),arm) -- LOCAL_SRC_FILES += $(JPG_LIBRARY_PATH)/jidctfst.S -- else -- LOCAL_SRC_FILES += $(JPG_LIBRARY_PATH)/jidctfst.c -- endif - endif - - ifeq ($(SUPPORT_PNG),true) -diff --git a/external/jpeg-9/Android.mk b/external/jpeg-9/Android.mk -index a5edbde..77f139c 100644 ---- a/external/jpeg-9/Android.mk -+++ b/external/jpeg-9/Android.mk -@@ -14,20 +14,6 @@ LOCAL_SRC_FILES := \ - jquant2.c jutils.c jmemmgr.c \ - jmem-android.c - --# the assembler is only for the ARM version, don't break the Linux sim --ifneq ($(TARGET_ARCH),arm) --ANDROID_JPEG_NO_ASSEMBLER := true --endif -- --# temp fix until we understand why this broke cnn.com --#ANDROID_JPEG_NO_ASSEMBLER := true -- --ifeq ($(strip $(ANDROID_JPEG_NO_ASSEMBLER)),true) --LOCAL_SRC_FILES += jidctint.c jidctfst.c --else --LOCAL_SRC_FILES += jidctint.c jidctfst.S --endif -- - LOCAL_CFLAGS += -DAVOID_TABLES - LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays - #LOCAL_CFLAGS += -march=armv6j diff --git a/pythonforandroid/recipes/sdl2_image/toggle_jpg_png_webp.patch b/pythonforandroid/recipes/sdl2_image/toggle_jpg_png_webp.patch index 320d1abf03..f7dce8ac62 100644 --- a/pythonforandroid/recipes/sdl2_image/toggle_jpg_png_webp.patch +++ b/pythonforandroid/recipes/sdl2_image/toggle_jpg_png_webp.patch @@ -1,25 +1,25 @@ ---- orig/Android.mk 2016-01-03 06:52:28.000000000 +0100 -+++ patch/Android.mk 2016-04-15 21:14:23.906688966 +0200 -@@ -6,19 +6,19 @@ - +--- SDL2_image-2.0.4/Android.mk.orig 2018-10-31 15:58:52.000000000 +0100 ++++ SDL2_image-2.0.4/Android.mk 2019-02-07 23:51:51.740299680 +0100 +@@ -3,18 +3,18 @@ SDL_IMAGE_LOCAL_PATH := $(call my-dir) + # Enable this if you want to support loading JPEG images # The library path should be a relative path to this directory. -SUPPORT_JPG ?= true +SUPPORT_JPG := true - JPG_LIBRARY_PATH := external/jpeg-9 - + JPG_LIBRARY_PATH := external/jpeg-9b + # Enable this if you want to support loading PNG images # The library path should be a relative path to this directory. -SUPPORT_PNG ?= true +SUPPORT_PNG := true - PNG_LIBRARY_PATH := external/libpng-1.6.2 - + PNG_LIBRARY_PATH := external/libpng-1.6.32 + # Enable this if you want to support loading WebP images # The library path should be a relative path to this directory. - # - # IMPORTANT: In order to enable this must have a symlink in your jni directory to external/libwebp-0.3.0. --SUPPORT_WEBP ?= false -+SUPPORT_WEBP := false - WEBP_LIBRARY_PATH := external/libwebp-0.3.0 - - +-SUPPORT_WEBP ?= true +-WEBP_LIBRARY_PATH := external/libwebp-0.6.0 ++SUPPORT_WEBP := true ++WEBP_LIBRARY_PATH := external/libwebp-1.0.0 + + + # Build the library diff --git a/pythonforandroid/recipes/secp256k1/__init__.py b/pythonforandroid/recipes/secp256k1/__init__.py index d07cea4343..889803100f 100644 --- a/pythonforandroid/recipes/secp256k1/__init__.py +++ b/pythonforandroid/recipes/secp256k1/__init__.py @@ -1,45 +1,29 @@ import os -from pythonforandroid.recipe import PythonRecipe +from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe -class Secp256k1Recipe(PythonRecipe): +class Secp256k1Recipe(CppCompiledComponentsPythonRecipe): - url = 'https://github.com/ludbb/secp256k1-py/archive/master.zip' + version = '0.13.2.4' + url = 'https://github.com/ludbb/secp256k1-py/archive/{version}.tar.gz' call_hostpython_via_targetpython = False depends = [ - 'openssl', ('hostpython2', 'hostpython3crystax'), - ('python2', 'python3crystax'), 'setuptools', + 'openssl', ('hostpython3', 'hostpython2', 'hostpython3crystax'), + ('python2', 'python3', 'python3crystax'), 'setuptools', 'libffi', 'cffi', 'libsecp256k1'] patches = [ "cross_compile.patch", "drop_setup_requires.patch", "pkg-config.patch", "find_lib.patch", "no-download.patch"] - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(Secp256k1Recipe, self).get_recipe_env(arch, with_flags_in_cc) - # sets linker to use the correct gcc (cross compiler) - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' + def get_recipe_env(self, arch=None): + env = super(Secp256k1Recipe, self).get_recipe_env(arch) libsecp256k1 = self.get_recipe('libsecp256k1', self.ctx) libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch) - env['LDFLAGS'] += ' -L{}'.format(libsecp256k1_dir) - env['CFLAGS'] = ' -I' + os.path.join(libsecp256k1_dir, 'include') - # only keeps major.minor (discards patch) - python_version = self.ctx.python_recipe.version[0:3] - # required additional library and path for Crystax - if self.ctx.ndk == 'crystax': - ndk_dir_python = os.path.join(self.ctx.ndk_dir, 'sources/python/', python_version) - env['LDFLAGS'] += ' -L{}'.format(os.path.join(ndk_dir_python, 'libs', arch.arch)) - env['LDFLAGS'] += ' -lpython{}m'.format(python_version) - # until `pythonforandroid/archs.py` gets merged upstream: - # https://github.com/kivy/python-for-android/pull/1250/files#diff-569e13021e33ced8b54385f55b49cbe6 - env['CFLAGS'] += ' -I{}/include/python/'.format(ndk_dir_python) - else: - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() - env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python{}'.format(python_version) - env['LDFLAGS'] += " -lpython{}".format(python_version) - env['LDFLAGS'] += " -lsecp256k1" + env['CFLAGS'] += ' -I' + os.path.join(libsecp256k1_dir, 'include') + env['LDFLAGS'] += ' -L{} -lsecp256k1'.format(libsecp256k1_dir) return env diff --git a/pythonforandroid/recipes/shapely/__init__.py b/pythonforandroid/recipes/shapely/__init__.py index 05e260846e..e0b093766b 100644 --- a/pythonforandroid/recipes/shapely/__init__.py +++ b/pythonforandroid/recipes/shapely/__init__.py @@ -4,7 +4,7 @@ class ShapelyRecipe(CythonRecipe): version = '1.5' url = 'https://github.com/Toblerity/Shapely/archive/master.zip' - depends = ['python2', 'setuptools', 'libgeos'] + depends = ['setuptools', 'libgeos'] call_hostpython_via_targetpython = False patches = ['setup.patch'] # Patch to force setup to fail when C extention fails to build diff --git a/pythonforandroid/recipes/simple-crypt/__init__.py b/pythonforandroid/recipes/simple-crypt/__init__.py index 0c2781ee4d..94c5f510b8 100644 --- a/pythonforandroid/recipes/simple-crypt/__init__.py +++ b/pythonforandroid/recipes/simple-crypt/__init__.py @@ -4,7 +4,7 @@ class SimpleCryptRecipe(PythonRecipe): version = '4.1.7' url = 'https://pypi.python.org/packages/source/s/simple-crypt/simple-crypt-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'pycrypto'] + depends = ['pycrypto'] site_packages_name = 'simplecrypt' diff --git a/pythonforandroid/recipes/sqlalchemy/__init__.py b/pythonforandroid/recipes/sqlalchemy/__init__.py index 809d45a3dc..974667af2c 100644 --- a/pythonforandroid/recipes/sqlalchemy/__init__.py +++ b/pythonforandroid/recipes/sqlalchemy/__init__.py @@ -6,7 +6,7 @@ class SQLAlchemyRecipe(CompiledComponentsPythonRecipe): version = '1.0.9' url = 'https://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'setuptools'] + depends = ['setuptools'] patches = ['zipsafe.patch'] diff --git a/pythonforandroid/recipes/storm/__init__.py b/pythonforandroid/recipes/storm/__init__.py index 117e73361d..6b64465998 100644 --- a/pythonforandroid/recipes/storm/__init__.py +++ b/pythonforandroid/recipes/storm/__init__.py @@ -5,7 +5,7 @@ class StormRecipe(PythonRecipe): version = '0.20' url = 'https://launchpad.net/storm/trunk/{version}/+download/storm-{version}.tar.bz2' - depends = [('python2', 'python3crystax')] + depends = [] site_packages_name = 'storm' call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/sympy/__init__.py b/pythonforandroid/recipes/sympy/__init__.py index 473c4332da..8684a95e06 100644 --- a/pythonforandroid/recipes/sympy/__init__.py +++ b/pythonforandroid/recipes/sympy/__init__.py @@ -6,7 +6,7 @@ class SympyRecipe(PythonRecipe): version = '1.1.1' url = 'https://github.com/sympy/sympy/releases/download/sympy-{version}/sympy-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'mpmath'] + depends = ['mpmath'] call_hostpython_via_targetpython = True diff --git a/pythonforandroid/recipes/ujson/__init__.py b/pythonforandroid/recipes/ujson/__init__.py index 57bc69f711..421e4d927c 100644 --- a/pythonforandroid/recipes/ujson/__init__.py +++ b/pythonforandroid/recipes/ujson/__init__.py @@ -4,7 +4,7 @@ class UJsonRecipe(CompiledComponentsPythonRecipe): version = '1.35' url = 'https://pypi.python.org/packages/source/u/ujson/ujson-{version}.tar.gz' - depends = [('python2', 'python3crystax')] + depends = [] recipe = UJsonRecipe() diff --git a/pythonforandroid/recipes/vispy/__init__.py b/pythonforandroid/recipes/vispy/__init__.py index fc41046bef..7ea046b3b3 100644 --- a/pythonforandroid/recipes/vispy/__init__.py +++ b/pythonforandroid/recipes/vispy/__init__.py @@ -4,7 +4,7 @@ class VispyRecipe(PythonRecipe): version = '0.4.0' url = 'https://github.com/vispy/vispy/archive/v{version}.tar.gz' - depends = ['python2', 'numpy', 'pysdl2'] + depends = ['numpy', 'pysdl2'] patches = ['disable_freetype.patch', 'disable_font_triage.patch', 'use_es2.patch', diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index ac44aecea9..8a3b8add87 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -21,7 +21,7 @@ class WebSocketClient(Recipe): # patches = ['websocket.patch'] # Paths relative to the recipe dir - depends = ['python2', 'android', 'pyjnius', 'cryptography', 'pyasn1', 'pyopenssl'] + depends = ['android', 'pyjnius', 'cryptography', 'pyasn1', 'pyopenssl'] recipe = WebSocketClient() diff --git a/pythonforandroid/recipes/wsaccel/__init__.py b/pythonforandroid/recipes/wsaccel/__init__.py index dd27caace8..7bfc3465db 100644 --- a/pythonforandroid/recipes/wsaccel/__init__.py +++ b/pythonforandroid/recipes/wsaccel/__init__.py @@ -4,7 +4,7 @@ class WSAccellRecipe(CythonRecipe): version = '0.6.2' url = 'https://pypi.python.org/packages/source/w/wsaccel/wsaccel-{version}.tar.gz' - depends = [('python2', 'python3crystax')] + depends = [] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/x3dh/__init__.py b/pythonforandroid/recipes/x3dh/__init__.py index 2990ac58dd..134bf2991e 100644 --- a/pythonforandroid/recipes/x3dh/__init__.py +++ b/pythonforandroid/recipes/x3dh/__init__.py @@ -7,7 +7,6 @@ class X3DHRecipe(PythonRecipe): url = 'https://pypi.python.org/packages/source/X/X3DH/X3DH-{version}.tar.gz' site_packages_name = 'x3dh' depends = [ - ('python2', 'python3crystax'), 'setuptools', 'cryptography', 'xeddsa', diff --git a/pythonforandroid/recipes/xeddsa/__init__.py b/pythonforandroid/recipes/xeddsa/__init__.py index bec7ba2d68..eb0e2aeb0c 100644 --- a/pythonforandroid/recipes/xeddsa/__init__.py +++ b/pythonforandroid/recipes/xeddsa/__init__.py @@ -9,7 +9,6 @@ class XedDSARecipe(CythonRecipe): version = '0.4.4' url = 'https://pypi.python.org/packages/source/X/XEdDSA/XEdDSA-{version}.tar.gz' depends = [ - ('python2', 'python3crystax'), 'setuptools', 'cffi', 'pynacl', diff --git a/pythonforandroid/recipes/zbar/__init__.py b/pythonforandroid/recipes/zbar/__init__.py index 6f604e3759..62aa85bbe7 100644 --- a/pythonforandroid/recipes/zbar/__init__.py +++ b/pythonforandroid/recipes/zbar/__init__.py @@ -1,4 +1,4 @@ -import os +from os.path import join from pythonforandroid.recipe import PythonRecipe @@ -15,7 +15,7 @@ class ZBarRecipe(PythonRecipe): call_hostpython_via_targetpython = False - depends = ['hostpython2', 'python2', 'setuptools', 'libzbar'] + depends = ['setuptools', 'libzbar'] patches = ["zbar-0.10-python-crash.patch"] @@ -24,13 +24,9 @@ def get_recipe_env(self, arch=None, with_flags_in_cc=True): libzbar = self.get_recipe('libzbar', self.ctx) libzbar_dir = libzbar.get_build_dir(arch.arch) env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() - env['CFLAGS'] += ' -I' + os.path.join(libzbar_dir, 'include') - env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' - # TODO - env['LDSHARED'] = env['CC'] + \ - ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' - # TODO: hardcoded Python version - env['LDFLAGS'] += " -landroid -lpython2.7 -lzbar" + env['CFLAGS'] += ' -I' + join(libzbar_dir, 'include') + env['LDFLAGS'] += ' -L' + join(libzbar_dir, 'zbar', '.libs') + env['LIBS'] = env.get('LIBS', '') + ' -landroid -lzbar' return env diff --git a/pythonforandroid/recipes/zbarlight/__init__.py b/pythonforandroid/recipes/zbarlight/__init__.py new file mode 100644 index 0000000000..966c7fb241 --- /dev/null +++ b/pythonforandroid/recipes/zbarlight/__init__.py @@ -0,0 +1,26 @@ +from os.path import join +from pythonforandroid.recipe import PythonRecipe + + +class ZBarLightRecipe(PythonRecipe): + + version = '2.1' + + url = 'https://github.com/Polyconseil/zbarlight/archive/{version}.tar.gz' # noqa + + call_hostpython_via_targetpython = False + + depends = ['setuptools', 'libzbar'] + + def get_recipe_env(self, arch=None, with_flags_in_cc=True): + env = super(ZBarLightRecipe, self).get_recipe_env(arch, with_flags_in_cc) + libzbar = self.get_recipe('libzbar', self.ctx) + libzbar_dir = libzbar.get_build_dir(arch.arch) + env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() + env['CFLAGS'] += ' -I' + join(libzbar_dir, 'include') + env['LDFLAGS'] += ' -L' + join(libzbar_dir, 'zbar', '.libs') + env['LIBS'] = env.get('LIBS', '') + ' -landroid -lzbar' + return env + + +recipe = ZBarLightRecipe() diff --git a/pythonforandroid/recipes/zeroconf/__init__.py b/pythonforandroid/recipes/zeroconf/__init__.py index 6ee14a19f5..5ca57084a1 100644 --- a/pythonforandroid/recipes/zeroconf/__init__.py +++ b/pythonforandroid/recipes/zeroconf/__init__.py @@ -1,24 +1,12 @@ from pythonforandroid.recipe import PythonRecipe -from os.path import join class ZeroconfRecipe(PythonRecipe): name = 'zeroconf' version = '0.17.4' url = 'https://pypi.python.org/packages/source/z/zeroconf/zeroconf-{version}.tar.gz' - depends = ['python2', 'netifaces', 'enum34', 'six'] - - def get_recipe_env(self, arch=None): - env = super(ZeroconfRecipe, self).get_recipe_env(arch) - - # TODO: fix hardcoded path - # This is required to prevent issue with _io.so import. - hostpython = self.get_recipe('hostpython2', self.ctx) - env['PYTHONPATH'] = ( - join(hostpython.get_build_dir(arch.arch), 'build', - 'lib.linux-x86_64-2.7') + ':' + env.get('PYTHONPATH', '') - ) - return env + depends = ['setuptools', 'enum34', 'six'] + call_hostpython_via_targetpython = False recipe = ZeroconfRecipe() diff --git a/pythonforandroid/recipes/zope_interface/__init__.py b/pythonforandroid/recipes/zope_interface/__init__.py index e1e43ed261..b1fb0bd122 100644 --- a/pythonforandroid/recipes/zope_interface/__init__.py +++ b/pythonforandroid/recipes/zope_interface/__init__.py @@ -1,5 +1,6 @@ from pythonforandroid.recipe import PythonRecipe from pythonforandroid.toolchain import current_directory +from os.path import join import sh @@ -9,10 +10,18 @@ class ZopeInterfaceRecipe(PythonRecipe): version = '4.1.3' url = 'https://pypi.python.org/packages/source/z/zope.interface/zope.interface-{version}.tar.gz' site_packages_name = 'zope.interface' - - depends = [('python2', 'python3crystax')] + depends = ['setuptools'] patches = ['no_tests.patch'] + def build_arch(self, arch): + super(ZopeInterfaceRecipe, self).build_arch(arch) + # The zope.interface module lacks of the __init__.py file in one of his + # folders (once is installed), that leads into an ImportError. + # Here we intentionally apply a patch to solve that, so, in case that + # this is solved in the future an error will be triggered + zope_install = join(self.ctx.get_site_packages_dir(arch.arch), 'zope') + self.apply_patch('fix-init.patch', arch.arch, build_dir=zope_install) + def prebuild_arch(self, arch): super(ZopeInterfaceRecipe, self).prebuild_arch(arch) with current_directory(self.get_build_dir(arch.arch)): diff --git a/pythonforandroid/recipes/zope_interface/fix-init.patch b/pythonforandroid/recipes/zope_interface/fix-init.patch new file mode 100644 index 0000000000..b618eb5314 --- /dev/null +++ b/pythonforandroid/recipes/zope_interface/fix-init.patch @@ -0,0 +1,9 @@ +The zope.interface module lacks of the __init__.py file in `zope` folder +(once is installed), this patch creates that missing file. This seems to be +caused during the installation process because that file exists in source +files. +diff -Naurp zope.orig/__init__.py zope/__init__.py +--- zope.orig/__init__.py 1970-01-01 01:00:00.000000000 +0100 ++++ zope/__init__.py 2019-02-05 11:29:22.666757227 +0100 +@@ -0,0 +1 @@ ++ diff --git a/pythonforandroid/recommendations.py b/pythonforandroid/recommendations.py new file mode 100644 index 0000000000..fd2fd3a8be --- /dev/null +++ b/pythonforandroid/recommendations.py @@ -0,0 +1,107 @@ +"""Simple functions for checking dependency versions.""" + +from distutils.version import LooseVersion +from os.path import join +from pythonforandroid.logger import info, warning +from pythonforandroid.util import BuildInterruptingException + +# We only check the NDK major version +MIN_NDK_VERSION = 17 +MAX_NDK_VERSION = 17 + +RECOMMENDED_NDK_VERSION = '17c' +OLD_NDK_MESSAGE = 'Older NDKs may not be compatible with all p4a features.' +NEW_NDK_MESSAGE = 'Newer NDKs may not be fully supported by p4a.' + + +def check_ndk_version(ndk_dir): + # Check the NDK version against what is currently recommended + version = read_ndk_version(ndk_dir) + + if version is None: + return # if we failed to read the version, just don't worry about it + + major_version = version.version[0] + + info('Found NDK revision {}'.format(version)) + + if major_version < MIN_NDK_VERSION: + warning('Minimum recommended NDK version is {}'.format( + RECOMMENDED_NDK_VERSION)) + warning(OLD_NDK_MESSAGE) + elif major_version > MAX_NDK_VERSION: + warning('Maximum recommended NDK version is {}'.format( + RECOMMENDED_NDK_VERSION)) + warning(NEW_NDK_MESSAGE) + + +def read_ndk_version(ndk_dir): + """Read the NDK version from the NDK dir, if possible""" + try: + with open(join(ndk_dir, 'source.properties')) as fileh: + ndk_data = fileh.read() + except IOError: + info('Could not determine NDK version, no source.properties ' + 'in the NDK dir') + return + + for line in ndk_data.split('\n'): + if line.startswith('Pkg.Revision'): + break + else: + info('Could not parse $NDK_DIR/source.properties, not checking ' + 'NDK version') + return + + # Line should have the form "Pkg.Revision = ..." + ndk_version = LooseVersion(line.split('=')[-1].strip()) + + return ndk_version + + +MIN_TARGET_API = 26 + +# highest version tested to work fine with SDL2 +# should be a good default for other bootstraps too +RECOMMENDED_TARGET_API = 27 + +ARMEABI_MAX_TARGET_API = 21 +OLD_API_MESSAGE = ( + 'Target APIs lower than 26 are no longer supported on Google Play, ' + 'and are not recommended. Note that the Target API can be higher than ' + 'your device Android version, and should usually be as high as possible.') + + +def check_target_api(api, arch): + """Warn if the user's target API is less than the current minimum + recommendation + """ + + if api >= ARMEABI_MAX_TARGET_API and arch == 'armeabi': + raise BuildInterruptingException( + 'Asked to build for armeabi architecture with API ' + '{}, but API {} or greater does not support armeabi'.format( + api, ARMEABI_MAX_TARGET_API), + instructions='You probably want to build with --arch=armeabi-v7a instead') + + if api < MIN_TARGET_API: + warning('Target API {} < {}'.format(api, MIN_TARGET_API)) + warning(OLD_API_MESSAGE) + + +MIN_NDK_API = 21 +RECOMMENDED_NDK_API = 21 +OLD_NDK_API_MESSAGE = ('NDK API less than {} is not supported'.format(MIN_NDK_API)) + + +def check_ndk_api(ndk_api, android_api): + """Warn if the user's NDK is too high or low.""" + if ndk_api > android_api: + raise BuildInterruptingException( + 'Target NDK API is {}, higher than the target Android API {}.'.format( + ndk_api, android_api), + instructions=('The NDK API is a minimum supported API number and must be lower ' + 'than the target Android API')) + + if ndk_api < MIN_NDK_API: + warning(OLD_NDK_API_MESSAGE) diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py index 7c62320fea..153bba1523 100644 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -7,8 +7,10 @@ """ from __future__ import print_function +from os import environ from pythonforandroid import __version__ -from pythonforandroid.build import DEFAULT_NDK_API, DEFAULT_ANDROID_API +from pythonforandroid.recommendations import ( + RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API) from pythonforandroid.util import BuildInterruptingException, handle_build_exception @@ -139,7 +141,6 @@ def wrapper_func(self, args): ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, user_ndk_dir=self.ndk_dir, user_android_api=self.android_api, - user_ndk_ver=self.ndk_version, user_ndk_api=self.ndk_api) dist = self._dist if dist.needs_build: @@ -170,16 +171,24 @@ def build_dist_from_args(ctx, dist, args): """Parses out any bootstrap related arguments, and uses them to build a dist.""" bs = Bootstrap.get_bootstrap(args.bootstrap, ctx) - build_order, python_modules, bs \ - = get_recipe_order_and_bootstrap(ctx, dist.recipes, bs) + blacklist = getattr(args, "blacklist_requirements", "").split(",") + if len(blacklist) == 1 and blacklist[0] == "": + blacklist = [] + build_order, python_modules, bs = ( + get_recipe_order_and_bootstrap( + ctx, dist.recipes, bs, + blacklist=blacklist + )) ctx.recipe_build_order = build_order ctx.python_modules = python_modules info('The selected bootstrap is {}'.format(bs.name)) info_main('# Creating dist with {} bootstrap'.format(bs.name)) bs.distribution = dist - info_notify('Dist will have name {} and recipes ({})'.format( + info_notify('Dist will have name {} and requirements ({})'.format( dist.name, ', '.join(dist.recipes))) + info('Dist contains the following requirements as recipes: {}'.format( + ctx.recipe_build_order)) info('Dist will also contain modules ({}) installed from pip'.format( ', '.join(ctx.python_modules))) @@ -258,16 +267,16 @@ def __init__(self): default=0, type=int, help=('The Android API level to build against defaults to {} if ' - 'not specified.').format(DEFAULT_ANDROID_API)) + 'not specified.').format(RECOMMENDED_TARGET_API)) generic_parser.add_argument( - '--ndk-version', '--ndk_version', dest='ndk_version', default='', - help=('The version of the Android NDK. This is optional: ' - 'we try to work it out automatically from the ndk_dir.')) + '--ndk-version', '--ndk_version', dest='ndk_version', default=None, + help=('DEPRECATED: the NDK version is now found automatically or ' + 'not at all.')) generic_parser.add_argument( '--ndk-api', type=int, default=None, help=('The Android API level to compile against. This should be your ' '*minimal supported* API, not normally the same as your --android-api. ' - 'Defaults to min(ANDROID_API, {}) if not specified.').format(DEFAULT_NDK_API)) + 'Defaults to min(ANDROID_API, {}) if not specified.').format(RECOMMENDED_NDK_API)) generic_parser.add_argument( '--symlink-java-src', '--symlink_java_src', action='store_true', @@ -300,6 +309,13 @@ def __init__(self): 'Python modules'), default='') + generic_parser.add_argument( + '--blacklist-requirements', + help=('Blacklist an internal recipe from use. Allows ' + 'disabling Python 3 core modules to save size'), + dest="blacklist_requirements", + default='') + generic_parser.add_argument( '--bootstrap', help='The bootstrap to build with. Leave unset to choose ' @@ -360,6 +376,11 @@ def add_parser(subparsers, *args, **kwargs): kwargs.pop('aliases') return subparsers.add_parser(*args, **kwargs) + add_parser( + subparsers, + 'recommendations', + parents=[generic_parser], + help='List recommended p4a dependencies') parser_recipes = add_parser( subparsers, 'recipes', @@ -442,7 +463,6 @@ def add_parser(subparsers, *args, **kwargs): help='Symlink the dist instead of copying') parser_apk = add_parser( - subparsers, 'apk', help='Build an APK', parents=[generic_parser]) @@ -464,20 +484,20 @@ def add_parser(subparsers, *args, **kwargs): '--signkeypw', dest='signkeypw', action='store', default=None, help='Password for key alias') - parser_create = add_parser( + add_parser( subparsers, 'create', help='Compile a set of requirements into a dist', parents=[generic_parser]) - parser_archs = add_parser( + add_parser( subparsers, 'archs', help='List the available target architectures', parents=[generic_parser]) - parser_distributions = add_parser( + add_parser( subparsers, 'distributions', aliases=['dists'], help='List the currently available (compiled) dists', parents=[generic_parser]) - parser_delete_dist = add_parser( + add_parser( subparsers, 'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist', parents=[generic_parser]) @@ -490,15 +510,15 @@ def add_parser(subparsers, *args, **kwargs): parser_sdk_tools.add_argument( 'tool', help='The binary tool name to run') - parser_adb = add_parser( + add_parser( subparsers, 'adb', help='Run adb from the given SDK', parents=[generic_parser]) - parser_logcat = add_parser( + add_parser( subparsers, 'logcat', help='Run logcat from the given SDK', parents=[generic_parser]) - parser_build_status = add_parser( + add_parser( subparsers, 'build_status', aliases=['build-status'], help='Print some debug information about current built components', @@ -521,9 +541,11 @@ def add_parser(subparsers, *args, **kwargs): if args.debug: logger.setLevel(logging.DEBUG) - # strip version from requirements, and put them in environ + # Process requirements and put version in environ if hasattr(args, 'requirements'): requirements = [] + + # Parse --requirements argument list: for requirement in split_argument_list(args.requirements): if "==" in requirement: requirement, version = requirement.split(u"==", 1) @@ -533,13 +555,14 @@ def add_parser(subparsers, *args, **kwargs): requirements.append(requirement) args.requirements = u",".join(requirements) + self.warn_on_deprecated_args(args) + self.ctx = Context() self.storage_dir = args.storage_dir self.ctx.setup_dirs(self.storage_dir) self.sdk_dir = args.sdk_dir self.ndk_dir = args.ndk_dir self.android_api = args.android_api - self.ndk_version = args.ndk_version self.ndk_api = args.ndk_api self.ctx.symlink_java_src = args.symlink_java_src self.ctx.java_build_tool = args.java_build_tool @@ -552,6 +575,19 @@ def add_parser(subparsers, *args, **kwargs): # Each subparser corresponds to a method getattr(self, args.subparser_name.replace('-', '_'))(args) + def warn_on_deprecated_args(self, args): + """ + Print warning messages for any deprecated arguments that were passed. + """ + + # NDK version is now determined automatically + if args.ndk_version is not None: + warning('--ndk-version is deprecated and no longer necessary, ' + 'the value you passed is ignored') + if 'ANDROIDNDKVER' in environ: + warning('$ANDROIDNDKVER is deprecated and no longer necessary, ' + 'the value you set is ignored') + def hook(self, name): if not self.args.hook: return @@ -594,7 +630,7 @@ def recipes(self, args): for name in sorted(Recipe.list_recipes(ctx)): try: recipe = Recipe.get_recipe(name, ctx) - except IOError: + except (IOError, ValueError): warning('Recipe "{}" could not be loaded'.format(name)) except SyntaxError: import traceback @@ -772,7 +808,7 @@ def apk(self, args): if len(argx) > 1: unknown_args[i] = '='.join( (argx[0], realpath(expanduser(argx[1])))) - else: + elif i + 1 < len(unknown_args): unknown_args[i+1] = realpath(expanduser(unknown_args[i+1])) env = os.environ.copy() @@ -959,7 +995,6 @@ def sdk_tools(self, args): ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, user_ndk_dir=self.ndk_dir, user_android_api=self.android_api, - user_ndk_ver=self.ndk_version, user_ndk_api=self.ndk_api) android = sh.Command(join(ctx.sdk_dir, 'tools', args.tool)) output = android( @@ -987,7 +1022,6 @@ def _adb(self, commands): ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, user_ndk_dir=self.ndk_dir, user_android_api=self.android_api, - user_ndk_ver=self.ndk_version, user_ndk_api=self.ndk_api) if platform in ('win32', 'cygwin'): adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb.exe')) diff --git a/pythonforandroid/util.py b/pythonforandroid/util.py index 4c83338a52..9c007c2142 100644 --- a/pythonforandroid/util.py +++ b/pythonforandroid/util.py @@ -1,8 +1,9 @@ import contextlib from os.path import exists, join -from os import getcwd, chdir, makedirs, walk +from os import getcwd, chdir, makedirs, walk, uname import io import json +import sh import shutil import sys from fnmatch import fnmatch @@ -24,6 +25,13 @@ class WgetDownloader(FancyURLopener): urlretrieve = WgetDownloader().retrieve +build_platform = '{system}-{machine}'.format( + system=uname()[0], machine=uname()[-1]).lower() +"""the build platform in the format `system-machine`. We use +this string to define the right build system when compiling some recipes or +to get the right path for clang compiler""" + + @contextlib.contextmanager def current_directory(new_dir): cur_dir = getcwd() @@ -127,6 +135,17 @@ def is_exe(fpath): return None +def get_virtualenv_executable(): + virtualenv = None + if virtualenv is None: + virtualenv = sh.which('virtualenv2') + if virtualenv is None: + virtualenv = sh.which('virtualenv-2.7') + if virtualenv is None: + virtualenv = sh.which('virtualenv') + return virtualenv + + def walk_valid_filens(base_dir, invalid_dir_names, invalid_file_patterns): """Recursively walks all the files and directories in ``dirn``, ignoring directories that match any pattern in ``invalid_dirns`` diff --git a/setup.py b/setup.py index 74117ffd6d..558dcb2f27 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,11 @@ -from setuptools import setup, find_packages +import glob +from io import open # for open(..,encoding=...) parameter in python 2 from os import walk from os.path import join, dirname, sep import os -import glob import re +from setuptools import setup, find_packages # NOTE: All package data should also be set in MANIFEST.in @@ -44,7 +45,7 @@ def recursively_include(results, directory, patterns): recursively_include(package_data, 'pythonforandroid/bootstraps', ['*.properties', '*.xml', '*.java', '*.tmpl', '*.txt', '*.png', '*.mk', '*.c', '*.h', '*.py', '*.sh', '*.jpg', '*.aidl', - '*.gradle', '.gitkeep', 'gradlew*', '*.jar', ]) + '*.gradle', '.gitkeep', 'gradlew*', '*.jar', "*.patch", ]) recursively_include(package_data, 'pythonforandroid/bootstraps', ['sdl-config', ]) recursively_include(package_data, 'pythonforandroid/bootstraps/webview', @@ -52,13 +53,19 @@ def recursively_include(results, directory, patterns): recursively_include(package_data, 'pythonforandroid', ['liblink', 'biglink', 'liblink.sh']) -with open(join(dirname(__file__), 'README.rst')) as fileh: +with open(join(dirname(__file__), 'README.md'), + encoding="utf-8", + errors="replace", + ) as fileh: long_description = fileh.read() init_filen = join(dirname(__file__), 'pythonforandroid', '__init__.py') version = None try: - with open(init_filen) as fileh: + with open(init_filen, + encoding="utf-8", + errors="replace" + ) as fileh: lines = fileh.readlines() except IOError: pass diff --git a/testapps/on_device_unit_tests/buildozer.spec b/testapps/on_device_unit_tests/buildozer.spec new file mode 100644 index 0000000000..b313bad059 --- /dev/null +++ b/testapps/on_device_unit_tests/buildozer.spec @@ -0,0 +1,282 @@ +[app] + +# (str) Title of your application +title = p4a unit tests + +# (str) Package name +package.name = p4aunittests + +# (str) Package domain (needed for android/ios packaging) +package.domain = org.kivy + +# (str) Source code where the main.py live +source.dir = test_app + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas + +# (list) List of inclusions using pattern matching +#source.include_patterns = assets/*,images/*.png + +# (list) Source files to exclude (let empty to not exclude anything) +#source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +#source.exclude_dirs = tests, bin + +# (list) List of exclusions using pattern matching +#source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +version = 0.1 + +# (str) Application versioning (method 2) +# version.regex = __version__ = ['"](.*)['"] +# version.filename = %(source.dir)s/main.py + +# (list) Application requirements +# comma separated e.g. requirements = sqlite3,kivy +requirements = python3,kivy,openssl,numpy,sqlite3 + +# (str) Custom source folders for requirements +# Sets custom source for any requirements with recipes +# requirements.source.kivy = ../../kivy + +# (list) Garden requirements +#garden_requirements = + +# (str) Presplash of the application +#presplash.filename = %(source.dir)s/data/presplash.png + +# (str) Icon of the application +#icon.filename = %(source.dir)s/data/icon.png + +# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) +orientation = portrait + +# (list) List of service to declare +#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY + +# +# OSX Specific +# + +# +# author = © Copyright Info + +# change the major version of python used by the app +osx.python_version = 3 + +# Kivy version to use +osx.kivy_version = 1.9.1 + +# +# Android specific +# + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = 0 + +# (string) Presplash background color (for new android toolchain) +# 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. +#android.presplash_color = #FFFFFF + +# (list) Permissions +#android.permissions = INTERNET + +# (int) Target Android API, should be as high as possible. +#android.api = 27 + +# (int) Minimum API your APK will support. +#android.minapi = 21 + +# (int) Android SDK version to use +#android.sdk = 20 + +# (str) Android NDK version to use +#android.ndk = 17c + +# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. +#android.ndk_api = 21 + +# (bool) Use --private data storage (True) or --dir public storage (False) +#android.private_storage = True + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +#android.ndk_path = + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +#android.sdk_path = + +# (str) ANT directory (if empty, it will be automatically downloaded.) +#android.ant_path = + +# (bool) If True, then skip trying to update the Android sdk +# This can be useful to avoid excess Internet downloads or save time +# when an update is due and you just want to test/build your package +# android.skip_update = False + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.renpy.android.PythonActivity + +# (list) Pattern to whitelist for the whole project +android.whitelist = unittest/* + +# (str) Path to a custom whitelist file +#android.whitelist_src = + +# (str) Path to a custom blacklist file +#android.blacklist_src = + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (list) Android AAR archives to add (currently works only with sdl2_gradle +# bootstrap) +#android.add_aars = + +# (list) Gradle dependencies to add (currently works only with sdl2_gradle +# bootstrap) +#android.gradle_dependencies = + +# (list) Java classes to add as activities to the manifest. +#android.add_activites = com.example.ExampleActivity + +# (str) python-for-android branch to use, defaults to master +p4a.branch = master + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) XML file to include as an intent filters in tag +#android.manifest.intent_filters = + +# (str) launchMode to set for the main activity +#android.manifest.launch_mode = standard + +# (list) Android additional libraries to copy into libs/armeabi +#android.add_libs_armeabi = libs/android/*.so +#android.add_libs_armeabi_v7a = libs/android-v7/*.so +#android.add_libs_x86 = libs/android-x86/*.so +#android.add_libs_mips = libs/android-mips/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +#android.wakelock = False + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# (str) Android logcat filters to use +#android.logcat_filters = *:S python:D + +# (bool) Copy library instead of making a libpymodules.so +#android.copy_libs = 1 + +# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86 +android.arch = armeabi-v7a + +# +# Python for android (p4a) specific +# + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) +#p4a.source_dir = + +# (str) The directory in which python-for-android should look for your own build recipes (if any) +#p4a.local_recipes = + +# (str) Filename to the hook for p4a +#p4a.hook = + +# (str) Bootstrap to use for android builds +# p4a.bootstrap = sdl2 + +# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) +#p4a.port = + + +# +# iOS specific +# + +# (str) Path to a custom kivy-ios folder +#ios.kivy_ios_dir = ../kivy-ios + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: ()" + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 2 + +# (int) Display warning if buildozer is run as root (0 = False, 1 = True) +warn_on_root = 1 + +# (str) Path to build artifact storage, absolute or relative to spec file +# build_dir = ./.buildozer + +# (str) Path to build output (i.e. .apk, .ipa) storage +# bin_dir = ./bin + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +#[app] +#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +#[app:source.exclude_patterns] +#license +#data/audio/*.wav +#data/images/original/* +# + + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +#[app@demo] +#title = My Application (demo) +# +#[app:source.exclude_patterns@demo] +#images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +#buildozer --profile demo android debug diff --git a/testapps/on_device_unit_tests/test_app/main.py b/testapps/on_device_unit_tests/test_app/main.py new file mode 100644 index 0000000000..e0cd9c527d --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/main.py @@ -0,0 +1,45 @@ +import sys +if sys.version_info.major < 3: + print(('Running under Python {} but these tests ' + 'require Python 3+').format(sys.version_info.major)) + +import unittest +import importlib + +print('Imported unittest') + + +class PythonTestMixIn(object): + + module_import = None + + def test_import_module(self): + """Test importing the specified Python module name. This import test + is common to all Python modules, it does not test any further + functionality. + """ + self.assertIsNotNone( + self.module_import, + 'module_import is not set (was default None)') + + importlib.import_module(self.module_import) + + def test_run_module(self): + """Import the specified module and do something with it as a minimal + check that it actually works. + + This test fails by default, it must be overridden by every + child test class. + """ + + self.fail('This test must be overridden by {}'.format(self)) + +print('Defined test case') + +import sys +sys.path.append('./') +from tests import test_requirements +suite = unittest.TestLoader().loadTestsFromModule(test_requirements) +unittest.TextTestRunner().run(suite) + +print('Ran tests') diff --git a/doc/source/old_toolchain/_static/.empty b/testapps/on_device_unit_tests/test_app/tests/__init__.py similarity index 100% rename from doc/source/old_toolchain/_static/.empty rename to testapps/on_device_unit_tests/test_app/tests/__init__.py diff --git a/testapps/on_device_unit_tests/test_app/tests/test_requirements.py b/testapps/on_device_unit_tests/test_app/tests/test_requirements.py new file mode 100644 index 0000000000..625a99e5db --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/tests/test_requirements.py @@ -0,0 +1,50 @@ + +from main import PythonTestMixIn +from unittest import TestCase + + +class NumpyTestCase(PythonTestMixIn, TestCase): + module_import = 'numpy' + + def test_run_module(self): + import numpy as np + + arr = np.random.random((3, 3)) + det = np.linalg.det(arr) + + +class OpensslTestCase(PythonTestMixIn, TestCase): + module_import = '_ssl' + + def test_run_module(self): + import ssl + + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.options &= ~ssl.OP_NO_SSLv3 + + +class SqliteTestCase(PythonTestMixIn, TestCase): + module_import = 'sqlite3' + + def test_run_module(self): + import sqlite3 + + conn = sqlite3.connect('example.db') + conn.cursor() + + +class KivyTestCase(PythonTestMixIn, TestCase): + module_import = 'kivy' + + def test_run_module(self): + # This import has side effects, if it works then it's an + # indication that Kivy is okay + from kivy.core.window import Window + + +class PyjniusTestCase(PythonTestMixIn, TestCase): + module_import = 'jnius' + + def test_run_module(self): + from jnius import autoclass + autoclass('org.kivy.android.PythonActivity') diff --git a/testapps/setup_keyboard.py b/testapps/setup_keyboard.py index 38fa78726f..026847764d 100644 --- a/testapps/setup_keyboard.py +++ b/testapps/setup_keyboard.py @@ -3,8 +3,10 @@ from setuptools import find_packages options = {'apk': {'debug': None, - 'requirements': 'sdl2,pyjnius,kivy,python2', - 'android-api': 19, + 'requirements': 'sdl2,pyjnius,kivy,python3', + 'blacklist-requirements': 'openssl,sqlite3', + 'android-api': 27, + 'ndk-api': 21, 'ndk-dir': '/home/asandy/android/crystax-ndk-10.3.2', 'dist-name': 'bdisttest', 'ndk-version': '10.3.2', diff --git a/testapps/setup_testapp_flask.py b/testapps/setup_testapp_flask.py index 1722b6d6c4..3302e8595c 100644 --- a/testapps/setup_testapp_flask.py +++ b/testapps/setup_testapp_flask.py @@ -3,7 +3,8 @@ from setuptools import find_packages options = {'apk': {'debug': None, - 'requirements': 'python2,flask,pyjnius', + 'requirements': 'python3,flask,pyjnius', + 'blacklist-requirements': 'openssl,sqlite3', 'android-api': 27, 'ndk-api': 21, 'ndk-dir': '/home/asandy/android/crystax-ndk-10.3.2', diff --git a/testapps/setup_testapp_python3.py b/testapps/setup_testapp_python3.py index 40ed20d231..bf83b06c33 100644 --- a/testapps/setup_testapp_python3.py +++ b/testapps/setup_testapp_python3.py @@ -2,7 +2,8 @@ from distutils.core import setup from setuptools import find_packages -options = {'apk': {'requirements': 'libffi,sdl2,numpy,pyjnius,kivy,python3', +options = {'apk': {'requirements': 'sdl2,numpy,pyjnius,kivy,python3', + 'blacklist-requirements': 'openssl,sqlite3', 'android-api': 27, 'ndk-api': 21, 'dist-name': 'bdisttest_python3_googlendk', diff --git a/testapps/setup_testapp_python3_sqlite_openssl.py b/testapps/setup_testapp_python3_sqlite_openssl.py index 19e6b91fef..0f7485d132 100644 --- a/testapps/setup_testapp_python3_sqlite_openssl.py +++ b/testapps/setup_testapp_python3_sqlite_openssl.py @@ -2,8 +2,7 @@ from distutils.core import setup from setuptools import find_packages -options = {'apk': {'requirements': 'libffi,openssl,sqlite3,requests,peewee,' - 'sdl2,pyjnius,kivy,python3', +options = {'apk': {'requirements': 'requests,peewee,sdl2,pyjnius,kivy,python3', 'android-api': 27, 'ndk-api': 21, 'dist-name': 'bdisttest_python3_sqlite_openssl_googlendk', diff --git a/testapps/setup_testapp_python_encryption.py b/testapps/setup_testapp_python_encryption.py new file mode 100644 index 0000000000..2a468ade86 --- /dev/null +++ b/testapps/setup_testapp_python_encryption.py @@ -0,0 +1,30 @@ + +from distutils.core import setup +from setuptools import find_packages + +options = {'apk': {'requirements': 'sdl2,pyjnius,kivy,python3,cryptography,' + 'pycrypto,scrypt,m2crypto,pysha3,' + 'pycryptodome,libtorrent', + 'blacklist-requirements': 'sqlite3', + 'android-api': 27, + 'ndk-api': 21, + 'dist-name': 'bdisttest_encryption', + 'ndk-version': '10.3.2', + 'arch': 'armeabi-v7a', + 'permissions': ['INTERNET', 'VIBRATE'], + }} + +package_data = {'': ['*.py', + '*.png'] + } + +setup( + name='testapp_encryption', + version='1.0', + description='p4a setup.py test', + author='Pol Canelles', + author_email='canellestudi@gmail.com', + packages=find_packages(), + options=options, + package_data={'testapp_encryption': ['*.py', '*.png']} +) diff --git a/testapps/setup_testapp_service.py b/testapps/setup_testapp_service.py index 0ae059e49d..b246f108d9 100644 --- a/testapps/setup_testapp_service.py +++ b/testapps/setup_testapp_service.py @@ -3,7 +3,8 @@ from setuptools import find_packages options = {'apk': {'debug': None, - 'requirements': 'python2,genericndkbuild', + 'requirements': 'python3,genericndkbuild,pyjnius', + 'blacklist-requirements': 'openssl,sqlite3', 'android-api': 27, 'ndk-api': 21, 'ndk-dir': '/home/asandy/android/crystax-ndk-10.3.2', @@ -12,6 +13,7 @@ 'bootstrap': 'service_only', 'permissions': ['INTERNET', 'VIBRATE'], 'arch': 'armeabi-v7a', + 'service': 'time:p4atime.py', }} package_data = {'': ['*.py']} @@ -21,7 +23,7 @@ setup( name='testapp_service', - version='1.0', + version='1.1', description='p4a service testapp', author='Alexander Taylor', author_email='alexanderjohntaylor@gmail.com', diff --git a/testapps/setup_vispy.py b/testapps/setup_vispy.py index 99e2a5563b..a0863d0a1c 100644 --- a/testapps/setup_vispy.py +++ b/testapps/setup_vispy.py @@ -3,8 +3,10 @@ from setuptools import find_packages options = {'apk': {'debug': None, - 'requirements': 'vispy', - 'android-api': 19, + 'requirements': 'python3,vispy', + 'blacklist-requirements': 'openssl,sqlite3', + 'android-api': 27, + 'ndk-api': 21, 'ndk-dir': '/home/asandy/android/crystax-ndk-10.3.2', 'dist-name': 'bdisttest', 'ndk-version': '10.3.2', diff --git a/testapps/testapp_encryption/colours.png b/testapps/testapp_encryption/colours.png new file mode 100644 index 0000000000..30b685e32b Binary files /dev/null and b/testapps/testapp_encryption/colours.png differ diff --git a/testapps/testapp_encryption/main.py b/testapps/testapp_encryption/main.py new file mode 100644 index 0000000000..b3289e857a --- /dev/null +++ b/testapps/testapp_encryption/main.py @@ -0,0 +1,345 @@ +print('main.py was successfully called') + +import os + +print('imported os') + +print('this dir is', os.path.abspath(os.curdir)) + +print('contents of this dir', os.listdir('./')) + +import sys + +print('pythonpath is', sys.path) + +import kivy + +print('imported kivy') +print('file is', kivy.__file__) + +from kivy.app import App + +from kivy.lang import Builder +from kivy.properties import StringProperty + +from kivy.uix.popup import Popup +from kivy.clock import Clock + +print('Imported kivy') +from kivy.utils import platform + +print('platform is', platform) + +# Test cryptography +try: + from cryptography.fernet import Fernet + + key = Fernet.generate_key() + f = Fernet(key) + cryptography_encrypted = f.encrypt( + b'A really secret message. Not for prying eyes.') + cryptography_decrypted = f.decrypt(cryptography_encrypted) +except Exception as e1: + print('**************************') + print('Error on cryptography operations:\n{}'.format(e1)) + print('**************************') + cryptography_encrypted = 'Error' + cryptography_decrypted = 'Error' + +# Test pycrypto +crypto_hash_message = 'A secret message' +try: + from Crypto.Hash import SHA256 + + hash = SHA256.new() + hash.update(crypto_hash_message) + crypto_hash_hexdigest = hash.hexdigest() +except Exception as e2: + print('**************************') + print('Error on Crypto operations:\n{}'.format(e2)) + print('**************************') + crypto_hash_hexdigest = 'Error' + +# Test scrypt +try: + from scrypt import * + + status_import_scrypt = 'Success' +except ImportError as e3: + print('**************************') + print('Unable to import scrypt:\n{}'.format(e3)) + print('**************************') + status_import_scrypt = 'Error' + +# Test M2Crypto +try: + from M2Crypto import * + + status_import_m2crypto = 'Success' +except ImportError as e5: + print('**************************') + print('Unable to import M2Crypto:\n{}'.format(e5)) + print('**************************\n') + status_import_m2crypto = 'Error' + +# Test pysha3 +try: + import sha3 + + print('Ok imported pysha3, testing some basic operations...') + k = sha3.keccak_512() + k.update(b"data") + print('Test pysha3 operation (keccak_512): {}'.format(k.hexdigest())) + status_import_pysha3 = 'Success' +except ImportError as e6: + print('**************************') + print('Unable to import/operate with pysha3:\n{}'.format(e6)) + print('**************************') + status_import_pysha3 = 'Error' + +# Test pycryptodome +try: + from Crypto.PublicKey import RSA + + print('Ok imported pycryptodome, testing some basic operations...') + secret_code = "Unguessable" + key = RSA.generate(2048) + encrypted_key = key.export_key(passphrase=secret_code, pkcs=8, + protection="scryptAndAES128-CBC") + print('\t -> Testing key for secret code "Unguessable": {}'.format( + encrypted_key)) + + file_out = open("rsa_key.bin", "wb") + file_out.write(encrypted_key) + print('\t -> Testing key write: {}'.format( + 'ok' if os.path.exists(file_out) else 'fail')) + + print('\t -> Testing Public key:'.format(key.publickey().export_key())) + status_import_pycryptodome = 'Success (import and doing simple operations)' +except ImportError as e6: + print('**************************') + print('Unable to import/operate with pycryptodome:\n{}'.format(e6)) + print('**************************') + status_import_pycryptodome = 'Error' + +# Test libtorrent +try: + import libtorrent as lt + + print('Imported libtorrent version {}'.format(lt.version)) + status_import_libtorrent = 'Success (version is: {})'.format(lt.version) +except Exception as e4: + print('**************************') + print('Unable to import libtorrent:\n{}'.format(e4)) + print('**************************') + status_import_libtorrent = 'Error' + +kv = ''' +#:import Metrics kivy.metrics.Metrics +#:import sys sys + +: + size_hint_y: None + height: dp(60) + +: + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + test_module: '' + test_result: '' + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + markup: True + text: '[b]*** TEST {} MODULE ***[/b]'.format(self.parent.test_module) + halign: 'center' + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + markup: True + text: + 'Import {}: [color=a0a0a0]{}[/color]'.format( + self.parent.test_module, self.parent.test_result) + halign: 'left' + Widget: + size_hint_y: None + height: 20 + + +ScrollView: + GridLayout: + cols: 1 + size_hint_y: None + height: self.minimum_height + FixedSizeButton: + text: 'test pyjnius' + on_press: app.test_pyjnius() + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + markup: True + text: '[b]*** TEST CRYPTOGRAPHY MODULE ***[/b]' + halign: 'center' + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + markup: True + text: + 'Cryptography decrypted:\\n[color=a0a0a0]%s[/color]\\n' \\ + 'Cryptography encrypted:\\n[color=a0a0a0]%s[/color]' % ( + app.cryptography_decrypted, app.cryptography_encrypted) + halign: 'left' + Widget: + size_hint_y: None + height: 20 + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + markup: True + text: '[b]*** TEST CRYPTO MODULE ***[/b]' + halign: 'center' + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + markup: True + text: + 'Crypto message: \\n[color=a0a0a0]%s[/color]\\n'\\ + 'Crypto hex: \\n[color=a0a0a0]%s[/color]' % ( + app.crypto_hash_message, app.crypto_hash_hexdigest) + halign: 'left' + Widget: + size_hint_y: None + height: 20 + TestImport: + test_module: 'scrypt' + test_result: app.status_import_scrypt + TestImport: + test_module: 'm2crypto' + test_result: app.status_import_m2crypto + TestImport: + test_module: 'pysha3' + test_result: app.status_import_pysha3 + TestImport: + test_module: 'pycryptodome' + test_result: app.status_import_pycryptodome + TestImport: + test_module: 'libtorrent' + test_result: app.status_import_libtorrent + Image: + keep_ratio: False + allow_stretch: True + source: 'colours.png' + size_hint_y: None + height: dp(100) + Label: + height: self.texture_size[1] + size_hint_y: None + font_size: 100 + text_size: self.size[0], None + markup: True + text: '[b]Kivy[/b] on [b]SDL2[/b] on [b]Android[/b]!' + halign: 'center' + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + markup: True + text: sys.version + halign: 'center' + padding_y: dp(10) + Widget: + size_hint_y: None + height: 20 + Label: + height: self.texture_size[1] + size_hint_y: None + font_size: 50 + text_size: self.size[0], None + markup: True + text: + 'dpi: [color=a0a0a0]%s[/color]\\n'\\ + 'density: [color=a0a0a0]%s[/color]\\n'\\ + 'fontscale: [color=a0a0a0]%s[/color]' % ( + Metrics.dpi, Metrics.density, Metrics.fontscale) + halign: 'center' + FixedSizeButton: + text: 'test ctypes' + on_press: app.test_ctypes() + Widget: + size_hint_y: None + height: 1000 + on_touch_down: print('touched at', args[-1].pos) + +: + title: 'Error' + size_hint: 0.75, 0.75 + Label: + text: root.error_text +''' + + +class ErrorPopup(Popup): + error_text = StringProperty('') + + +def raise_error(error): + print('ERROR:', error) + ErrorPopup(error_text=error).open() + + +class TestApp(App): + cryptography_encrypted = cryptography_encrypted + cryptography_decrypted = cryptography_decrypted + crypto_hash_message = crypto_hash_message + crypto_hash_hexdigest = crypto_hash_hexdigest + status_import_scrypt = status_import_scrypt + status_import_m2crypto = status_import_m2crypto + status_import_pysha3 = status_import_pysha3 + status_import_pycryptodome = status_import_pycryptodome + status_import_libtorrent = status_import_libtorrent + + def build(self): + root = Builder.load_string(kv) + Clock.schedule_interval(self.print_something, 2) + # Clock.schedule_interval(self.test_pyjnius, 5) + print('testing metrics') + from kivy.metrics import Metrics + print('dpi is', Metrics.dpi) + print('density is', Metrics.density) + print('fontscale is', Metrics.fontscale) + return root + + def print_something(self, *args): + print('App print tick', Clock.get_boottime()) + + def on_pause(self): + return True + + def test_pyjnius(self, *args): + try: + from jnius import autoclass + except ImportError: + raise_error('Could not import pyjnius') + return + + print('Attempting to vibrate with pyjnius') + python_activity = autoclass('org.kivy.android.PythonActivity') + activity = python_activity.mActivity + intent = autoclass('android.content.Intent') + context = autoclass('android.content.Context') + vibrator = activity.getSystemService(context.VIBRATOR_SERVICE) + + vibrator.vibrate(1000) + + def test_ctypes(self, *args): + import ctypes + + +TestApp().run() diff --git a/testapps/testapp_keyboard/main.py b/testapps/testapp_keyboard/main.py index 03e66aa3e1..cb76b7af99 100644 --- a/testapps/testapp_keyboard/main.py +++ b/testapps/testapp_keyboard/main.py @@ -2,23 +2,29 @@ import os print('imported os') +import sys +print('imported sys') from kivy import platform if platform == 'android': - print('contents of ./lib/python2.7/site-packages/ etc.') - print(os.listdir('./lib')) - print(os.listdir('./lib/python2.7')) - print(os.listdir('./lib/python2.7/site-packages')) + site_dir_path = './_python_bundle/site-packages' + if not os.path.exists(site_dir_path): + print('warning: site-packages dir not found: ' + site_dir_path) + else: + print('contents of ' + site_dir_path) + print(os.listdir(site_dir_path)) print('this dir is', os.path.abspath(os.curdir)) print('contents of this dir', os.listdir('./')) - with open('./lib/python2.7/site-packages/kivy/app.pyo', 'rb') as fileh: - print('app.pyo size is', len(fileh.read())) + if (os.path.exists(site_dir_path) and + os.path.exists(site_dir_path + '/kivy/app.pyo') + ): + with open(site_dir_path + '/kivy/app.pyo', 'rb') as fileh: + print('app.pyo size is', len(fileh.read())) -import sys print('pythonpath is', sys.path) import kivy diff --git a/testapps/testapp_service/main.py b/testapps/testapp_service/main.py index 29ccb477bb..dc781cd3bd 100644 --- a/testapps/testapp_service/main.py +++ b/testapps/testapp_service/main.py @@ -1,4 +1,4 @@ -print('main.py was successfully called') +print('Service Test App main.py was successfully called') import sys print('python version is: ', sys.version) @@ -11,17 +11,9 @@ print(i, sqrt(i)) print('Just printing stuff apparently worked, trying a simple service') -import datetime, threading, time -next_call = time.time() - - -def service_timer(): - global next_call - print('P4a datetime service: {}'.format(datetime.datetime.now())) - next_call = next_call + 1 - threading.Timer(next_call - time.time(), service_timer).start() - - -print('Starting the service timer...') -service_timer() +from jnius import autoclass +service = autoclass('org.test.testapp_service.ServiceTime') +mActivity = autoclass('org.kivy.android.PythonActivity').mActivity +argument = 'test argument ok' +service.start(mActivity, argument) diff --git a/testapps/testapp_service/p4atime.py b/testapps/testapp_service/p4atime.py new file mode 100644 index 0000000000..75a1d76e0f --- /dev/null +++ b/testapps/testapp_service/p4atime.py @@ -0,0 +1,19 @@ +import datetime +import threading +import time +from os import environ +argument = environ.get('PYTHON_SERVICE_ARGUMENT', '') +print('p4atime.py was successfully called with argument: "{}"'.format(argument)) + +next_call = time.time() + + +def service_timer(): + global next_call + print('P4a datetime service: {}'.format(datetime.datetime.now())) + next_call = next_call + 1 + threading.Timer(next_call - time.time(), service_timer).start() + + +print('Starting the service timer...') +service_timer() diff --git a/tests/recipes/test_gevent.py b/tests/recipes/test_gevent.py new file mode 100644 index 0000000000..8548ffa64a --- /dev/null +++ b/tests/recipes/test_gevent.py @@ -0,0 +1,73 @@ +import unittest +from mock import patch +from pythonforandroid.archs import ArchARMv7_a +from pythonforandroid.build import Context +from pythonforandroid.recipe import Recipe + + +class TestGeventRecipe(unittest.TestCase): + + def setUp(self): + """ + Setups recipe and context. + """ + self.context = Context() + self.context.ndk_api = 21 + self.context.android_api = 27 + self.arch = ArchARMv7_a(self.context) + self.recipe = Recipe.get_recipe('gevent', self.context) + + def test_get_recipe_env(self): + """ + Makes sure `get_recipe_env()` sets compilation flags properly. + """ + mocked_cflags = ( + '-DANDROID -fomit-frame-pointer -D__ANDROID_API__=27 -mandroid ' + '-isystem /path/to/isystem ' + '-I/path/to/include1 ' + '-isysroot /path/to/sysroot ' + '-I/path/to/include2 ' + '-march=armv7-a -mfloat-abi=softfp -mfpu=vfp -mthumb ' + '-I/path/to/python3-libffi-openssl/include' + ) + mocked_ldflags = ( + ' --sysroot /path/to/sysroot ' + '-lm ' + '-L/path/to/library1 ' + '-L/path/to/library2 ' + '-lpython3.7m ' + # checks the regex doesn't parse `python3-libffi-openssl` as a `-libffi` + '-L/path/to/python3-libffi-openssl/library3 ' + ) + mocked_env = { + 'CFLAGS': mocked_cflags, + 'LDFLAGS': mocked_ldflags, + } + with patch('pythonforandroid.recipe.CythonRecipe.get_recipe_env') as m_get_recipe_env: + m_get_recipe_env.return_value = mocked_env + env = self.recipe.get_recipe_env() + expected_cflags = ( + ' -fomit-frame-pointer -mandroid -isystem /path/to/isystem' + ' -isysroot /path/to/sysroot' + ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfp -mthumb' + ) + expected_cppflags = ( + '-DANDROID -D__ANDROID_API__=27 ' + '-I/path/to/include1 ' + '-I/path/to/include2 ' + '-I/path/to/python3-libffi-openssl/include' + ) + expected_ldflags = ( + ' --sysroot /path/to/sysroot' + ' -L/path/to/library1' + ' -L/path/to/library2' + ' -L/path/to/python3-libffi-openssl/library3 ' + ) + expected_libs = '-lm -lpython3.7m' + expected_env = { + 'CFLAGS': expected_cflags, + 'CPPFLAGS': expected_cppflags, + 'LDFLAGS': expected_ldflags, + 'LIBS': expected_libs, + } + self.assertEqual(expected_env, env) diff --git a/tests/test_graph.py b/tests/test_graph.py index 96cda76d3f..e113c3bb3b 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,24 +1,75 @@ from pythonforandroid.build import Context -from pythonforandroid.graph import get_recipe_order_and_bootstrap +from pythonforandroid.graph import ( + fix_deplist, get_dependency_tuple_list_for_recipe, + get_recipe_order_and_bootstrap, obvious_conflict_checker, +) from pythonforandroid.bootstrap import Bootstrap +from pythonforandroid.recipe import Recipe from pythonforandroid.util import BuildInterruptingException from itertools import product +import mock import pytest - ctx = Context() name_sets = [['python2'], ['kivy']] bootstraps = [None, - Bootstrap.get_bootstrap('pygame', ctx), Bootstrap.get_bootstrap('sdl2', ctx)] valid_combinations = list(product(name_sets, bootstraps)) valid_combinations.extend( [(['python3crystax'], Bootstrap.get_bootstrap('sdl2', ctx)), - (['kivy', 'python3crystax'], Bootstrap.get_bootstrap('sdl2', ctx))]) -invalid_combinations = [[['python2', 'python3crystax'], None]] + (['kivy', 'python3crystax'], Bootstrap.get_bootstrap('sdl2', ctx)), + (['flask'], Bootstrap.get_bootstrap('webview', ctx)), + (['pysdl2'], None), # auto-detect bootstrap! important corner case + ] +) +invalid_combinations = [ + [['python2', 'python3crystax'], None], + [['pysdl2', 'genericndkbuild'], None], +] +invalid_combinations_simple = list(invalid_combinations) +# NOTE !! keep in mind when setting invalid_combinations_simple: +# +# This is used to test obvious_conflict_checker(), which only +# catches CERTAIN conflicts: +# +# This must be a list of conflicts where the conflict is ONLY in +# non-tuple/non-ambiguous dependencies, e.g.: +# +# dependencies_1st = ["python2", "pillow"] +# dependencies_2nd = ["python3crystax", "pillow"] +# +# This however won't work: +# +# dependencies_1st = [("python2", "python3"), "pillow"] +# dependencies_2nd = [("python2legacy", "python3crystax"), "pillow"] +# +# (This is simply because the conflict checker doesn't resolve this to +# keep the code ismple enough) + + +def get_fake_recipe(name, depends=None, conflicts=None): + recipe = mock.Mock() + recipe.name = name + recipe.get_opt_depends_in_list = lambda: [] + recipe.get_dir_name = lambda: name + recipe.depends = list(depends or []) + recipe.conflicts = list(conflicts or []) + return recipe + + +def register_fake_recipes_for_test(monkeypatch, recipe_list): + _orig_get_recipe = Recipe.get_recipe + + def mock_get_recipe(name, ctx): + for recipe in recipe_list: + if recipe.name == name: + return recipe + return _orig_get_recipe(name, ctx) + # Note: staticmethod() needed for python ONLY, don't ask me why: + monkeypatch.setattr(Recipe, 'get_recipe', staticmethod(mock_get_recipe)) @pytest.mark.parametrize('names,bootstrap', valid_combinations) @@ -30,18 +81,151 @@ def test_valid_recipe_order_and_bootstrap(names, bootstrap): def test_invalid_recipe_order_and_bootstrap(names, bootstrap): with pytest.raises(BuildInterruptingException) as e_info: get_recipe_order_and_bootstrap(ctx, names, bootstrap) - assert e_info.value.message == ( - "Didn't find any valid dependency graphs. " - "This means that some of your requirements pull in conflicting dependencies." + assert "conflict" in e_info.value.message.lower() + + +def test_blacklist(): + # First, get order without blacklist: + build_order, python_modules, bs = get_recipe_order_and_bootstrap( + ctx, ["python3", "kivy"], None + ) + # Now, obtain again with blacklist: + build_order_2, python_modules_2, bs_2 = get_recipe_order_and_bootstrap( + ctx, ["python3", "kivy"], None, blacklist=["libffi"] + ) + assert "libffi" not in build_order_2 + assert set(build_order_2).union({"libffi"}) == set(build_order) + + # Check that we get a conflict when using webview and kivy combined: + wbootstrap = Bootstrap.get_bootstrap('webview', ctx) + with pytest.raises(BuildInterruptingException) as e_info: + get_recipe_order_and_bootstrap(ctx, ["flask", "kivy"], wbootstrap) + assert "conflict" in e_info.value.message.lower() + + # We should no longer get a conflict blacklisting sdl2 and pygame: + get_recipe_order_and_bootstrap( + ctx, ["flask", "kivy"], wbootstrap, blacklist=["sdl2", "pygame"] ) +def test_get_dependency_tuple_list_for_recipe(monkeypatch): + r = get_fake_recipe("recipe1", depends=[ + "libffi", + ("libffi", "Pillow") + ]) + dep_list = get_dependency_tuple_list_for_recipe( + r, blacklist={"libffi"} + ) + assert(dep_list == [("pillow",)]) + + +@pytest.mark.parametrize('names,bootstrap', valid_combinations) +def test_valid_obvious_conflict_checker(names, bootstrap): + # Note: obvious_conflict_checker is stricter on input + # (needs fix_deplist) than get_recipe_order_and_bootstrap! + obvious_conflict_checker(ctx, fix_deplist(names)) + + +@pytest.mark.parametrize('names,bootstrap', + invalid_combinations_simple # see above for why this + ) # is a separate list +def test_invalid_obvious_conflict_checker(names, bootstrap): + # Note: obvious_conflict_checker is stricter on input + # (needs fix_deplist) than get_recipe_order_and_bootstrap! + with pytest.raises(BuildInterruptingException) as e_info: + obvious_conflict_checker(ctx, fix_deplist(names)) + assert "conflict" in e_info.value.message.lower() + + +def test_misc_obvious_conflict_checker(monkeypatch): + # Check that the assert about wrong input data is hit: + with pytest.raises(AssertionError) as e_info: + obvious_conflict_checker( + ctx, + ["this_is_invalid"] + # (invalid because it isn't properly nested as tuple) + ) + + # Test that non-recipe dependencies work in overall: + obvious_conflict_checker( + ctx, fix_deplist(["python3", "notarecipelibrary"]) + ) + + # Test that a conflict with a non-recipe dependency works: + # This is currently not used, so we need a custom test recipe: + # To get that, we simply modify one! + with monkeypatch.context() as m: + register_fake_recipes_for_test(m, [ + get_fake_recipe("recipe1", conflicts=[("fakelib")]), + ]) + with pytest.raises(BuildInterruptingException) as e_info: + obvious_conflict_checker(ctx, fix_deplist(["recipe1", "fakelib"])) + assert "conflict" in e_info.value.message.lower() + + # Test a case where a recipe pulls in a conditional tuple + # of additional dependencies. This is e.g. done for ('python3', + # 'python2', ...) but most recipes don't depend on this anymore, + # so we need to add a manual test for this case: + with monkeypatch.context() as m: + register_fake_recipes_for_test(m, [ + get_fake_recipe("recipe1", depends=[("libffi", "Pillow")]), + ]) + obvious_conflict_checker(ctx, fix_deplist(["recipe1"])) + + +def test_indirectconflict_obvious_conflict_checker(monkeypatch): + # Test a case where there's an indirect conflict, which also + # makes sure the error message correctly blames the OUTER recipes + # as original conflict source: + with monkeypatch.context() as m: + register_fake_recipes_for_test(m, [ + get_fake_recipe("outerrecipe1", depends=["innerrecipe1"]), + get_fake_recipe("outerrecipe2", depends=["innerrecipe2"]), + get_fake_recipe("innerrecipe1"), + get_fake_recipe("innerrecipe2", conflicts=["innerrecipe1"]), + ]) + with pytest.raises(BuildInterruptingException) as e_info: + obvious_conflict_checker( + ctx, + fix_deplist(["outerrecipe1", "outerrecipe2"]) + ) + assert ("conflict" in e_info.value.message.lower() and + "outerrecipe1" in e_info.value.message.lower() and + "outerrecipe2" in e_info.value.message.lower()) + + +def test_multichoice_obvious_conflict_checker(monkeypatch): + # Test a case where there's a conflict with a multi-choice tuple: + with monkeypatch.context() as m: + register_fake_recipes_for_test(m, [ + get_fake_recipe("recipe1", conflicts=["lib1", "lib2"]), + get_fake_recipe("recipe2", depends=[("lib1", "lib2")]), + ]) + with pytest.raises(BuildInterruptingException) as e_info: + obvious_conflict_checker( + ctx, + fix_deplist([("lib1", "lib2"), "recipe1"]) + ) + assert "conflict" in e_info.value.message.lower() + + def test_bootstrap_dependency_addition(): build_order, python_modules, bs = get_recipe_order_and_bootstrap( ctx, ['kivy'], None) assert (('hostpython2' in build_order) or ('hostpython3' in build_order)) +def test_graph_deplist_transformation(): + test_pairs = [ + (["Pillow", ('python2', 'python3')], + [('pillow',), ('python2', 'python3')]), + (["Pillow", ('python2',)], + [('pillow',), ('python2',)]), + ] + for (before_list, after_list) in test_pairs: + assert fix_deplist(before_list) == after_list + + def test_bootstrap_dependency_addition2(): build_order, python_modules, bs = get_recipe_order_and_bootstrap( ctx, ['kivy', 'python2'], None) diff --git a/tests/test_logger.py b/tests/test_logger.py new file mode 100644 index 0000000000..8212b4596f --- /dev/null +++ b/tests/test_logger.py @@ -0,0 +1,18 @@ +import unittest +from mock import MagicMock +from pythonforandroid import logger + + +class TestShprint(unittest.TestCase): + + def test_unicode_encode(self): + """ + Makes sure `shprint()` can handle unicode command output. + Running the test with PYTHONIOENCODING=ASCII env would fail, refs: + https://github.com/kivy/python-for-android/issues/1654 + """ + expected_command_output = ["foo\xa0bar"] + command = MagicMock() + command.return_value = expected_command_output + output = logger.shprint(command, 'a1', k1='k1') + self.assertEqual(output, expected_command_output) diff --git a/tests/test_recipe.py b/tests/test_recipe.py new file mode 100644 index 0000000000..d46a6e9ef8 --- /dev/null +++ b/tests/test_recipe.py @@ -0,0 +1,43 @@ +import types +import unittest +from pythonforandroid.build import Context +from pythonforandroid.recipe import Recipe + + +class TestRecipe(unittest.TestCase): + + def test_recipe_dirs(self): + """ + Trivial `recipe_dirs()` test. + Makes sure the list is not empty and has the root directory. + """ + ctx = Context() + recipes_dir = Recipe.recipe_dirs(ctx) + # by default only the root dir `recipes` directory + self.assertEqual(len(recipes_dir), 1) + self.assertTrue(recipes_dir[0].startswith(ctx.root_dir)) + + def test_list_recipes(self): + """ + Trivial test verifying list_recipes returns a generator with some recipes. + """ + ctx = Context() + recipes = Recipe.list_recipes(ctx) + self.assertTrue(isinstance(recipes, types.GeneratorType)) + recipes = list(recipes) + self.assertIn('python3', recipes) + + def test_get_recipe(self): + """ + Makes sure `get_recipe()` returns a `Recipe` object when possible. + """ + ctx = Context() + recipe_name = 'python3' + recipe = Recipe.get_recipe(recipe_name, ctx) + self.assertTrue(isinstance(recipe, Recipe)) + self.assertEqual(recipe.name, recipe_name) + recipe_name = 'does_not_exist' + with self.assertRaises(ValueError) as e: + Recipe.get_recipe(recipe_name, ctx) + self.assertEqual( + e.exception.args[0], 'Recipe does not exist: {}'.format(recipe_name)) diff --git a/tox.ini b/tox.ini index 00ac584626..74cdc3940b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = pep8,py27,py3 +basepython = python3 [testenv] deps = @@ -17,6 +18,6 @@ commands = flake8 pythonforandroid/ tests/ ci/ ignore = E123, E124, E126, E226, - E402, E501, E722, - F812, F841, W503, + E402, E501, + W503, W504