diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1cacf53..ead010f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Publish Assets on: push: - branches: [master] + branches: [master, upgrade_p4a_clean] jobs: build_arm64_aar: @@ -16,20 +16,14 @@ jobs: cp -r /root/.buildozer ~/.buildozer/ - name: setup run: | + apt update + apt install libssl-dev zip unzip openjdk-11-jdk -y export B_VERSION=$(cat $GITHUB_WORKSPACE/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+') echo "NEXUS_SIGNING_KEYRING_FILE=$GITHUB_WORKSPACE/signing2.pgp" >> $GITHUB_ENV echo "BUILD_VERSION=${B_VERSION}" >> $GITHUB_ENV export PATH=/usr/bin:$PATH - wget -q 'https://eu.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/ - tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/ - rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 - ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 - cp -f $GITHUB_WORKSPACE/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh - cp -f $GITHUB_WORKSPACE/scripts/mangled-glibc-syscalls__arm64.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm64/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h - cp -f $GITHUB_WORKSPACE/scripts/build-binary.mk ~/.buildozer/android/crystax-ndk-10.3.2/build/core/build-binary.mk - rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/sources/sqlite - cp -Rf $GITHUB_WORKSPACE/scripts/crystax-sources/sqlite ~/.buildozer/android/crystax-ndk-10.3.2/sources/sqlite - rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz + wget -q 'https://dl.google.com/android/repository/android-ndk-r25b-linux.zip' -P ~/.buildozer/android/ + unzip ~/.buildozer/android/android-ndk-r25b-linux.zip -d ~/.buildozer/android/ mv buildozer.spec.arm64.ci buildozer.spec chmod u+x ./build-release.sh - name: build release @@ -77,6 +71,7 @@ jobs: echo "NEXUS_SIGNING_KEYRING_FILE=$GITHUB_WORKSPACE/signing2.pgp" >> $GITHUB_ENV export PATH=/usr/bin:$PATH wget -q 'https://eu.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/ + unzip ~/ tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/ rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 diff --git a/.gitignore b/.gitignore index b39ffe8..0713181 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json p4a/*.apk p4a/*.aar +venv \ No newline at end of file diff --git a/buildozer.spec.arm64.ci b/buildozer.spec.arm64.ci index 94797f8..2570999 100644 --- a/buildozer.spec.arm64.ci +++ b/buildozer.spec.arm64.ci @@ -39,8 +39,9 @@ version.filename = %(source.dir)s/main.py # (list) Application requirements # comma seperated e.g. requirements = sqlite3,kivy -requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.5.30, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.17, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry" - +requirements = openssl, sqlite3, android, distro==1.4.0, pyjnius, certifi==2021.5.30, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.17, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, cffi, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry" +# requirements = coincurve, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry" +# python3crystax, hostpython3crystax, # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes # requirements.source.kivy = ../../kivy @@ -101,13 +102,15 @@ android.minapi = 21 android.sdk = 23 # (str) Android NDK version to use -#android.ndk = 13b +android.ndk = 25b # (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 = ~/.buildozer/android/crystax-ndk-10.3.2 +# android.ndk_path = ~/.buildozer/android/crystax-ndk-10.3.2 +android.ndk_path = ~/.buildozer/android/android-ndk-r25b + # (str) Android SDK directory (if empty, it will be automatically downloaded.) #android.sdk_path = ~/.buildozer/android diff --git a/docker/Dockerfile.python39.platform-28 b/docker/Dockerfile.python39.platform-28 index 6607f07..4ad2688 100644 --- a/docker/Dockerfile.python39.platform-28 +++ b/docker/Dockerfile.python39.platform-28 @@ -24,6 +24,7 @@ RUN wget 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildoz RUN tar -xvf ~/.buildozer/android/platform/android-sdk_r23-linux.tgz -C ~/.buildozer/android/platform/ && \ mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk && \ unzip ~/.buildozer/android/platform/platform-28_r06.zip -d ~/.buildozer/android/platform/android-sdk/platforms && \ + # android 9 (pie) comes with api level 28. mv ~/.buildozer/android/platform/android-sdk/platforms/android-9 ~/.buildozer/android/platform/android-sdk/platforms/android-28 && \ mkdir -p ~/.buildozer/android/platform/android-sdk/build-tools && \ unzip ~/.buildozer/android/platform/build-tools_r28.0.3-linux.zip -d ~/.buildozer/android/platform/android-sdk/build-tools && \ @@ -45,3 +46,10 @@ RUN git clone https://github.com/lbryio/buildozer.git RUN cd buildozer && python setup.py install && cd .. CMD ["/bin/bash"] + +# result is +# ./root/.buildozer/android/platform/android-sdk/[build-tools,cmdline-tools,licenses,platforms,tools] +# ...build-tools/28.0.3/ +# ...cmdline-tools/5.0/[bin,lib,source.properties] +# ...platforms/android-28/[] +# ...tools/[] diff --git a/p4a/pythonforandroid/__init__.py b/p4a/pythonforandroid/__init__.py index 27f4493..f39a847 100644 --- a/p4a/pythonforandroid/__init__.py +++ b/p4a/pythonforandroid/__init__.py @@ -1,2 +1 @@ - -__version__ = '0.5' +__version__ = '2022.09.04' diff --git a/p4a/pythonforandroid/androidndk.py b/p4a/pythonforandroid/androidndk.py new file mode 100644 index 0000000..83cb355 --- /dev/null +++ b/p4a/pythonforandroid/androidndk.py @@ -0,0 +1,83 @@ +import sys +import os + + +class AndroidNDK: + """ + This class is used to get the current NDK information. + """ + + ndk_dir = "" + + def __init__(self, ndk_dir): + self.ndk_dir = ndk_dir + + @property + def host_tag(self): + """ + Returns the host tag for the current system. + Note: The host tag is ``darwin-x86_64`` even on Apple Silicon macs. + """ + return f"{sys.platform}-x86_64" + + @property + def llvm_prebuilt_dir(self): + return os.path.join( + self.ndk_dir, "toolchains", "llvm", "prebuilt", self.host_tag + ) + + @property + def llvm_bin_dir(self): + return os.path.join(self.llvm_prebuilt_dir, "bin") + + @property + def clang(self): + return os.path.join(self.llvm_bin_dir, "clang") + + @property + def clang_cxx(self): + return os.path.join(self.llvm_bin_dir, "clang++") + + @property + def llvm_binutils_prefix(self): + return os.path.join(self.llvm_bin_dir, "llvm-") + + @property + def llvm_ar(self): + return f"{self.llvm_binutils_prefix}ar" + + @property + def llvm_ranlib(self): + return f"{self.llvm_binutils_prefix}ranlib" + + @property + def llvm_objcopy(self): + return f"{self.llvm_binutils_prefix}objcopy" + + @property + def llvm_objdump(self): + return f"{self.llvm_binutils_prefix}objdump" + + @property + def llvm_readelf(self): + return f"{self.llvm_binutils_prefix}readelf" + + @property + def llvm_strip(self): + return f"{self.llvm_binutils_prefix}strip" + + @property + def sysroot(self): + return os.path.join(self.llvm_prebuilt_dir, "sysroot") + + @property + def sysroot_include_dir(self): + return os.path.join(self.sysroot, "usr", "include") + + @property + def sysroot_lib_dir(self): + return os.path.join(self.sysroot, "usr", "lib") + + @property + def libcxx_include_dir(self): + return os.path.join(self.sysroot_include_dir, "c++", "v1") diff --git a/p4a/pythonforandroid/archs.py b/p4a/pythonforandroid/archs.py index 09ebba4..b960ca6 100644 --- a/p4a/pythonforandroid/archs.py +++ b/p4a/pythonforandroid/archs.py @@ -1,22 +1,46 @@ from distutils.spawn import find_executable from os import environ -from os.path import (exists, join, dirname, split) -from glob import glob +from os.path import join +from multiprocessing import cpu_count from pythonforandroid.recipe import Recipe from pythonforandroid.util import BuildInterruptingException, build_platform -class Arch(object): - - toolchain_prefix = None - '''The prefix for the toolchain dir in the NDK.''' +class Arch: command_prefix = None '''The prefix for NDK commands such as gcc.''' + arch = "" + '''Name of the arch such as: `armeabi-v7a`, `arm64-v8a`, `x86`...''' + + arch_cflags = [] + '''Specific arch `cflags`, expect to be overwrote in subclass if needed.''' + + common_cflags = [ + '-target {target}', + '-fomit-frame-pointer' + ] + + common_cppflags = [ + '-DANDROID', + '-I{ctx.ndk.sysroot_include_dir}', + '-I{python_includes}', + ] + + common_ldflags = ['-L{ctx_libs_dir}'] + + common_ldlibs = ['-lm'] + + common_ldshared = [ + '-pthread', + '-shared', + '-Wl,-O1', + '-Wl,-Bsymbolic-functions', + ] + def __init__(self, ctx): - super(Arch, self).__init__() self.ctx = ctx # Allows injecting additional linker paths used by any recipe. @@ -28,6 +52,14 @@ class Arch(object): def __str__(self): return self.arch + @property + def ndk_lib_dir(self): + return join(self.ctx.ndk.sysroot_lib_dir, self.command_prefix) + + @property + def ndk_lib_dir_versioned(self): + return join(self.ndk_lib_dir, str(self.ctx.ndk_api)) + @property def include_dirs(self): return [ @@ -38,216 +70,235 @@ class Arch(object): @property def target(self): - target_data = self.command_prefix.split('-') - return '-'.join( - [target_data[0], 'none', target_data[1], target_data[2]]) + # As of NDK r19, the toolchains installed by default with the + # NDK may be used in-place. The make_standalone_toolchain.py script + # is no longer needed for interfacing with arbitrary build systems. + # See: https://developer.android.com/ndk/guides/other_build_systems + return '{triplet}{ndk_api}'.format( + triplet=self.command_prefix, ndk_api=self.ctx.ndk_api + ) - def get_env(self, with_flags_in_cc=True, clang=False): + @property + def clang_exe(self): + """Full path of the clang compiler depending on the android's ndk + version used.""" + return self.get_clang_exe() + + @property + def clang_exe_cxx(self): + """Full path of the clang++ compiler depending on the android's ndk + version used.""" + return self.get_clang_exe(plus_plus=True) + + def get_clang_exe(self, with_target=False, plus_plus=False): + """Returns the full path of the clang/clang++ compiler, supports two + kwargs: + + - `with_target`: prepend `target` to clang + - `plus_plus`: will return the clang++ compiler (defaults to `False`) + """ + compiler = 'clang' + if with_target: + compiler = '{target}-{compiler}'.format( + target=self.target, compiler=compiler + ) + if plus_plus: + compiler += '++' + return join(self.ctx.ndk.llvm_bin_dir, compiler) + + def get_env(self, with_flags_in_cc=True): env = {} - cflags = [ - '-DANDROID', - '-fomit-frame-pointer', - '-D__ANDROID_API__={}'.format(self.ctx.ndk_api)] - if not clang: - cflags.append('-mandroid') - else: - cflags.append('-target ' + self.target) - toolchain = '{android_host}-{toolchain_version}'.format( - android_host=self.ctx.toolchain_prefix, - toolchain_version=self.ctx.toolchain_version) - toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain, - 'prebuilt', build_platform) - cflags.append('-gcc-toolchain {}'.format(toolchain)) + # HOME: User's home directory + # + # Many tools including p4a store outputs in the user's home + # directory. This is found from the HOME environment variable + # and falls back to the system account database. Setting HOME + # can be used to globally divert these tools to use a different + # path. Furthermore, in containerized environments the user may + # not exist in the account database, so if HOME isn't set than + # these tools will fail. + if 'HOME' in environ: + env['HOME'] = environ['HOME'] - env['CFLAGS'] = ' '.join(cflags) + # CFLAGS/CXXFLAGS: the processor flags + env['CFLAGS'] = ' '.join(self.common_cflags).format(target=self.target) + if self.arch_cflags: + # each architecture may have has his own CFLAGS + env['CFLAGS'] += ' ' + ' '.join(self.arch_cflags) + env['CXXFLAGS'] = env['CFLAGS'] - # Link the extra global link paths first before anything else + # CPPFLAGS (for macros and includes) + env['CPPFLAGS'] = ' '.join(self.common_cppflags).format( + ctx=self.ctx, + command_prefix=self.command_prefix, + python_includes=join( + self.ctx.get_python_install_dir(self.arch), + 'include/python{}'.format(self.ctx.python_recipe.version[0:3]), + ), + ) + + # LDFLAGS: Link the extra global link paths first before anything else # (such that overriding system libraries with them is possible) - env['LDFLAGS'] = ' ' + " ".join([ - "-L'" + l.replace("'", "'\"'\"'") + "'" # no shlex.quote in py2 - for l in self.extra_global_link_paths - ]) + ' ' + env['LDFLAGS'] = ( + ' ' + + " ".join( + [ + "-L'" + + link_path.replace("'", "'\"'\"'") + + "'" # no shlex.quote in py2 + for link_path in self.extra_global_link_paths + ] + ) + + ' ' + ' '.join(self.common_ldflags).format( + ctx_libs_dir=self.ctx.get_libs_dir(self.arch) + ) + ) - sysroot = join(self.ctx._ndk_dir, 'sysroot') - if exists(sysroot): - # post-15 NDK per - # https://android.googlesource.com/platform/ndk/+/ndk-r15-release/docs/UnifiedHeaders.md - env['CFLAGS'] += ' -isystem {}/sysroot/usr/include/{}'.format( - self.ctx.ndk_dir, self.ctx.toolchain_prefix) - env['CFLAGS'] += ' -I{}/sysroot/usr/include/{}'.format( - self.ctx.ndk_dir, self.command_prefix) - else: - sysroot = self.ctx.ndk_platform - env['CFLAGS'] += ' -I{}'.format(self.ctx.ndk_platform) - env['CFLAGS'] += ' -isysroot {} '.format(sysroot) - env['CFLAGS'] += '-I' + join(self.ctx.get_python_install_dir(), - 'include/python{}'.format( - self.ctx.python_recipe.version[0:3]) - ) - - env['LDFLAGS'] += '--sysroot={} '.format(self.ctx.ndk_platform) - - env["CXXFLAGS"] = env["CFLAGS"] - - env["LDFLAGS"] += " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)]) - - if self.ctx.ndk == 'crystax': - env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch) - - toolchain_prefix = self.ctx.toolchain_prefix - toolchain_version = self.ctx.toolchain_version - command_prefix = self.command_prefix - - env['TOOLCHAIN_PREFIX'] = toolchain_prefix - env['TOOLCHAIN_VERSION'] = toolchain_version + # LDLIBS: Library flags or names given to compilers when they are + # supposed to invoke the linker. + env['LDLIBS'] = ' '.join(self.common_ldlibs) + # CCACHE ccache = '' if self.ctx.ccache and bool(int(environ.get('USE_CCACHE', '1'))): # print('ccache found, will optimize builds') ccache = self.ctx.ccache + ' ' env['USE_CCACHE'] = '1' env['NDK_CCACHE'] = self.ctx.ccache - env.update({k: v for k, v in environ.items() if k.startswith('CCACHE_')}) + env.update( + {k: v for k, v in environ.items() if k.startswith('CCACHE_')} + ) - if clang: - llvm_dirname = split( - glob(join(self.ctx.ndk_dir, 'toolchains', 'llvm*'))[-1])[-1] - clang_path = join(self.ctx.ndk_dir, 'toolchains', llvm_dirname, - 'prebuilt', build_platform, 'bin') - environ['PATH'] = '{clang_path}:{path}'.format( - clang_path=clang_path, path=environ['PATH']) - exe = join(clang_path, 'clang') - execxx = join(clang_path, 'clang++') - else: - exe = '{command_prefix}-gcc'.format(command_prefix=command_prefix) - execxx = '{command_prefix}-g++'.format(command_prefix=command_prefix) - - cc = find_executable(exe, path=environ['PATH']) + # Compiler: `CC` and `CXX` (and make sure that the compiler exists) + env['PATH'] = self.ctx.env['PATH'] + cc = find_executable(self.clang_exe, path=env['PATH']) if cc is None: - print('Searching path are: {!r}'.format(environ['PATH'])) + print('Searching path are: {!r}'.format(env['PATH'])) raise BuildInterruptingException( 'Couldn\'t find executable for CC. This indicates a ' 'problem locating the {} executable in the Android ' 'NDK, not that you don\'t have a normal compiler ' - 'installed. Exiting.'.format(exe)) + 'installed. Exiting.'.format(self.clang_exe)) if with_flags_in_cc: env['CC'] = '{ccache}{exe} {cflags}'.format( - exe=exe, + exe=self.clang_exe, ccache=ccache, cflags=env['CFLAGS']) env['CXX'] = '{ccache}{execxx} {cxxflags}'.format( - execxx=execxx, + execxx=self.clang_exe_cxx, ccache=ccache, cxxflags=env['CXXFLAGS']) else: env['CC'] = '{ccache}{exe}'.format( - exe=exe, + exe=self.clang_exe, ccache=ccache) env['CXX'] = '{ccache}{execxx}'.format( - execxx=execxx, + execxx=self.clang_exe_cxx, ccache=ccache) - env['AR'] = '{}-ar'.format(command_prefix) - env['RANLIB'] = '{}-ranlib'.format(command_prefix) - env['LD'] = '{}-ld'.format(command_prefix) - env['LDSHARED'] = env["CC"] + " -pthread -shared " +\ - "-Wl,-O1 -Wl,-Bsymbolic-functions " - if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax: - # For crystax python, we can't use the host python headers: - env["CFLAGS"] += ' -I{}/sources/python/{}/include/python/'.\ - format(self.ctx.ndk_dir, self.ctx.python_recipe.version[0:3]) - env['STRIP'] = '{}-strip --strip-unneeded'.format(command_prefix) - env['MAKE'] = 'make -j5' - env['READELF'] = '{}-readelf'.format(command_prefix) - env['NM'] = '{}-nm'.format(command_prefix) + # Android's LLVM binutils + env['AR'] = self.ctx.ndk.llvm_ar + env['RANLIB'] = self.ctx.ndk.llvm_ranlib + env['STRIP'] = f'{self.ctx.ndk.llvm_strip} --strip-unneeded' + env['READELF'] = self.ctx.ndk.llvm_readelf + env['OBJCOPY'] = self.ctx.ndk.llvm_objcopy + env['MAKE'] = 'make -j{}'.format(str(cpu_count())) + + # Android's arch/toolchain + env['ARCH'] = self.arch + env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api)) + + # Custom linker options + env['LDSHARED'] = env['CC'] + ' ' + ' '.join(self.common_ldshared) + + # Host python (used by some recipes) hostpython_recipe = Recipe.get_recipe( 'host' + self.ctx.python_recipe.name, self.ctx) env['BUILDLIB_PATH'] = join( hostpython_recipe.get_build_dir(self.arch), - 'build', 'lib.{}-{}'.format( - build_platform, self.ctx.python_recipe.major_minor_version_string) + 'native-build', + 'build', + 'lib.{}-{}'.format( + build_platform, + self.ctx.python_recipe.major_minor_version_string, + ), ) - env['PATH'] = environ['PATH'] - - env['ARCH'] = self.arch - env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api)) - - if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax: - env['CRYSTAX_PYTHON_VERSION'] = self.ctx.python_recipe.version + # for reproducible builds + if 'SOURCE_DATE_EPOCH' in environ: + for k in 'LC_ALL TZ SOURCE_DATE_EPOCH PYTHONHASHSEED BUILD_DATE BUILD_TIME'.split(): + if k in environ: + env[k] = environ[k] return env class ArchARM(Arch): arch = "armeabi" - toolchain_prefix = 'arm-linux-androideabi' command_prefix = 'arm-linux-androideabi' - platform_dir = 'arch-arm' @property def target(self): target_data = self.command_prefix.split('-') - return '-'.join( - ['armv7a', 'none', target_data[1], target_data[2]]) + return '{triplet}{ndk_api}'.format( + triplet='-'.join(['armv7a', target_data[1], target_data[2]]), + ndk_api=self.ctx.ndk_api, + ) class ArchARMv7_a(ArchARM): arch = 'armeabi-v7a' - - def get_env(self, with_flags_in_cc=True, clang=False): - env = super(ArchARMv7_a, self).get_env(with_flags_in_cc, clang=clang) - env['CFLAGS'] = (env['CFLAGS'] + - (' -march=armv7-a -mfloat-abi=softfp ' - '-mfpu=vfp -mthumb')) - env['CXXFLAGS'] = env['CFLAGS'] - return env + arch_cflags = [ + '-march=armv7-a', + '-mfloat-abi=softfp', + '-mfpu=vfp', + '-mthumb', + '-fPIC', + ] class Archx86(Arch): arch = 'x86' - toolchain_prefix = 'x86' command_prefix = 'i686-linux-android' - platform_dir = 'arch-x86' - - def get_env(self, with_flags_in_cc=True, clang=False): - env = super(Archx86, self).get_env(with_flags_in_cc, clang=clang) - env['CFLAGS'] = (env['CFLAGS'] + - ' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32') - env['CXXFLAGS'] = env['CFLAGS'] - return env + arch_cflags = [ + '-march=i686', + '-mssse3', + '-mfpmath=sse', + '-m32', + '-fPIC', + ] class Archx86_64(Arch): arch = 'x86_64' - toolchain_prefix = 'x86_64' command_prefix = 'x86_64-linux-android' - platform_dir = 'arch-x86_64' - - def get_env(self, with_flags_in_cc=True, clang=False): - env = super(Archx86_64, self).get_env(with_flags_in_cc, clang=clang) - env['CFLAGS'] = (env['CFLAGS'] + - ' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel') - env['CXXFLAGS'] = env['CFLAGS'] - return env + arch_cflags = [ + '-march=x86-64', + '-msse4.2', + '-mpopcnt', + '-m64', + '-fPIC', + ] class ArchAarch_64(Arch): arch = 'arm64-v8a' - toolchain_prefix = 'aarch64-linux-android' command_prefix = 'aarch64-linux-android' - platform_dir = 'arch-arm64' + arch_cflags = [ + '-march=armv8-a', + '-fPIC' + # '-I' + join(dirname(__file__), 'includes', 'arm64-v8a'), + ] - def get_env(self, with_flags_in_cc=True, clang=False): - env = super(ArchAarch_64, self).get_env(with_flags_in_cc, clang=clang) - incpath = ' -I' + join(dirname(__file__), 'includes', 'arm64-v8a') - env['EXTRA_CFLAGS'] = incpath - env['CFLAGS'] += incpath - env['CXXFLAGS'] += incpath - if with_flags_in_cc: - env['CC'] += incpath - env['CXX'] += incpath - return env + # Note: This `EXTRA_CFLAGS` below should target the commented `include` + # above in `arch_cflags`. The original lines were added during the Sdl2's + # bootstrap creation, and modified/commented during the migration to the + # NDK r19 build system, because it seems that we don't need it anymore, + # do we need them? + # def get_env(self, with_flags_in_cc=True): + # env = super().get_env(with_flags_in_cc) + # env['EXTRA_CFLAGS'] = self.arch_cflags[-1] + # return env diff --git a/p4a/pythonforandroid/bdistapk.py b/p4a/pythonforandroid/bdistapk.py index a27f4d1..bcf77cd 100644 --- a/p4a/pythonforandroid/bdistapk.py +++ b/p4a/pythonforandroid/bdistapk.py @@ -1,6 +1,4 @@ -from __future__ import print_function from setuptools import Command -from pythonforandroid import toolchain import sys from os.path import realpath, join, exists, dirname, curdir, basename, split @@ -16,16 +14,16 @@ def argv_contains(t): return False -class BdistAPK(Command): - description = 'Create an APK with python-for-android' +class Bdist(Command): user_options = [] + package_type = None def initialize_options(self): for option in self.user_options: setattr(self, option[0].strip('=').replace('-', '_'), None) - option_dict = self.distribution.get_option_dict('apk') + option_dict = self.distribution.get_option_dict(self.package_type) # This is a hack, we probably aren't supposed to loop through # the option_dict so early because distutils does exactly the @@ -34,10 +32,9 @@ class BdistAPK(Command): for (option, (source, value)) in option_dict.items(): setattr(self, option, str(value)) - def finalize_options(self): - setup_options = self.distribution.get_option_dict('apk') + setup_options = self.distribution.get_option_dict(self.package_type) for (option, (source, value)) in setup_options.items(): if source == 'command line': continue @@ -70,16 +67,15 @@ class BdistAPK(Command): sys.argv.append('--version={}'.format(version)) if not argv_contains('--arch'): - arch = 'arm64-v8a' + arch = 'armeabi-v7a' self.arch = arch sys.argv.append('--arch={}'.format(arch)) def run(self): - self.prepare_build_dir() - from pythonforandroid.toolchain import main - sys.argv[1] = 'apk' + from pythonforandroid.entrypoints import main + sys.argv[1] = self.package_type main() def prepare_build_dir(self): @@ -112,7 +108,7 @@ class BdistAPK(Command): makedirs(new_dir) print('Including {}'.format(filen)) copyfile(filen, join(bdist_dir, filen)) - if basename(filen) in ('main.py', 'main.pyo'): + if basename(filen) in ('main.py', 'main.pyc'): main_py_dirs.append(filen) # This feels ridiculous, but how else to define the main.py dir? @@ -123,7 +119,7 @@ class BdistAPK(Command): exit(1) if len(main_py_dirs) > 1: print('WARNING: Multiple main.py dirs found, using the shortest path') - main_py_dirs.sort(key=lambda j: len(split(j))) + main_py_dirs = sorted(main_py_dirs, key=lambda j: len(split(j))) if not argv_contains('--launcher'): sys.argv.append('--private={}'.format( @@ -131,18 +127,39 @@ class BdistAPK(Command): ) +class BdistAPK(Bdist): + """distutil command handler for 'apk'.""" + description = 'Create an APK with python-for-android' + package_type = 'apk' + + +class BdistAAR(Bdist): + """distutil command handler for 'aar'.""" + description = 'Create an AAR with python-for-android' + package_type = 'aar' + + +class BdistAAB(Bdist): + """distutil command handler for 'aab'.""" + description = 'Create an AAB with python-for-android' + package_type = 'aab' + + def _set_user_options(): # This seems like a silly way to do things, but not sure if there's a # better way to pass arbitrary options onwards to p4a - user_options = [('requirements=', None, None),] + user_options = [('requirements=', None, None), ] for i, arg in enumerate(sys.argv): if arg.startswith('--'): if ('=' in arg or - (i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))): + (i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))): user_options.append((arg[2:].split('=')[0] + '=', None, None)) else: user_options.append((arg[2:], None, None)) BdistAPK.user_options = user_options + BdistAAB.user_options = user_options + BdistAAR.user_options = user_options + _set_user_options() diff --git a/p4a/pythonforandroid/bootstrap.py b/p4a/pythonforandroid/bootstrap.py old mode 100644 new mode 100755 index b4a9a9e..d587347 --- a/p4a/pythonforandroid/bootstrap.py +++ b/p4a/pythonforandroid/bootstrap.py @@ -1,20 +1,20 @@ +import functools +import glob +import importlib +import os from os.path import (join, dirname, isdir, normpath, splitext, basename) from os import listdir, walk, sep import sh import shlex -import glob -import importlib -import os import shutil -from pythonforandroid.logger import (warning, shprint, info, logger, - debug) -from pythonforandroid.util import (current_directory, ensure_dir, - temp_directory) +from pythonforandroid.logger import (shprint, info, logger, debug) +from pythonforandroid.util import ( + current_directory, ensure_dir, temp_directory, BuildInterruptingException) from pythonforandroid.recipe import Recipe -def copy_files(src_root, dest_root, override=True): +def copy_files(src_root, dest_root, override=True, symlink=False): for root, dirnames, filenames in walk(src_root): for filename in filenames: subdir = normpath(root.replace(src_root, "")) @@ -29,12 +29,44 @@ def copy_files(src_root, dest_root, override=True): if override and os.path.exists(dest_file): os.unlink(dest_file) if not os.path.exists(dest_file): - shutil.copy(src_file, dest_file) + if symlink: + os.symlink(src_file, dest_file) + else: + shutil.copy(src_file, dest_file) else: os.makedirs(dest_file) -class Bootstrap(object): +default_recipe_priorities = [ + "webview", "sdl2", "service_only" # last is highest +] +# ^^ NOTE: these are just the default priorities if no special rules +# apply (which you can find in the code below), so basically if no +# known graphical lib or web lib is used - in which case service_only +# is the most reasonable guess. + + +def _cmp_bootstraps_by_priority(a, b): + def rank_bootstrap(bootstrap): + """ Returns a ranking index for each bootstrap, + with higher priority ranked with higher number. """ + if bootstrap.name in default_recipe_priorities: + return default_recipe_priorities.index(bootstrap.name) + 1 + return 0 + + # Rank bootstraps in order: + rank_a = rank_bootstrap(a) + rank_b = rank_bootstrap(b) + if rank_a != rank_b: + return (rank_b - rank_a) + else: + if a.name < b.name: # alphabetic sort for determinism + return -1 + else: + return 1 + + +class Bootstrap: '''An Android project template, containing recipe stuff for compilation and templated fields for APK info. ''' @@ -45,15 +77,11 @@ class Bootstrap(object): bootstrap_dir = None build_dir = None - dist_dir = None dist_name = None distribution = None # All bootstraps should include Python in some way: - recipe_depends = [ - ("python2", "python2legacy", "python3", "python3crystax"), - 'android', - ] + recipe_depends = ['python3', 'android'] can_be_chosen_automatically = True '''Determines whether the bootstrap can be chosen as one that @@ -70,9 +98,9 @@ class Bootstrap(object): def dist_dir(self): '''The dist dir at which to place the finished distribution.''' if self.distribution is None: - warning('Tried to access {}.dist_dir, but {}.distribution ' - 'is None'.format(self, self)) - exit(1) + raise BuildInterruptingException( + 'Internal error: tried to access {}.dist_dir, but {}.distribution ' + 'is None'.format(self, self)) return self.distribution.dist_dir @property @@ -84,7 +112,7 @@ class Bootstrap(object): and optional dependencies are being used, and returns a list of these.''' recipes = [] - built_recipes = self.ctx.recipe_build_order + built_recipes = self.ctx.recipe_build_order or [] for recipe in self.recipe_depends: if isinstance(recipe, (tuple, list)): for alternative in recipe: @@ -104,70 +132,102 @@ class Bootstrap(object): def get_dist_dir(self, name): return join(self.ctx.dist_dir, name) - def get_common_dir(self): - return os.path.abspath(join(self.bootstrap_dir, "..", 'common')) - @property def name(self): modname = self.__class__.__module__ return modname.split(".", 2)[-1] + def get_bootstrap_dirs(self): + """get all bootstrap directories, following the MRO path""" + + # get all bootstrap names along the __mro__, cutting off Bootstrap and object + classes = self.__class__.__mro__[:-2] + bootstrap_names = [cls.name for cls in classes] + ['common'] + bootstrap_dirs = [ + join(self.ctx.root_dir, 'bootstraps', bootstrap_name) + for bootstrap_name in reversed(bootstrap_names) + ] + return bootstrap_dirs + + def _copy_in_final_files(self): + if self.name == "sdl2": + # Get the paths for copying SDL2's java source code: + sdl2_recipe = Recipe.get_recipe("sdl2", self.ctx) + sdl2_build_dir = sdl2_recipe.get_jni_dir() + src_dir = join(sdl2_build_dir, "SDL", "android-project", + "app", "src", "main", "java", + "org", "libsdl", "app") + target_dir = join(self.dist_dir, 'src', 'main', 'java', 'org', + 'libsdl', 'app') + + # Do actual copying: + info('Copying in SDL2 .java files from: ' + str(src_dir)) + if not os.path.exists(target_dir): + os.makedirs(target_dir) + copy_files(src_dir, target_dir, override=True) + def prepare_build_dir(self): - '''Ensure that a build dir exists for the recipe. This same single - dir will be used for building all different archs.''' + """Ensure that a build dir exists for the recipe. This same single + dir will be used for building all different archs.""" + bootstrap_dirs = self.get_bootstrap_dirs() + # now do a cumulative copy of all bootstrap dirs self.build_dir = self.get_build_dir() - self.common_dir = self.get_common_dir() - copy_files(join(self.bootstrap_dir, 'build'), self.build_dir) - copy_files(join(self.common_dir, 'build'), self.build_dir, - override=False) - if self.ctx.symlink_java_src: - info('Symlinking java src instead of copying') - shprint(sh.rm, '-r', join(self.build_dir, 'src')) - shprint(sh.mkdir, join(self.build_dir, 'src')) - for dirn in listdir(join(self.bootstrap_dir, 'build', 'src')): - shprint(sh.ln, '-s', join(self.bootstrap_dir, 'build', 'src', dirn), - join(self.build_dir, 'src')) + for bootstrap_dir in bootstrap_dirs: + copy_files(join(bootstrap_dir, 'build'), self.build_dir, symlink=self.ctx.symlink_bootstrap_files) + with current_directory(self.build_dir): with open('project.properties', 'w') as fileh: fileh.write('target=android-{}'.format(self.ctx.android_api)) - def prepare_dist_dir(self, name): + def prepare_dist_dir(self): ensure_dir(self.dist_dir) - def run_distribute(self): + def assemble_distribution(self): + ''' Copies all the files into the distribution (this function is + overridden by the specific bootstrap classes to do this) + and add in the distribution info. + ''' + self._copy_in_final_files() self.distribution.save_info(self.dist_dir) @classmethod - def list_bootstraps(cls): + def all_bootstraps(cls): '''Find all the available bootstraps and return them.''' forbidden_dirs = ('__pycache__', 'common') bootstraps_dir = join(dirname(__file__), 'bootstraps') + result = set() for name in listdir(bootstraps_dir): if name in forbidden_dirs: continue filen = join(bootstraps_dir, name) if isdir(filen): - yield name + result.add(name) + return result @classmethod - def get_bootstrap_from_recipes(cls, recipes, ctx): - '''Returns a bootstrap whose recipe requirements do not conflict with - the given recipes.''' + def get_usable_bootstraps_for_recipes(cls, recipes, ctx): + '''Returns all bootstrap whose recipe requirements do not conflict + with the given recipes, in no particular order.''' info('Trying to find a bootstrap that matches the given recipes.') bootstraps = [cls.get_bootstrap(name, ctx) - for name in cls.list_bootstraps()] - acceptable_bootstraps = [] + for name in cls.all_bootstraps()] + acceptable_bootstraps = set() + + # Find out which bootstraps are acceptable: for bs in bootstraps: if not bs.can_be_chosen_automatically: continue - possible_dependency_lists = expand_dependencies(bs.recipe_depends) + possible_dependency_lists = expand_dependencies(bs.recipe_depends, ctx) for possible_dependencies in possible_dependency_lists: ok = True + # Check if the bootstap's dependencies have an internal conflict: for recipe in possible_dependencies: recipe = Recipe.get_recipe(recipe, ctx) - if any([conflict in recipes for conflict in recipe.conflicts]): + if any(conflict in recipes for conflict in recipe.conflicts): ok = False break + # Check if bootstrap's dependencies conflict with chosen + # packages: for recipe in recipes: try: recipe = Recipe.get_recipe(recipe, ctx) @@ -175,19 +235,63 @@ class Bootstrap(object): conflicts = [] else: conflicts = recipe.conflicts - if any([conflict in possible_dependencies - for conflict in conflicts]): + if any(conflict in possible_dependencies + for conflict in conflicts): ok = False break if ok and bs not in acceptable_bootstraps: - acceptable_bootstraps.append(bs) + acceptable_bootstraps.add(bs) + info('Found {} acceptable bootstraps: {}'.format( len(acceptable_bootstraps), [bs.name for bs in acceptable_bootstraps])) - if acceptable_bootstraps: - info('Using the first of these: {}' - .format(acceptable_bootstraps[0].name)) - return acceptable_bootstraps[0] + return acceptable_bootstraps + + @classmethod + def get_bootstrap_from_recipes(cls, recipes, ctx): + '''Picks a single recommended default bootstrap out of + all_usable_bootstraps_from_recipes() for the given reicpes, + and returns it.''' + + known_web_packages = {"flask"} # to pick webview over service_only + recipes_with_deps_lists = expand_dependencies(recipes, ctx) + acceptable_bootstraps = cls.get_usable_bootstraps_for_recipes( + recipes, ctx + ) + + def have_dependency_in_recipes(dep): + for dep_list in recipes_with_deps_lists: + if dep in dep_list: + return True + return False + + # Special rule: return SDL2 bootstrap if there's an sdl2 dep: + if (have_dependency_in_recipes("sdl2") and + "sdl2" in [b.name for b in acceptable_bootstraps] + ): + info('Using sdl2 bootstrap since it is in dependencies') + return cls.get_bootstrap("sdl2", ctx) + + # Special rule: return "webview" if we depend on common web recipe: + for possible_web_dep in known_web_packages: + if have_dependency_in_recipes(possible_web_dep): + # We have a web package dep! + if "webview" in [b.name for b in acceptable_bootstraps]: + info('Using webview bootstrap since common web packages ' + 'were found {}'.format( + known_web_packages.intersection(recipes) + )) + return cls.get_bootstrap("webview", ctx) + + prioritized_acceptable_bootstraps = sorted( + list(acceptable_bootstraps), + key=functools.cmp_to_key(_cmp_bootstraps_by_priority) + ) + + if prioritized_acceptable_bootstraps: + info('Using the highest ranked/first of these: {}' + .format(prioritized_acceptable_bootstraps[0].name)) + return prioritized_acceptable_bootstraps[0] return None @classmethod @@ -218,15 +322,16 @@ class Bootstrap(object): tgt_dir = join(dest_dir, arch.arch) ensure_dir(tgt_dir) for src_dir in src_dirs: - for lib in glob.glob(join(src_dir, wildcard)): - shprint(sh.cp, '-a', lib, tgt_dir) + libs = glob.glob(join(src_dir, wildcard)) + if libs: + shprint(sh.cp, '-a', *libs, tgt_dir) def distribute_javaclasses(self, javaclass_dir, dest_dir="src"): '''Copy existing javaclasses from build dir to current dist dir.''' info('Copying java files') ensure_dir(dest_dir) - for filename in glob.glob(javaclass_dir): - shprint(sh.cp, '-a', filename, dest_dir) + filenames = glob.glob(javaclass_dir) + shprint(sh.cp, '-a', *filenames, dest_dir) def distribute_aars(self, arch): '''Process existing .aar bundles and copy to current dist dir.''' @@ -259,24 +364,19 @@ class Bootstrap(object): debug(" to {}".format(so_tgt_dir)) ensure_dir(so_tgt_dir) so_files = glob.glob(join(so_src_dir, '*.so')) - for f in so_files: - shprint(sh.cp, '-a', f, so_tgt_dir) + shprint(sh.cp, '-a', *so_files, so_tgt_dir) def strip_libraries(self, arch): info('Stripping libraries') - if self.ctx.python_recipe.from_crystax: - info('Python was loaded from CrystaX, skipping strip') - return env = arch.get_env() tokens = shlex.split(env['STRIP']) strip = sh.Command(tokens[0]) + logger.info(f'Strip Env {env["STRIP"]} strip {strip} env {env}') if len(tokens) > 1: strip = strip.bake(tokens[1:]) - libs_dir = join(self.dist_dir, '_python_bundle', + libs_dir = join(self.dist_dir, f'_python_bundle__{arch.arch}', '_python_bundle', 'modules') - if self.ctx.python_recipe.name == 'python2legacy': - libs_dir = join(self.dist_dir, 'private') filens = shprint(sh.find, libs_dir, join(self.dist_dir, 'libs'), '-iname', '*.so', _env=env).stdout.decode('utf-8') @@ -301,9 +401,31 @@ class Bootstrap(object): shprint(sh.rm, '-rf', d) -def expand_dependencies(recipes): +def expand_dependencies(recipes, ctx): + """ This function expands to lists of all different available + alternative recipe combinations, with the dependencies added in + ONLY for all the not-with-alternative recipes. + (So this is like the deps graph very simplified and incomplete, but + hopefully good enough for most basic bootstrap compatibility checks) + """ + + # Add in all the deps of recipes where there is no alternative: + recipes_with_deps = list(recipes) + for entry in recipes: + if not isinstance(entry, (tuple, list)) or len(entry) == 1: + if isinstance(entry, (tuple, list)): + entry = entry[0] + try: + recipe = Recipe.get_recipe(entry, ctx) + recipes_with_deps += recipe.depends + except ValueError: + # it's a pure python package without a recipe, so we + # don't know the dependencies...skipping for now + pass + + # Split up lists by available alternatives: recipe_lists = [[]] - for recipe in recipes: + for recipe in recipes_with_deps: if isinstance(recipe, (tuple, list)): new_recipe_lists = [] for alternative in recipe: @@ -313,6 +435,6 @@ def expand_dependencies(recipes): new_recipe_lists.append(new_list) recipe_lists = new_recipe_lists else: - for old_list in recipe_lists: - old_list.append(recipe) + for existing_list in recipe_lists: + existing_list.append(recipe) return recipe_lists diff --git a/p4a/pythonforandroid/bootstraps/common/build/build.py b/p4a/pythonforandroid/bootstraps/common/build/build.py index 342115e..036ad62 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/build.py +++ b/p4a/pythonforandroid/bootstraps/common/build/build.py @@ -1,13 +1,13 @@ -#!/usr/bin/env python2.7 - -from __future__ import print_function +#!/usr/bin/env python3 +from gzip import GzipFile +import hashlib import json from os.path import ( dirname, join, isfile, realpath, relpath, split, exists, basename ) -from os import listdir, makedirs, remove +from os import environ, listdir, makedirs, remove import os import shlex import shutil @@ -16,19 +16,20 @@ import sys import tarfile import tempfile import time -from zipfile import ZipFile from distutils.version import LooseVersion from fnmatch import fnmatch import jinja2 -def get_dist_info_for(key): +def get_dist_info_for(key, error_if_missing=True): try: with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh: info = json.load(fileh) - value = str(info[key]) + value = info[key] except (OSError, KeyError) as e: + if not error_if_missing: + return None print("BUILD FAILURE: Couldn't extract the key `" + key + "` " + "from dist_info.json: " + str(e)) sys.exit(1) @@ -39,10 +40,6 @@ def get_hostpython(): return get_dist_info_for('hostpython') -def get_python_version(): - return get_dist_info_for('python_version') - - def get_bootstrap_name(): return get_dist_info_for('bootstrap') @@ -57,7 +54,6 @@ else: curdir = dirname(__file__) PYTHON = get_hostpython() -PYTHON_VERSION = get_python_version() if PYTHON is not None and not exists(PYTHON): PYTHON = None @@ -72,29 +68,23 @@ BLACKLIST_PATTERNS = [ '~', '*.bak', '*.swp', + + # Android artifacts + '*.apk', + '*.aab', ] -# pyc/py -if PYTHON is not None: - BLACKLIST_PATTERNS.append('*.py') - if PYTHON_VERSION and int(PYTHON_VERSION[0]) == 2: - # we only blacklist `.pyc` for python2 because in python3 the compiled - # extension is `.pyc` (.pyo files not exists for python >= 3.6) - BLACKLIST_PATTERNS.append('*.pyc') WHITELIST_PATTERNS = [] if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'): WHITELIST_PATTERNS.append('pyconfig.h') -python_files = [] - environment = jinja2.Environment(loader=jinja2.FileSystemLoader( join(curdir, 'templates'))) -def try_unlink(fn): - if exists(fn): - os.unlink(fn) +DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity' +DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService' def ensure_dir(path): @@ -154,75 +144,33 @@ def listfiles(d): yield fn -def make_python_zip(): - ''' - Search for all the python related files, and construct the pythonXX.zip - According to - # http://randomsplat.com/id5-cross-compiling-python-for-embedded-linux.html - site-packages, config and lib-dynload will be not included. - ''' - - if not exists('private'): - print('No compiled python is present to zip, skipping.') - return - - global python_files - d = realpath(join('private', 'lib', 'python2.7')) - - def select(fn): - if is_blacklist(fn): - return False - fn = realpath(fn) - assert(fn.startswith(d)) - fn = fn[len(d):] - if (fn.startswith('/site-packages/') - or fn.startswith('/config/') - or fn.startswith('/lib-dynload/') - or fn.startswith('/libpymodules.so')): - return False - return fn - - # get a list of all python file - python_files = [x for x in listfiles(d) if select(x)] - - # create the final zipfile - zfn = join('private', 'lib', 'python27.zip') - zf = ZipFile(zfn, 'w') - - # put all the python files in it - for fn in python_files: - afn = fn[len(d):] - zf.write(fn, afn) - zf.close() - - -def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True): +def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True): ''' Make a zip file `fn` from the contents of source_dis. ''' - # selector function - def select(fn): - rfn = realpath(fn) - for p in ignore_path: - if p.endswith('/'): - p = p[:-1] - if rfn.startswith(p): - return False - if rfn in python_files: - return False - return not is_blacklist(fn) + def clean(tinfo): + """cleaning function (for reproducible builds)""" + tinfo.uid = tinfo.gid = 0 + tinfo.uname = tinfo.gname = '' + tinfo.mtime = 0 + return tinfo # get the files and relpath file of all the directory we asked for files = [] for sd in source_dirs: sd = realpath(sd) - compile_dir(sd, optimize_python=optimize_python) - files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd) - if select(x)] + for fn in listfiles(sd): + if is_blacklist(fn): + continue + if fn.endswith('.py') and byte_compile_python: + fn = compile_py_file(fn, optimize_python=optimize_python) + files.append((fn, relpath(realpath(fn), sd))) + files.sort() # deterministic # create tar.gz of thoses files - tf = tarfile.open(tfn, 'w:gz', format=tarfile.USTAR_FORMAT) + gf = GzipFile(tfn, 'wb', mtime=0) # deterministic + tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT) dirs = [] for fn, afn in files: dn = dirname(afn) @@ -238,25 +186,24 @@ def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True): dirs.append(d) tinfo = tarfile.TarInfo(d) tinfo.type = tarfile.DIRTYPE + clean(tinfo) tf.addfile(tinfo) # put the file - tf.add(fn, afn) + tf.add(fn, afn, filter=clean) tf.close() + gf.close() -def compile_dir(dfn, optimize_python=True): +def compile_py_file(python_file, optimize_python=True): ''' - Compile *.py in directory `dfn` to *.pyo + Compile python_file to *.pyc and return the filename of the *.pyc file. ''' if PYTHON is None: return - if int(PYTHON_VERSION[0]) >= 3: - args = [PYTHON, '-m', 'compileall', '-b', '-f', dfn] - else: - args = [PYTHON, '-m', 'compileall', '-f', dfn] + args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file] if optimize_python: # -OO = strip docstrings args.insert(1, '-OO') @@ -268,16 +215,18 @@ def compile_dir(dfn, optimize_python=True): 'error, see logs above') exit(1) + return ".".join([os.path.splitext(python_file)[0], "pyc"]) + def make_package(args): - # If no launcher is specified, require a main.py/main.pyo: + # If no launcher is specified, require a main.py/main.pyc: if (get_bootstrap_name() != "sdl" or args.launcher is None) and \ - get_bootstrap_name() != "webview": + get_bootstrap_name() not in ["webview", "service_library"]: # (webview doesn't need an entrypoint, apparently) if args.private is None or ( not exists(join(realpath(args.private), 'main.py')) and - not exists(join(realpath(args.private), 'main.pyo'))): - print('''BUILD FAILURE: No main.py(o) found in your app directory. This + not exists(join(realpath(args.private), 'main.pyc'))): + print('''BUILD FAILURE: No main.py(c) found in your app directory. This file must exist to act as the entry point for you app. If your app is started by a file with a different name, rename it to main.py or add a main.py that loads it.''') @@ -286,53 +235,168 @@ main.py that loads it.''') assets_dir = "src/main/assets" # Delete the old assets. - try_unlink(join(assets_dir, 'public.mp3')) - try_unlink(join(assets_dir, 'private.mp3')) + shutil.rmtree(assets_dir, ignore_errors=True) ensure_dir(assets_dir) - - # In order to speedup import and initial depack, - # construct a python27.zip - make_python_zip() - + # remove make_python_zip() # Add extra environment variable file into tar-able directory: env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-") with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f: - f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n") + if hasattr(args, "window"): + f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n") if hasattr(args, "orientation"): f.write("P4A_ORIENTATION=" + str(args.orientation) + "\n") f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n") f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n") # Package up the private data (public not supported). + use_setup_py = get_dist_info_for("use_setup_py", + error_if_missing=False) is True tar_dirs = [env_vars_tarpath] - if args.private: - tar_dirs.append(args.private) - for python_bundle_dir in ('private', 'crystax_python', '_python_bundle'): - if exists(python_bundle_dir): - tar_dirs.append(python_bundle_dir) - if get_bootstrap_name() == "webview": - tar_dirs.append('webview_includes') - if args.private or args.launcher: - make_tar( - join(assets_dir, 'private.mp3'), tar_dirs, args.ignore_path, - optimize_python=args.optimize_python) + _temp_dirs_to_clean = [] + try: + if args.private: + if not use_setup_py or ( + not exists(join(args.private, "setup.py")) and + not exists(join(args.private, "pyproject.toml")) + ): + print('No setup.py/pyproject.toml used, copying ' + 'full private data into .apk.') + tar_dirs.append(args.private) + else: + print("Copying main.py's ONLY, since other app data is " + "expected in site-packages.") + main_py_only_dir = tempfile.mkdtemp() + _temp_dirs_to_clean.append(main_py_only_dir) + + # skip this: + # if exists(join(args.private, "main.pyo")): + # shutil.copyfile(join(args.private, "main.pyo"), + # join(main_py_only_dir, "main.pyo")) + # elif exists(join(args.private, "main.py")): + # shutil.copyfile(join(args.private, "main.py"), + # join(main_py_only_dir, "main.py")) + # tar_dirs.append(main_py_only_dir) + + # Check all main.py files we need to copy: + copy_paths = ["main.py", join("service", "main.py")] + for copy_path in copy_paths: + variants = [ + copy_path, + copy_path.partition(".")[0] + ".pyc", + ] + # Check in all variants with all possible endings: + for variant in variants: + if exists(join(args.private, variant)): + # Make sure surrounding directly exists: + dir_path = os.path.dirname(variant) + if (len(dir_path) > 0 and + not exists( + join(main_py_only_dir, dir_path) + )): + os.mkdir(join(main_py_only_dir, dir_path)) + # Copy actual file: + shutil.copyfile( + join(args.private, variant), + join(main_py_only_dir, variant), + ) + + # Append directory with all main.py's to result apk paths: + tar_dirs.append(main_py_only_dir) + # if get_bootstrap_name() == "webview": + # for asset in listdir('webview_includes'): + # shutil.copy(join('webview_includes', asset), join(assets_dir, asset)) + + for asset in args.assets: + asset_src, asset_dest = asset.split(":") + if isfile(realpath(asset_src)): + ensure_dir(dirname(join(assets_dir, asset_dest))) + shutil.copy(realpath(asset_src), join(assets_dir, asset_dest)) + else: + shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest)) + + if args.private or args.launcher: + for arch in get_dist_info_for("archs"): + libs_dir = f"libs/{arch}" + make_tar( + join(libs_dir, "libpybundle.so"), + [f"_python_bundle__{arch}"], + byte_compile_python=args.byte_compile_python, + optimize_python=args.optimize_python, + ) + make_tar( + join(assets_dir, "private.tar"), + tar_dirs, + byte_compile_python=args.byte_compile_python, + optimize_python=args.optimize_python, + ) + finally: + for directory in _temp_dirs_to_clean: + shutil.rmtree(directory) # Remove extra env vars tar-able directory: shutil.rmtree(env_vars_tarpath) # Prepare some variables for templating process res_dir = "src/main/res" + res_dir_initial = "src/res_initial" + # make res_dir stateless + if exists(res_dir_initial): + shutil.rmtree(res_dir, ignore_errors=True) + shutil.copytree(res_dir_initial, res_dir) + else: + shutil.copytree(res_dir, res_dir_initial) + + # Add user resouces + for resource in args.resources: + resource_src, resource_dest = resource.split(":") + if isfile(realpath(resource_src)): + ensure_dir(dirname(join(res_dir, resource_dest))) + shutil.copy(realpath(resource_src), join(res_dir, resource_dest)) + else: + shutil.copytree(realpath(resource_src), + join(res_dir, resource_dest), dirs_exist_ok=True) + default_icon = 'templates/kivy-icon.png' default_presplash = 'templates/kivy-presplash.jpg' shutil.copy( args.icon or default_icon, - join(res_dir, 'drawable/icon.png') + join(res_dir, 'mipmap/icon.png') ) + if args.icon_fg and args.icon_bg: + shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png')) + shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png')) + with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd: + fd.write(""" + + + + +""") + elif args.icon_fg or args.icon_bg: + print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. " + "Ignoring.") + if get_bootstrap_name() != "service_only": - shutil.copy( - args.presplash or default_presplash, - join(res_dir, 'drawable/presplash.jpg') - ) + lottie_splashscreen = join(res_dir, 'raw/splashscreen.json') + if args.presplash_lottie: + shutil.copy( + 'templates/lottie.xml', + join(res_dir, 'layout/lottie.xml') + ) + ensure_dir(join(res_dir, 'raw')) + shutil.copy( + args.presplash_lottie, + join(res_dir, 'raw/splashscreen.json') + ) + else: + if exists(lottie_splashscreen): + remove(lottie_splashscreen) + remove(join(res_dir, 'layout/lottie.xml')) + + shutil.copy( + args.presplash or default_presplash, + join(res_dir, 'drawable/presplash.jpg') + ) # If extra Java jars were requested, copy them into the libs directory jars = [] @@ -360,17 +424,17 @@ main.py that loads it.''') version_code = 0 if not args.numeric_version: - # Set version code in format (arch-minsdk-app_version) - with open(join(dirname(__file__), 'dist_info.json'), 'r') as dist_info: - dist_data = json.load(dist_info) - arch = dist_data["archs"][0] - arch_dict = {"x86_64": "9", "arm64-v8a": "8", "armeabi-v7a": "7", "x86": "6"} - arch_code = arch_dict.get(arch, '1') + """ + Set version code in format (10 + minsdk + app_version) + Historically versioning was (arch + minsdk + app_version), + with arch expressed with a single digit from 6 to 9. + Since the multi-arch support, has been changed to 10. + """ min_sdk = args.min_sdk_version for i in args.version.split('.'): version_code *= 100 version_code += int(i) - args.numeric_version = "{}{}{}".format(arch_code, min_sdk, version_code) + args.numeric_version = "{}{}{}".format("10", min_sdk, version_code) if args.intent_filters: with open(args.intent_filters) as fd: @@ -387,6 +451,9 @@ main.py that loads it.''') for spec in args.extra_source_dirs: if ':' in spec: specdir, specincludes = spec.split(':') + print('WARNING: Currently gradle builds only support including source ' + 'directories, so when building using gradle all files in ' + '{} will be included.'.format(specdir)) else: specdir = spec specincludes = '**' @@ -402,6 +469,7 @@ main.py that loads it.''') service = True service_names = [] + base_service_class = args.service_class_name.split('.')[-1] for sid, spec in enumerate(args.services): spec = spec.split(':') name = spec[0] @@ -426,6 +494,7 @@ main.py that loads it.''') foreground=foreground, sticky=sticky, service_id=sid + 1, + base_service_class=base_service_class, ) # Find the SDK directory and target API @@ -447,19 +516,37 @@ main.py that loads it.''') # Try to build with the newest available build tools ignored = {".DS_Store", ".ds_store"} build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored] - build_tools_versions.sort(key=LooseVersion) + build_tools_versions = sorted(build_tools_versions, + key=LooseVersion) build_tools_version = build_tools_versions[-1] # Folder name for launcher (used by SDL2 bootstrap) url_scheme = 'kivy' + # Copy backup rules file if specified and update the argument + res_xml_dir = join(res_dir, 'xml') + if args.backup_rules: + ensure_dir(res_xml_dir) + shutil.copy(join(args.private, args.backup_rules), res_xml_dir) + args.backup_rules = split(args.backup_rules)[1][:-4] + + # Copy res_xml files to src/main/res/xml + if args.res_xmls: + ensure_dir(res_xml_dir) + for xmlpath in args.res_xmls: + if not os.path.exists(xmlpath): + xmlpath = join(args.private, xmlpath) + shutil.copy(xmlpath, res_xml_dir) + # Render out android manifest: manifest_path = "src/main/AndroidManifest.xml" render_args = { "args": args, "service": service, "service_names": service_names, - "android_api": android_api + "android_api": android_api, + "debug": "debug" in args.build_mode, + "native_services": args.native_services } if get_bootstrap_name() == "sdl2": render_args["url_scheme"] = url_scheme @@ -482,9 +569,17 @@ main.py that loads it.''') aars=aars, jars=jars, android_api=android_api, - build_tools_version=build_tools_version + build_tools_version=build_tools_version, + debug_build="debug" in args.build_mode, + is_library=(get_bootstrap_name() == 'service_library'), ) + # gradle properties + render( + 'gradle.tmpl.properties', + 'gradle.properties', + args=args) + # ant build templates render( 'build.tmpl.xml', @@ -493,9 +588,18 @@ main.py that loads it.''') versioned_name=versioned_name) # String resources: + timestamp = time.time() + if 'SOURCE_DATE_EPOCH' in environ: + # for reproducible builds + timestamp = int(environ['SOURCE_DATE_EPOCH']) + private_version = "{} {} {}".format( + args.version, + args.numeric_version, + timestamp + ) render_args = { "args": args, - "private_version": str(time.time()) + "private_version": hashlib.sha1(private_version.encode()).hexdigest() } if get_bootstrap_name() == "sdl2": render_args["url_scheme"] = url_scheme @@ -527,27 +631,31 @@ main.py that loads it.''') for patch_name in os.listdir(join('src', 'patches')): patch_path = join('src', 'patches', patch_name) print("Applying patch: " + str(patch_path)) + + # -N: insist this is FORWARD patch, don't reverse apply + # -p1: strip first path component + # -t: batch mode, don't ask questions + patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path] + try: - subprocess.check_output([ - # -N: insist this is FORWARd patch, don't reverse apply - # -p1: strip first path component - # -t: batch mode, don't ask questions - "patch", "-N", "-p1", "-t", "-i", patch_path - ]) + # Use a dry run to establish whether the patch is already applied. + # If we don't check this, the patch may be partially applied (which is bad!) + subprocess.check_output(patch_command + ["--dry-run"]) except subprocess.CalledProcessError as e: if e.returncode == 1: - # Return code 1 means it didn't apply, this will - # usually mean it is already applied. - print("Warning: failed to apply patch (" + - "exit code 1), " + - "assuming it is already applied: " + - str(patch_path) - ) + # Return code 1 means not all hunks could be applied, this usually + # means the patch is already applied. + print("Warning: failed to apply patch (exit code 1), " + "assuming it is already applied: ", + str(patch_path)) else: raise e + else: + # The dry run worked, so do the real thing + subprocess.check_output(patch_command) -def parse_args(args=None): +def parse_args_and_make_package(args=None): global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON # Get the default minsdk, equal to the NDK API that this dist is built against @@ -602,16 +710,36 @@ tools directory of the Android SDK. help='Custom key=value to add in application metadata') ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[], help='Used shared libraries included using tag in AndroidManifest.xml') + ap.add_argument('--asset', dest='assets', + action="append", default=[], + metavar="/path/to/source:dest", + help='Put this in the assets folder at assets/dest') + ap.add_argument('--resource', dest='resources', + action="append", default=[], + metavar="/path/to/source:kind/asset", + help='Put this in the res folder at res/kind') ap.add_argument('--icon', dest='icon', help=('A png file to use as the icon for ' 'the application.')) + ap.add_argument('--icon-fg', dest='icon_fg', + help=('A png file to use as the foreground of the adaptive icon ' + 'for the application.')) + ap.add_argument('--icon-bg', dest='icon_bg', + help=('A png file to use as the background of the adaptive icon ' + 'for the application.')) ap.add_argument('--service', dest='services', action='append', default=[], help='Declare a new service entrypoint: ' 'NAME:PATH_TO_PY[:foreground]') + ap.add_argument('--native-service', dest='native_services', action='append', default=[], + help='Declare a new native service: ' + 'package.name.service') if get_bootstrap_name() != "service_only": ap.add_argument('--presplash', dest='presplash', help=('A jpeg file to use as a screen while the ' 'application is loading.')) + ap.add_argument('--presplash-lottie', dest='presplash_lottie', + help=('A lottie (json) file to use as an animation while the ' + 'application is loading.')) ap.add_argument('--presplash-color', dest='presplash_color', default='#000000', @@ -636,6 +764,28 @@ tools directory of the Android SDK. 'https://developer.android.com/guide/' 'topics/manifest/' 'activity-element.html')) + + ap.add_argument('--enable-androidx', dest='enable_androidx', + action='store_true', + help=('Enable the AndroidX support library, ' + 'requires api = 28 or greater')) + ap.add_argument('--android-entrypoint', dest='android_entrypoint', + default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS, + help='Defines which java class will be used for startup, usually a subclass of PythonActivity') + ap.add_argument('--android-apptheme', dest='android_apptheme', + default='@android:style/Theme.NoTitleBar', + help='Defines which app theme should be selected for the main activity') + ap.add_argument('--add-compile-option', dest='compile_options', default=[], + action='append', help='add compile options to gradle.build') + ap.add_argument('--add-gradle-repository', dest='gradle_repositories', + default=[], + action='append', + help='Ddd a repository for gradle') + ap.add_argument('--add-packaging-option', dest='packaging_options', + default=[], + action='append', + help='Dndroid packaging options') + ap.add_argument('--wakelock', dest='wakelock', action='store_true', help=('Indicate if the application needs the device ' 'to stay on')) @@ -647,6 +797,13 @@ tools directory of the Android SDK. default=join(curdir, 'whitelist.txt'), help=('Use a whitelist file to prevent blacklisting of ' 'file in the final APK')) + ap.add_argument('--release', dest='build_mode', action='store_const', + const='release', default='debug', + help='Build your app as a non-debug release build. ' + '(Disables gdb debugging among other things)') + ap.add_argument('--with-debug-symbols', dest='with_debug_symbols', + action='store_const', const=True, default=False, + help='Will keep debug symbols from `.so` files.') ap.add_argument('--add-jar', dest='add_jar', action='append', help=('Add a Java .jar to the libs, so you can access its ' 'classes with pyjnius. You can specify this ' @@ -674,6 +831,8 @@ tools directory of the Android SDK. 'filename containing xml. The filename should be ' 'located relative to the python-for-android ' 'directory')) + ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[], + help='Add files to res/xml directory (for example device-filters)', nargs='+') ap.add_argument('--with-billing', dest='billing_pubkey', help='If set, the billing service will be added (not implemented)') ap.add_argument('--add-source', dest='extra_source_dirs', action='append', @@ -685,8 +844,6 @@ tools directory of the Android SDK. ap.add_argument('--try-system-python-compile', dest='try_system_python_compile', action='store_true', help='Use the system python during compileall if possible.') - ap.add_argument('--no-compile-pyo', dest='no_compile_pyo', action='store_true', - help='Do not optimise .py files to .pyo.') ap.add_argument('--sign', action='store_true', help=('Try to sign the APK with your credentials. You must set ' 'the appropriate environment variables.')) @@ -698,10 +855,33 @@ tools directory of the Android SDK. help='Set the launch mode of the main activity in the manifest.') ap.add_argument('--allow-backup', dest='allow_backup', default='true', help="if set to 'false', then android won't backup the application.") + ap.add_argument('--backup-rules', dest='backup_rules', default='', + help=('Backup rules for Android Auto Backup. Argument is a ' + 'filename containing xml. The filename should be ' + 'located relative to the private directory containing your source code ' + 'files (containing your main.py entrypoint). ' + 'See https://developer.android.com/guide/topics/data/' + 'autobackup#IncludingFiles for more information')) + ap.add_argument('--no-byte-compile-python', dest='byte_compile_python', + action='store_false', default=True, + help='Skip byte compile for .py files.') ap.add_argument('--no-optimize-python', dest='optimize_python', action='store_false', default=True, - help=('Whether to compile to optimised .pyo files, using -OO ' + help=('Whether to compile to optimised .pyc files, using -OO ' '(strips docstrings and asserts)')) + ap.add_argument('--extra-manifest-xml', default='', + help=('Extra xml to write directly inside the element of' + 'AndroidManifest.xml')) + ap.add_argument('--extra-manifest-application-arguments', default='', + help='Extra arguments to be added to the tag of' + 'AndroidManifest.xml') + ap.add_argument('--manifest-placeholders', dest='manifest_placeholders', + default='[:]', help=('Inject build variables into the manifest ' + 'via the manifestPlaceholders property')) + ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS, + help='Use that parameter if you need to implement your own PythonServive Java class') + ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS, + help='The full java class name of the main activity') # Put together arguments, and add those from .p4a config file: if args is None: @@ -721,7 +901,6 @@ tools directory of the Android SDK. _read_configuration() args = ap.parse_args(args) - args.ignore_path = [] if args.name and args.name[0] == '"' and args.name[-1] == '"': args.name = args.name[1:-1] @@ -751,21 +930,19 @@ tools directory of the Android SDK. if args.permissions and isinstance(args.permissions[0], list): args.permissions = [p for perm in args.permissions for p in perm] + if args.res_xmls and isinstance(args.res_xmls[0], list): + args.res_xmls = [x for res in args.res_xmls for x in res] + if args.try_system_python_compile: # Hardcoding python2.7 is okay for now, as python3 skips the # compilation anyway - if not exists('crystax_python'): - python_executable = 'python2.7' - try: - subprocess.call([python_executable, '--version']) - except (OSError, subprocess.CalledProcessError): - pass - else: - PYTHON = python_executable - - if args.no_compile_pyo: - PYTHON = None - BLACKLIST_PATTERNS.remove('*.py') + python_executable = 'python2.7' + try: + subprocess.call([python_executable, '--version']) + except (OSError, subprocess.CalledProcessError): + pass + else: + PYTHON = python_executable if args.blacklist: with open(args.blacklist) as fd: @@ -785,10 +962,11 @@ tools directory of the Android SDK. '--launcher (SDL2 bootstrap only)' + 'to have something to launch inside the .apk!') sys.exit(1) + print('ARGS ARGS ARGS', args) make_package(args) return args if __name__ == "__main__": - parse_args() + parse_args_and_make_package() diff --git a/p4a/pythonforandroid/bootstraps/common/build/gradle/wrapper/gradle-wrapper.properties b/p4a/pythonforandroid/bootstraps/common/build/gradle/wrapper/gradle-wrapper.properties index efc019a..dd012b8 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/gradle/wrapper/gradle-wrapper.properties +++ b/p4a/pythonforandroid/bootstraps/common/build/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip diff --git a/p4a/pythonforandroid/bootstraps/common/build/jni/application/src/Android.mk b/p4a/pythonforandroid/bootstraps/common/build/jni/application/src/Android.mk index 4a442ee..fb2b177 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/jni/application/src/Android.mk +++ b/p4a/pythonforandroid/bootstraps/common/build/jni/application/src/Android.mk @@ -21,7 +21,3 @@ LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog $(EXTRA_LDLIBS) LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) include $(BUILD_SHARED_LIBRARY) - -ifdef CRYSTAX_PYTHON_VERSION - $(call import-module,python/$(CRYSTAX_PYTHON_VERSION)) -endif diff --git a/p4a/pythonforandroid/bootstraps/common/build/jni/application/src/start.c b/p4a/pythonforandroid/bootstraps/common/build/jni/application/src/start.c index 3429118..bad5218 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/jni/application/src/start.c +++ b/p4a/pythonforandroid/bootstraps/common/build/jni/application/src/start.c @@ -15,15 +15,11 @@ #include #include "bootstrap_name.h" + #ifndef BOOTSTRAP_USES_NO_SDL_HEADERS #include "SDL.h" -#ifndef BOOTSTRAP_NAME_PYGAME #include "SDL_opengles2.h" #endif -#endif -#ifdef BOOTSTRAP_NAME_PYGAME -#include "jniwrapperstuff.h" -#endif #include "android/log.h" #define ENTRYPOINT_MAXLEN 128 @@ -169,26 +165,14 @@ int main(int argc, char *argv[]) { // Set up the python path char paths[256]; - char crystax_python_dir[256]; - snprintf(crystax_python_dir, 256, - "%s/crystax_python", getenv("ANDROID_UNPACK")); char python_bundle_dir[256]; snprintf(python_bundle_dir, 256, "%s/_python_bundle", getenv("ANDROID_UNPACK")); - if (dir_exists(crystax_python_dir) || dir_exists(python_bundle_dir)) { - if (dir_exists(crystax_python_dir)) { - LOGP("crystax_python exists"); - snprintf(paths, 256, - "%s/stdlib.zip:%s/modules", - crystax_python_dir, crystax_python_dir); - } - - if (dir_exists(python_bundle_dir)) { - LOGP("_python_bundle dir exists"); - snprintf(paths, 256, - "%s/stdlib.zip:%s/modules", - python_bundle_dir, python_bundle_dir); - } + if (dir_exists(python_bundle_dir)) { + LOGP("_python_bundle dir exists"); + snprintf(paths, 256, + "%s/stdlib.zip:%s/modules", + python_bundle_dir, python_bundle_dir); LOGP("calculated paths to be..."); LOGP(paths); @@ -200,24 +184,11 @@ int main(int argc, char *argv[]) { LOGP("set wchar paths..."); } else { - // We do not expect to see crystax_python any more, so no point - // reminding the user about it. If it does exist, we'll have - // logged it earlier. - LOGP("_python_bundle does not exist"); + LOGP("_python_bundle does not exist...this not looks good, all python" + " recipes should have this folder, should we expect a crash soon?"); } Py_Initialize(); - -#if PY_MAJOR_VERSION < 3 - // Can't Py_SetPath in python2 but we can set PySys_SetPath, which must - // be applied after Py_Initialize rather than before like Py_SetPath - #if PY_MICRO_VERSION >= 15 - // Only for python native-build - PySys_SetPath(paths); - #endif - PySys_SetArgv(argc, argv); -#endif - LOGP("Initialized python"); /* ensure threads will work. @@ -236,34 +207,8 @@ int main(int argc, char *argv[]) { * replace sys.path with our path */ PyRun_SimpleString("import sys, posix\n"); - if (dir_exists("lib")) { - /* If we built our own python, set up the paths correctly. - * This is only the case if we are using the python2legacy recipe - */ - LOGP("Setting up python from ANDROID_APP_PATH"); - PyRun_SimpleString("private = posix.environ['ANDROID_APP_PATH']\n" - "argument = posix.environ['ANDROID_ARGUMENT']\n" - "sys.path[:] = [ \n" - " private + '/lib/python27.zip', \n" - " private + '/lib/python2.7/', \n" - " private + '/lib/python2.7/lib-dynload/', \n" - " private + '/lib/python2.7/site-packages/', \n" - " argument ]\n"); - } char add_site_packages_dir[256]; - if (dir_exists(crystax_python_dir)) { - snprintf(add_site_packages_dir, 256, - "sys.path.append('%s/site-packages')", - crystax_python_dir); - - PyRun_SimpleString("import sys\n" - "sys.argv = ['notaninterpreterreally']\n" - "from os.path import realpath, join, dirname"); - PyRun_SimpleString(add_site_packages_dir); - /* "sys.path.append(join(dirname(realpath(__file__)), 'site-packages'))") */ - PyRun_SimpleString("sys.path = ['.'] + sys.path"); - } if (dir_exists(python_bundle_dir)) { snprintf(add_site_packages_dir, 256, @@ -281,13 +226,13 @@ int main(int argc, char *argv[]) { PyRun_SimpleString( "class LogFile(object):\n" " def __init__(self):\n" - " self.buffer = ''\n" + " self.__buffer = ''\n" " def write(self, s):\n" - " s = self.buffer + s\n" - " lines = s.split(\"\\n\")\n" + " s = self.__buffer + s\n" + " lines = s.split('\\n')\n" " for l in lines[:-1]:\n" - " androidembed.log(l)\n" - " self.buffer = lines[-1]\n" + " androidembed.log(l.replace('\\x00', ''))\n" + " self.__buffer = lines[-1]\n" " def flush(self):\n" " return\n" "sys.stdout = sys.stderr = LogFile()\n" @@ -306,14 +251,10 @@ int main(int argc, char *argv[]) { */ LOGP("Run user program, change dir and execute entrypoint"); - /* Get the entrypoint, search the .pyo then .py + /* Get the entrypoint, search the .pyc then .py */ char *dot = strrchr(env_entrypoint, '.'); -#if PY_MAJOR_VERSION > 2 char *ext = ".pyc"; -#else - char *ext = ".pyo"; -#endif if (dot <= 0) { LOGP("Invalid entrypoint, abort."); return -1; @@ -329,21 +270,17 @@ int main(int argc, char *argv[]) { entrypoint[strlen(env_entrypoint) - 1] = '\0'; LOGP(entrypoint); if (!file_exists(entrypoint)) { - LOGP("Entrypoint not found (.pyc/.pyo, fallback on .py), abort"); + LOGP("Entrypoint not found (.pyc, fallback on .py), abort"); return -1; } } else { strcpy(entrypoint, env_entrypoint); } } else if (!strcmp(dot, ".py")) { - /* if .py is passed, check the pyo version first */ + /* if .py is passed, check the pyc version first */ strcpy(entrypoint, env_entrypoint); entrypoint[strlen(env_entrypoint) + 1] = '\0'; -#if PY_MAJOR_VERSION > 2 entrypoint[strlen(env_entrypoint)] = 'c'; -#else - entrypoint[strlen(env_entrypoint)] = 'o'; -#endif if (!file_exists(entrypoint)) { /* fallback on pure python version */ if (!file_exists(env_entrypoint)) { @@ -353,7 +290,7 @@ int main(int argc, char *argv[]) { strcpy(entrypoint, env_entrypoint); } } else { - LOGP("Entrypoint have an invalid extension (must be .py or .pyc/.pyo), abort."); + LOGP("Entrypoint have an invalid extension (must be .py or .pyc), abort."); return -1; } // LOGP("Entrypoint is:"); @@ -374,8 +311,7 @@ int main(int argc, char *argv[]) { ret = 1; PyErr_Print(); /* This exits with the right code if SystemExit. */ PyObject *f = PySys_GetObject("stdout"); - if (PyFile_WriteString( - "\n", f)) /* python2 used Py_FlushLine, but this no longer exists */ + if (PyFile_WriteString("\n", f)) PyErr_Clear(); } diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/Octal.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/Octal.java new file mode 100755 index 0000000..dd10624 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/Octal.java @@ -0,0 +1,141 @@ +/** + * Copyright 2012 Kamran Zafar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.kamranzafar.jtar; + +/** + * @author Kamran Zafar + * + */ +public class Octal { + + /** + * Parse an octal string from a header buffer. This is used for the file + * permission mode value. + * + * @param header + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * + * @return The long value of the octal string. + */ + public static long parseOctal(byte[] header, int offset, int length) { + long result = 0; + boolean stillPadding = true; + + int end = offset + length; + for (int i = offset; i < end; ++i) { + if (header[i] == 0) + break; + + if (header[i] == (byte) ' ' || header[i] == '0') { + if (stillPadding) + continue; + + if (header[i] == (byte) ' ') + break; + } + + stillPadding = false; + + result = ( result << 3 ) + ( header[i] - '0' ); + } + + return result; + } + + /** + * Parse an octal integer from a header buffer. + * + * @param value + * @param buf + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * + * @return The integer value of the octal bytes. + */ + public static int getOctalBytes(long value, byte[] buf, int offset, int length) { + int idx = length - 1; + + buf[offset + idx] = 0; + --idx; + buf[offset + idx] = (byte) ' '; + --idx; + + if (value == 0) { + buf[offset + idx] = (byte) '0'; + --idx; + } else { + for (long val = value; idx >= 0 && val > 0; --idx) { + buf[offset + idx] = (byte) ( (byte) '0' + (byte) ( val & 7 ) ); + val = val >> 3; + } + } + + for (; idx >= 0; --idx) { + buf[offset + idx] = (byte) ' '; + } + + return offset + length; + } + + /** + * Parse the checksum octal integer from a header buffer. + * + * @param value + * @param buf + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * @return The integer value of the entry's checksum. + */ + public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) { + getOctalBytes( value, buf, offset, length ); + buf[offset + length - 1] = (byte) ' '; + buf[offset + length - 2] = 0; + return offset + length; + } + + /** + * Parse an octal long integer from a header buffer. + * + * @param value + * @param buf + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * + * @return The long value of the octal bytes. + */ + public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) { + byte[] temp = new byte[length + 1]; + getOctalBytes( value, temp, 0, length + 1 ); + System.arraycopy( temp, 0, buf, offset, length ); + return offset + length; + } + +} diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarConstants.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarConstants.java new file mode 100755 index 0000000..4611e20 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarConstants.java @@ -0,0 +1,28 @@ +/** + * Copyright 2012 Kamran Zafar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.kamranzafar.jtar; + +/** + * @author Kamran Zafar + * + */ +public class TarConstants { + public static final int EOF_BLOCK = 1024; + public static final int DATA_BLOCK = 512; + public static final int HEADER_BLOCK = 512; +} diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarEntry.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarEntry.java new file mode 100755 index 0000000..fe01db4 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarEntry.java @@ -0,0 +1,284 @@ +/** + * Copyright 2012 Kamran Zafar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.kamranzafar.jtar; + +import java.io.File; +import java.util.Date; + +/** + * @author Kamran Zafar + * + */ +public class TarEntry { + protected File file; + protected TarHeader header; + + private TarEntry() { + this.file = null; + header = new TarHeader(); + } + + public TarEntry(File file, String entryName) { + this(); + this.file = file; + this.extractTarHeader(entryName); + } + + public TarEntry(byte[] headerBuf) { + this(); + this.parseTarHeader(headerBuf); + } + + /** + * Constructor to create an entry from an existing TarHeader object. + * + * This method is useful to add new entries programmatically (e.g. for + * adding files or directories that do not exist in the file system). + * + * @param header + * + */ + public TarEntry(TarHeader header) { + this.file = null; + this.header = header; + } + + public boolean equals(TarEntry it) { + return header.name.toString().equals(it.header.name.toString()); + } + + public boolean isDescendent(TarEntry desc) { + return desc.header.name.toString().startsWith(header.name.toString()); + } + + public TarHeader getHeader() { + return header; + } + + public String getName() { + String name = header.name.toString(); + if (header.namePrefix != null && !header.namePrefix.toString().equals("")) { + name = header.namePrefix.toString() + "/" + name; + } + + return name; + } + + public void setName(String name) { + header.name = new StringBuffer(name); + } + + public int getUserId() { + return header.userId; + } + + public void setUserId(int userId) { + header.userId = userId; + } + + public int getGroupId() { + return header.groupId; + } + + public void setGroupId(int groupId) { + header.groupId = groupId; + } + + public String getUserName() { + return header.userName.toString(); + } + + public void setUserName(String userName) { + header.userName = new StringBuffer(userName); + } + + public String getGroupName() { + return header.groupName.toString(); + } + + public void setGroupName(String groupName) { + header.groupName = new StringBuffer(groupName); + } + + public void setIds(int userId, int groupId) { + this.setUserId(userId); + this.setGroupId(groupId); + } + + public void setModTime(long time) { + header.modTime = time / 1000; + } + + public void setModTime(Date time) { + header.modTime = time.getTime() / 1000; + } + + public Date getModTime() { + return new Date(header.modTime * 1000); + } + + public File getFile() { + return this.file; + } + + public long getSize() { + return header.size; + } + + public void setSize(long size) { + header.size = size; + } + + /** + * Checks if the org.kamrazafar.jtar entry is a directory + * + * @return + */ + public boolean isDirectory() { + if (this.file != null) + return this.file.isDirectory(); + + if (header != null) { + if (header.linkFlag == TarHeader.LF_DIR) + return true; + + if (header.name.toString().endsWith("/")) + return true; + } + + return false; + } + + /** + * Extract header from File + * + * @param entryName + */ + public void extractTarHeader(String entryName) { + header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory()); + } + + /** + * Calculate checksum + * + * @param buf + * @return + */ + public long computeCheckSum(byte[] buf) { + long sum = 0; + + for (int i = 0; i < buf.length; ++i) { + sum += 255 & buf[i]; + } + + return sum; + } + + /** + * Writes the header to the byte buffer + * + * @param outbuf + */ + public void writeEntryHeader(byte[] outbuf) { + int offset = 0; + + offset = TarHeader.getNameBytes(header.name, outbuf, offset, TarHeader.NAMELEN); + offset = Octal.getOctalBytes(header.mode, outbuf, offset, TarHeader.MODELEN); + offset = Octal.getOctalBytes(header.userId, outbuf, offset, TarHeader.UIDLEN); + offset = Octal.getOctalBytes(header.groupId, outbuf, offset, TarHeader.GIDLEN); + + long size = header.size; + + offset = Octal.getLongOctalBytes(size, outbuf, offset, TarHeader.SIZELEN); + offset = Octal.getLongOctalBytes(header.modTime, outbuf, offset, TarHeader.MODTIMELEN); + + int csOffset = offset; + for (int c = 0; c < TarHeader.CHKSUMLEN; ++c) + outbuf[offset++] = (byte) ' '; + + outbuf[offset++] = header.linkFlag; + + offset = TarHeader.getNameBytes(header.linkName, outbuf, offset, TarHeader.NAMELEN); + offset = TarHeader.getNameBytes(header.magic, outbuf, offset, TarHeader.USTAR_MAGICLEN); + offset = TarHeader.getNameBytes(header.userName, outbuf, offset, TarHeader.USTAR_USER_NAMELEN); + offset = TarHeader.getNameBytes(header.groupName, outbuf, offset, TarHeader.USTAR_GROUP_NAMELEN); + offset = Octal.getOctalBytes(header.devMajor, outbuf, offset, TarHeader.USTAR_DEVLEN); + offset = Octal.getOctalBytes(header.devMinor, outbuf, offset, TarHeader.USTAR_DEVLEN); + offset = TarHeader.getNameBytes(header.namePrefix, outbuf, offset, TarHeader.USTAR_FILENAME_PREFIX); + + for (; offset < outbuf.length;) + outbuf[offset++] = 0; + + long checkSum = this.computeCheckSum(outbuf); + + Octal.getCheckSumOctalBytes(checkSum, outbuf, csOffset, TarHeader.CHKSUMLEN); + } + + /** + * Parses the tar header to the byte buffer + * + * @param header + * @param bh + */ + public void parseTarHeader(byte[] bh) { + int offset = 0; + + header.name = TarHeader.parseName(bh, offset, TarHeader.NAMELEN); + offset += TarHeader.NAMELEN; + + header.mode = (int) Octal.parseOctal(bh, offset, TarHeader.MODELEN); + offset += TarHeader.MODELEN; + + header.userId = (int) Octal.parseOctal(bh, offset, TarHeader.UIDLEN); + offset += TarHeader.UIDLEN; + + header.groupId = (int) Octal.parseOctal(bh, offset, TarHeader.GIDLEN); + offset += TarHeader.GIDLEN; + + header.size = Octal.parseOctal(bh, offset, TarHeader.SIZELEN); + offset += TarHeader.SIZELEN; + + header.modTime = Octal.parseOctal(bh, offset, TarHeader.MODTIMELEN); + offset += TarHeader.MODTIMELEN; + + header.checkSum = (int) Octal.parseOctal(bh, offset, TarHeader.CHKSUMLEN); + offset += TarHeader.CHKSUMLEN; + + header.linkFlag = bh[offset++]; + + header.linkName = TarHeader.parseName(bh, offset, TarHeader.NAMELEN); + offset += TarHeader.NAMELEN; + + header.magic = TarHeader.parseName(bh, offset, TarHeader.USTAR_MAGICLEN); + offset += TarHeader.USTAR_MAGICLEN; + + header.userName = TarHeader.parseName(bh, offset, TarHeader.USTAR_USER_NAMELEN); + offset += TarHeader.USTAR_USER_NAMELEN; + + header.groupName = TarHeader.parseName(bh, offset, TarHeader.USTAR_GROUP_NAMELEN); + offset += TarHeader.USTAR_GROUP_NAMELEN; + + header.devMajor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN); + offset += TarHeader.USTAR_DEVLEN; + + header.devMinor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN); + offset += TarHeader.USTAR_DEVLEN; + + header.namePrefix = TarHeader.parseName(bh, offset, TarHeader.USTAR_FILENAME_PREFIX); + } +} \ No newline at end of file diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarHeader.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarHeader.java new file mode 100755 index 0000000..b9d3a86 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarHeader.java @@ -0,0 +1,243 @@ +/** + * Copyright 2012 Kamran Zafar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.kamranzafar.jtar; + +import java.io.File; + +/** + * Header + * + *
+ * Offset  Size     Field
+ * 0       100      File name
+ * 100     8        File mode
+ * 108     8        Owner's numeric user ID
+ * 116     8        Group's numeric user ID
+ * 124     12       File size in bytes
+ * 136     12       Last modification time in numeric Unix time format
+ * 148     8        Checksum for header block
+ * 156     1        Link indicator (file type)
+ * 157     100      Name of linked file
+ * 
+ * + * + * File Types + * + *
+ * Value        Meaning
+ * '0'          Normal file
+ * (ASCII NUL)  Normal file (now obsolete)
+ * '1'          Hard link
+ * '2'          Symbolic link
+ * '3'          Character special
+ * '4'          Block special
+ * '5'          Directory
+ * '6'          FIFO
+ * '7'          Contigous
+ * 
+ * + * + * + * Ustar header + * + *
+ * Offset  Size    Field
+ * 257     6       UStar indicator "ustar"
+ * 263     2       UStar version "00"
+ * 265     32      Owner user name
+ * 297     32      Owner group name
+ * 329     8       Device major number
+ * 337     8       Device minor number
+ * 345     155     Filename prefix
+ * 
+ */ + +public class TarHeader { + + /* + * Header + */ + public static final int NAMELEN = 100; + public static final int MODELEN = 8; + public static final int UIDLEN = 8; + public static final int GIDLEN = 8; + public static final int SIZELEN = 12; + public static final int MODTIMELEN = 12; + public static final int CHKSUMLEN = 8; + public static final byte LF_OLDNORM = 0; + + /* + * File Types + */ + public static final byte LF_NORMAL = (byte) '0'; + public static final byte LF_LINK = (byte) '1'; + public static final byte LF_SYMLINK = (byte) '2'; + public static final byte LF_CHR = (byte) '3'; + public static final byte LF_BLK = (byte) '4'; + public static final byte LF_DIR = (byte) '5'; + public static final byte LF_FIFO = (byte) '6'; + public static final byte LF_CONTIG = (byte) '7'; + + /* + * Ustar header + */ + + public static final String USTAR_MAGIC = "ustar"; // POSIX + + public static final int USTAR_MAGICLEN = 8; + public static final int USTAR_USER_NAMELEN = 32; + public static final int USTAR_GROUP_NAMELEN = 32; + public static final int USTAR_DEVLEN = 8; + public static final int USTAR_FILENAME_PREFIX = 155; + + // Header values + public StringBuffer name; + public int mode; + public int userId; + public int groupId; + public long size; + public long modTime; + public int checkSum; + public byte linkFlag; + public StringBuffer linkName; + public StringBuffer magic; // ustar indicator and version + public StringBuffer userName; + public StringBuffer groupName; + public int devMajor; + public int devMinor; + public StringBuffer namePrefix; + + public TarHeader() { + this.magic = new StringBuffer(TarHeader.USTAR_MAGIC); + + this.name = new StringBuffer(); + this.linkName = new StringBuffer(); + + String user = System.getProperty("user.name", ""); + + if (user.length() > 31) + user = user.substring(0, 31); + + this.userId = 0; + this.groupId = 0; + this.userName = new StringBuffer(user); + this.groupName = new StringBuffer(""); + this.namePrefix = new StringBuffer(); + } + + /** + * Parse an entry name from a header buffer. + * + * @param name + * @param header + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * @return The header's entry name. + */ + public static StringBuffer parseName(byte[] header, int offset, int length) { + StringBuffer result = new StringBuffer(length); + + int end = offset + length; + for (int i = offset; i < end; ++i) { + if (header[i] == 0) + break; + result.append((char) header[i]); + } + + return result; + } + + /** + * Determine the number of bytes in an entry name. + * + * @param name + * @param header + * The header buffer from which to parse. + * @param offset + * The offset into the buffer from which to parse. + * @param length + * The number of header bytes to parse. + * @return The number of bytes in a header's entry name. + */ + public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) { + int i; + + for (i = 0; i < length && i < name.length(); ++i) { + buf[offset + i] = (byte) name.charAt(i); + } + + for (; i < length; ++i) { + buf[offset + i] = 0; + } + + return offset + length; + } + + /** + * Creates a new header for a file/directory entry. + * + * + * @param name + * File name + * @param size + * File size in bytes + * @param modTime + * Last modification time in numeric Unix time format + * @param dir + * Is directory + * + * @return + */ + public static TarHeader createHeader(String entryName, long size, long modTime, boolean dir) { + String name = entryName; + name = TarUtils.trim(name.replace(File.separatorChar, '/'), '/'); + + TarHeader header = new TarHeader(); + header.linkName = new StringBuffer(""); + + if (name.length() > 100) { + header.namePrefix = new StringBuffer(name.substring(0, name.lastIndexOf('/'))); + header.name = new StringBuffer(name.substring(name.lastIndexOf('/') + 1)); + } else { + header.name = new StringBuffer(name); + } + + if (dir) { + header.mode = 040755; + header.linkFlag = TarHeader.LF_DIR; + if (header.name.charAt(header.name.length() - 1) != '/') { + header.name.append("/"); + } + header.size = 0; + } else { + header.mode = 0100644; + header.linkFlag = TarHeader.LF_NORMAL; + header.size = size; + } + + header.modTime = modTime; + header.checkSum = 0; + header.devMajor = 0; + header.devMinor = 0; + + return header; + } +} \ No newline at end of file diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarInputStream.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarInputStream.java new file mode 100755 index 0000000..ec50a1b --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarInputStream.java @@ -0,0 +1,249 @@ +/** + * Copyright 2012 Kamran Zafar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.kamranzafar.jtar; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Kamran Zafar + * + */ +public class TarInputStream extends FilterInputStream { + + private static final int SKIP_BUFFER_SIZE = 2048; + private TarEntry currentEntry; + private long currentFileSize; + private long bytesRead; + private boolean defaultSkip = false; + + public TarInputStream(InputStream in) { + super(in); + currentFileSize = 0; + bytesRead = 0; + } + + @Override + public boolean markSupported() { + return false; + } + + /** + * Not supported + * + */ + @Override + public synchronized void mark(int readlimit) { + } + + /** + * Not supported + * + */ + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + /** + * Read a byte + * + * @see java.io.FilterInputStream#read() + */ + @Override + public int read() throws IOException { + byte[] buf = new byte[1]; + + int res = this.read(buf, 0, 1); + + if (res != -1) { + return 0xFF & buf[0]; + } + + return res; + } + + /** + * Checks if the bytes being read exceed the entry size and adjusts the byte + * array length. Updates the byte counters + * + * + * @see java.io.FilterInputStream#read(byte[], int, int) + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (currentEntry != null) { + if (currentFileSize == currentEntry.getSize()) { + return -1; + } else if ((currentEntry.getSize() - currentFileSize) < len) { + len = (int) (currentEntry.getSize() - currentFileSize); + } + } + + int br = super.read(b, off, len); + + if (br != -1) { + if (currentEntry != null) { + currentFileSize += br; + } + + bytesRead += br; + } + + return br; + } + + /** + * Returns the next entry in the tar file + * + * @return TarEntry + * @throws IOException + */ + public TarEntry getNextEntry() throws IOException { + closeCurrentEntry(); + + byte[] header = new byte[TarConstants.HEADER_BLOCK]; + byte[] theader = new byte[TarConstants.HEADER_BLOCK]; + int tr = 0; + + // Read full header + while (tr < TarConstants.HEADER_BLOCK) { + int res = read(theader, 0, TarConstants.HEADER_BLOCK - tr); + + if (res < 0) { + break; + } + + System.arraycopy(theader, 0, header, tr, res); + tr += res; + } + + // Check if record is null + boolean eof = true; + for (byte b : header) { + if (b != 0) { + eof = false; + break; + } + } + + if (!eof) { + currentEntry = new TarEntry(header); + } + + return currentEntry; + } + + /** + * Returns the current offset (in bytes) from the beginning of the stream. + * This can be used to find out at which point in a tar file an entry's content begins, for instance. + */ + public long getCurrentOffset() { + return bytesRead; + } + + /** + * Closes the current tar entry + * + * @throws IOException + */ + protected void closeCurrentEntry() throws IOException { + if (currentEntry != null) { + if (currentEntry.getSize() > currentFileSize) { + // Not fully read, skip rest of the bytes + long bs = 0; + while (bs < currentEntry.getSize() - currentFileSize) { + long res = skip(currentEntry.getSize() - currentFileSize - bs); + + if (res == 0 && currentEntry.getSize() - currentFileSize > 0) { + // I suspect file corruption + throw new IOException("Possible tar file corruption"); + } + + bs += res; + } + } + + currentEntry = null; + currentFileSize = 0L; + skipPad(); + } + } + + /** + * Skips the pad at the end of each tar entry file content + * + * @throws IOException + */ + protected void skipPad() throws IOException { + if (bytesRead > 0) { + int extra = (int) (bytesRead % TarConstants.DATA_BLOCK); + + if (extra > 0) { + long bs = 0; + while (bs < TarConstants.DATA_BLOCK - extra) { + long res = skip(TarConstants.DATA_BLOCK - extra - bs); + bs += res; + } + } + } + } + + /** + * Skips 'n' bytes on the InputStream
+ * Overrides default implementation of skip + * + */ + @Override + public long skip(long n) throws IOException { + if (defaultSkip) { + // use skip method of parent stream + // may not work if skip not implemented by parent + long bs = super.skip(n); + bytesRead += bs; + + return bs; + } + + if (n <= 0) { + return 0; + } + + long left = n; + byte[] sBuff = new byte[SKIP_BUFFER_SIZE]; + + while (left > 0) { + int res = read(sBuff, 0, (int) (left < SKIP_BUFFER_SIZE ? left : SKIP_BUFFER_SIZE)); + if (res < 0) { + break; + } + left -= res; + } + + return n - left; + } + + public boolean isDefaultSkip() { + return defaultSkip; + } + + public void setDefaultSkip(boolean defaultSkip) { + this.defaultSkip = defaultSkip; + } +} diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarOutputStream.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarOutputStream.java new file mode 100755 index 0000000..ffdfe87 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarOutputStream.java @@ -0,0 +1,163 @@ +/** + * Copyright 2012 Kamran Zafar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.kamranzafar.jtar; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * @author Kamran Zafar + * + */ +public class TarOutputStream extends OutputStream { + private final OutputStream out; + private long bytesWritten; + private long currentFileSize; + private TarEntry currentEntry; + + public TarOutputStream(OutputStream out) { + this.out = out; + bytesWritten = 0; + currentFileSize = 0; + } + + public TarOutputStream(final File fout) throws FileNotFoundException { + this.out = new BufferedOutputStream(new FileOutputStream(fout)); + bytesWritten = 0; + currentFileSize = 0; + } + + /** + * Opens a file for writing. + */ + public TarOutputStream(final File fout, final boolean append) throws IOException { + @SuppressWarnings("resource") + RandomAccessFile raf = new RandomAccessFile(fout, "rw"); + final long fileSize = fout.length(); + if (append && fileSize > TarConstants.EOF_BLOCK) { + raf.seek(fileSize - TarConstants.EOF_BLOCK); + } + out = new BufferedOutputStream(new FileOutputStream(raf.getFD())); + } + + /** + * Appends the EOF record and closes the stream + * + * @see java.io.FilterOutputStream#close() + */ + @Override + public void close() throws IOException { + closeCurrentEntry(); + write( new byte[TarConstants.EOF_BLOCK] ); + out.close(); + } + /** + * Writes a byte to the stream and updates byte counters + * + * @see java.io.FilterOutputStream#write(int) + */ + @Override + public void write(int b) throws IOException { + out.write( b ); + bytesWritten += 1; + + if (currentEntry != null) { + currentFileSize += 1; + } + } + + /** + * Checks if the bytes being written exceed the current entry size. + * + * @see java.io.FilterOutputStream#write(byte[], int, int) + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (currentEntry != null && !currentEntry.isDirectory()) { + if (currentEntry.getSize() < currentFileSize + len) { + throw new IOException( "The current entry[" + currentEntry.getName() + "] size[" + + currentEntry.getSize() + "] is smaller than the bytes[" + ( currentFileSize + len ) + + "] being written." ); + } + } + + out.write( b, off, len ); + + bytesWritten += len; + + if (currentEntry != null) { + currentFileSize += len; + } + } + + /** + * Writes the next tar entry header on the stream + * + * @param entry + * @throws IOException + */ + public void putNextEntry(TarEntry entry) throws IOException { + closeCurrentEntry(); + + byte[] header = new byte[TarConstants.HEADER_BLOCK]; + entry.writeEntryHeader( header ); + + write( header ); + + currentEntry = entry; + } + + /** + * Closes the current tar entry + * + * @throws IOException + */ + protected void closeCurrentEntry() throws IOException { + if (currentEntry != null) { + if (currentEntry.getSize() > currentFileSize) { + throw new IOException( "The current entry[" + currentEntry.getName() + "] of size[" + + currentEntry.getSize() + "] has not been fully written." ); + } + + currentEntry = null; + currentFileSize = 0; + + pad(); + } + } + + /** + * Pads the last content block + * + * @throws IOException + */ + protected void pad() throws IOException { + if (bytesWritten > 0) { + int extra = (int) ( bytesWritten % TarConstants.DATA_BLOCK ); + + if (extra > 0) { + write( new byte[TarConstants.DATA_BLOCK - extra] ); + } + } + } +} diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarUtils.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarUtils.java new file mode 100755 index 0000000..5016576 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarUtils.java @@ -0,0 +1,96 @@ +/** + * Copyright 2012 Kamran Zafar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.kamranzafar.jtar; + +import java.io.File; + +/** + * @author Kamran + * + */ +public class TarUtils { + /** + * Determines the tar file size of the given folder/file path + * + * @param path + * @return + */ + public static long calculateTarSize(File path) { + return tarSize(path) + TarConstants.EOF_BLOCK; + } + + private static long tarSize(File dir) { + long size = 0; + + if (dir.isFile()) { + return entrySize(dir.length()); + } else { + File[] subFiles = dir.listFiles(); + + if (subFiles != null && subFiles.length > 0) { + for (File file : subFiles) { + if (file.isFile()) { + size += entrySize(file.length()); + } else { + size += tarSize(file); + } + } + } else { + // Empty folder header + return TarConstants.HEADER_BLOCK; + } + } + + return size; + } + + private static long entrySize(long fileSize) { + long size = 0; + size += TarConstants.HEADER_BLOCK; // Header + size += fileSize; // File size + + long extra = size % TarConstants.DATA_BLOCK; + + if (extra > 0) { + size += (TarConstants.DATA_BLOCK - extra); // pad + } + + return size; + } + + public static String trim(String s, char c) { + StringBuffer tmp = new StringBuffer(s); + for (int i = 0; i < tmp.length(); i++) { + if (tmp.charAt(i) != c) { + break; + } else { + tmp.deleteCharAt(i); + } + } + + for (int i = tmp.length() - 1; i >= 0; i--) { + if (tmp.charAt(i) != c) { + break; + } else { + tmp.deleteCharAt(i); + } + } + + return tmp.toString(); + } +} diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonService.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonService.java index 4f20fb7..dd6f307 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonService.java +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonService.java @@ -14,10 +14,10 @@ import android.app.PendingIntent; import android.os.Process; import java.io.File; -import org.kivy.android.PythonUtil; - -import org.renpy.android.Hardware; - +//imports for channel definition +import android.app.NotificationManager; +import android.app.NotificationChannel; +import android.graphics.Color; public class PythonService extends Service implements Runnable { @@ -33,6 +33,8 @@ public class PythonService extends Service implements Runnable { private String serviceEntrypoint; // Argument to pass to Python code, private String pythonServiceArgument; + + public static PythonService mService = null; private Intent startIntent = null; @@ -42,10 +44,6 @@ public class PythonService extends Service implements Runnable { autoRestartService = restart; } - public boolean canDisplayNotification() { - return true; - } - public int startType() { return START_NOT_STICKY; } @@ -64,10 +62,15 @@ public class PythonService extends Service implements Runnable { public int onStartCommand(Intent intent, int flags, int startId) { if (pythonThread != null) { Log.v("python service", "service exists, do not start again"); - return START_NOT_STICKY; + return startType(); + } + //intent is null if OS restarts a STICKY service + if (intent == null) { + Context context = getApplicationContext(); + intent = getThisDefaultIntent(context, ""); } - startIntent = intent; + startIntent = intent; Bundle extras = intent.getExtras(); androidPrivate = extras.getString("androidPrivate"); androidArgument = extras.getString("androidArgument"); @@ -75,28 +78,38 @@ public class PythonService extends Service implements Runnable { pythonName = extras.getString("pythonName"); pythonHome = extras.getString("pythonHome"); pythonPath = extras.getString("pythonPath"); + boolean serviceStartAsForeground = ( + extras.getString("serviceStartAsForeground").equals("true") + ); pythonServiceArgument = extras.getString("pythonServiceArgument"); - pythonThread = new Thread(this); pythonThread.start(); - if (canDisplayNotification()) { + if (serviceStartAsForeground) { doStartForeground(extras); } return startType(); } + protected int getServiceId() { + return 1; + } + + protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) { + return null; + } + protected void doStartForeground(Bundle extras) { String serviceTitle = extras.getString("serviceTitle"); String serviceDescription = extras.getString("serviceDescription"); - Notification notification; Context context = getApplicationContext(); Intent contextIntent = new Intent(context, PythonActivity.class); PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { notification = new Notification( context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis()); try { @@ -109,14 +122,26 @@ public class PythonService extends Service implements Runnable { IllegalArgumentException | InvocationTargetException e) { } } else { - Notification.Builder builder = new Notification.Builder(context); + // for android 8+ we need to create our own channel + // https://stackoverflow.com/questions/47531742/startforeground-fail-after-upgrade-to-android-8-1 + String NOTIFICATION_CHANNEL_ID = "org.kivy.p4a"; //TODO: make this configurable + String channelName = "Background Service"; //TODO: make this configurable + NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, + NotificationManager.IMPORTANCE_NONE); + + chan.setLightColor(Color.BLUE); + chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + manager.createNotificationChannel(chan); + + Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); builder.setContentTitle(serviceTitle); builder.setContentText(serviceDescription); builder.setContentIntent(pIntent); builder.setSmallIcon(context.getApplicationInfo().icon); notification = builder.build(); } - startForeground(1, notification); + startForeground(getServiceId(), notification); } @Override @@ -137,7 +162,10 @@ public class PythonService extends Service implements Runnable { @Override public void onTaskRemoved(Intent rootIntent) { super.onTaskRemoved(rootIntent); - stopSelf(); + //sticky servcie runtime/restart is managed by the OS. leave it running when app is closed + if (startType() != START_STICKY) { + stopSelf(); + } } @Override diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java index 1f26738..2d6ca9f 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java @@ -1,12 +1,20 @@ package org.kivy.android; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.File; +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; import android.util.Log; +import android.widget.Toast; + import java.util.ArrayList; -import java.io.FilenameFilter; import java.util.regex.Pattern; +import org.renpy.android.AssetExtract; public class PythonUtil { private static final String TAG = "pythonutil"; @@ -32,21 +40,25 @@ public class PythonUtil { protected static ArrayList getLibraries(File libsDir) { ArrayList libsList = new ArrayList(); - addLibraryIfExists(libsList, "crystax", libsDir); addLibraryIfExists(libsList, "sqlite3", libsDir); addLibraryIfExists(libsList, "ffi", libsDir); + addLibraryIfExists(libsList, "png16", libsDir); addLibraryIfExists(libsList, "ssl.*", libsDir); addLibraryIfExists(libsList, "crypto.*", libsDir); - libsList.add("python2.7"); + addLibraryIfExists(libsList, "SDL2", libsDir); + addLibraryIfExists(libsList, "SDL2_image", libsDir); + addLibraryIfExists(libsList, "SDL2_mixer", libsDir); + addLibraryIfExists(libsList, "SDL2_ttf", libsDir); libsList.add("python3.5m"); libsList.add("python3.6m"); libsList.add("python3.7m"); + libsList.add("python3.8"); + libsList.add("python3.9"); libsList.add("main"); return libsList; } public static void loadLibraries(File filesDir, File libsDir) { - String filesDirPath = filesDir.getAbsolutePath(); boolean foundPython = false; for (String lib : getLibraries(libsDir)) { @@ -61,8 +73,8 @@ public class PythonUtil { // load, and it has failed, give a more // general error Log.v(TAG, "Library loading error: " + e.getMessage()); - if (lib.startsWith("python3.7") && !foundPython) { - throw new java.lang.RuntimeException("Could not load any libpythonXXX.so"); + if (lib.startsWith("python3.9") && !foundPython) { + throw new RuntimeException("Could not load any libpythonXXX.so"); } else if (lib.startsWith("python")) { continue; } else { @@ -73,5 +85,174 @@ public class PythonUtil { } Log.v(TAG, "Loaded everything!"); - } + } + + public static String getAppRoot(Context ctx) { + String appRoot = ctx.getFilesDir().getAbsolutePath() + "/app"; + return appRoot; + } + + public static String getResourceString(Context ctx, String name) { + // Taken from org.renpy.android.ResourceManager + Resources res = ctx.getResources(); + int id = res.getIdentifier(name, "string", ctx.getPackageName()); + return res.getString(id); + } + + /** + * Show an error using a toast. (Only makes sense from non-UI threads.) + */ + protected static void toastError(final Activity activity, final String msg) { + activity.runOnUiThread(new Runnable () { + public void run() { + Toast.makeText(activity, msg, Toast.LENGTH_LONG).show(); + } + }); + + // Wait to show the error. + synchronized (activity) { + try { + activity.wait(1000); + } catch (InterruptedException e) { + } + } + } + + protected static void recursiveDelete(File f) { + if (f.isDirectory()) { + for (File r : f.listFiles()) { + recursiveDelete(r); + } + } + f.delete(); + } + + public static void unpackAsset( + Context ctx, + final String resource, + File target, + boolean cleanup_on_version_update) { + + Log.v(TAG, "Unpacking " + resource + " " + target.getName()); + + // The version of data in memory and on disk. + String dataVersion = getResourceString(ctx, resource + "_version"); + String diskVersion = null; + + Log.v(TAG, "Data version is " + dataVersion); + + // If no version, no unpacking is necessary. + if (dataVersion == null) { + return; + } + + // Check the current disk version, if any. + String filesDir = target.getAbsolutePath(); + String diskVersionFn = filesDir + "/" + resource + ".version"; + + try { + byte buf[] = new byte[64]; + InputStream is = new FileInputStream(diskVersionFn); + int len = is.read(buf); + diskVersion = new String(buf, 0, len); + is.close(); + } catch (Exception e) { + diskVersion = ""; + } + + // If the disk data is out of date, extract it and write the version file. + if (! dataVersion.equals(diskVersion)) { + Log.v(TAG, "Extracting " + resource + " assets."); + + if (cleanup_on_version_update) { + recursiveDelete(target); + } + target.mkdirs(); + + AssetExtract ae = new AssetExtract(ctx); + if (!ae.extractTar(resource + ".tar", target.getAbsolutePath(), "private")) { + String msg = "Could not extract " + resource + " data."; + if (ctx instanceof Activity) { + toastError((Activity)ctx, msg); + } else { + Log.v(TAG, msg); + } + } + + try { + // Write .nomedia. + new File(target, ".nomedia").createNewFile(); + + // Write version file. + FileOutputStream os = new FileOutputStream(diskVersionFn); + os.write(dataVersion.getBytes()); + os.close(); + } catch (Exception e) { + Log.w(TAG, e); + } + } + } + + public static void unpackPyBundle( + Context ctx, + final String resource, + File target, + boolean cleanup_on_version_update) { + + Log.v(TAG, "Unpacking " + resource + " " + target.getName()); + + // The version of data in memory and on disk. + String dataVersion = getResourceString(ctx, "private_version"); + String diskVersion = null; + + Log.v(TAG, "Data version is " + dataVersion); + + // If no version, no unpacking is necessary. + if (dataVersion == null) { + return; + } + + // Check the current disk version, if any. + String filesDir = target.getAbsolutePath(); + String diskVersionFn = filesDir + "/" + "libpybundle" + ".version"; + + try { + byte buf[] = new byte[64]; + InputStream is = new FileInputStream(diskVersionFn); + int len = is.read(buf); + diskVersion = new String(buf, 0, len); + is.close(); + } catch (Exception e) { + diskVersion = ""; + } + + if (! dataVersion.equals(diskVersion)) { + // If the disk data is out of date, extract it and write the version file. + Log.v(TAG, "Extracting " + resource + " assets."); + + if (cleanup_on_version_update) { + recursiveDelete(target); + } + target.mkdirs(); + + AssetExtract ae = new AssetExtract(ctx); + if (!ae.extractTar(resource + ".so", target.getAbsolutePath(), "pybundle")) { + String msg = "Could not extract " + resource + " data."; + if (ctx instanceof Activity) { + toastError((Activity)ctx, msg); + } else { + Log.v(TAG, msg); + } + } + + try { + // Write version file. + FileOutputStream os = new FileOutputStream(diskVersionFn); + os.write(dataVersion.getBytes()); + os.close(); + } catch (Exception e) { + Log.w(TAG, e); + } + } + } } diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/AssetExtract.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/AssetExtract.java index 52d6424..0a5dda6 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/AssetExtract.java +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/AssetExtract.java @@ -2,36 +2,34 @@ // spaces amount package org.renpy.android; -import java.io.*; - -import android.app.Activity; +import android.content.Context; import android.util.Log; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.FileInputStream; +import java.io.OutputStream; import java.io.FileOutputStream; +import java.io.FileNotFoundException; import java.io.File; +import java.io.FileInputStream; import java.util.zip.GZIPInputStream; import android.content.res.AssetManager; - -import org.kamranzafar.jtar.*; +import org.kamranzafar.jtar.TarEntry; +import org.kamranzafar.jtar.TarInputStream; public class AssetExtract { private AssetManager mAssetManager = null; - private Activity mActivity = null; - public AssetExtract(Activity act) { - mActivity = act; - mAssetManager = act.getAssets(); + public AssetExtract(Context context) { + mAssetManager = context.getAssets(); } - public boolean extractTar(String asset, String target) { + public boolean extractTar(String asset, String target, String method) { byte buf[] = new byte[1024 * 1024]; @@ -39,7 +37,12 @@ public class AssetExtract { TarInputStream tis = null; try { - assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING); + if(method == "private"){ + assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING); + } else if (method == "pybundle") { + assetStream = new FileInputStream(asset); + } + tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192)); } catch (IOException e) { Log.e("python", "opening up extract tar", e); @@ -51,7 +54,7 @@ public class AssetExtract { try { entry = tis.getNextEntry(); - } catch ( java.io.IOException e ) { + } catch ( IOException e ) { Log.e("python", "extracting tar", e); return false; } @@ -76,8 +79,7 @@ public class AssetExtract { try { out = new BufferedOutputStream(new FileOutputStream(path), 8192); - } catch ( FileNotFoundException e ) { - } catch ( SecurityException e ) { }; + } catch ( FileNotFoundException | SecurityException e ) {} if ( out == null ) { Log.e("python", "could not open " + path); @@ -97,7 +99,7 @@ public class AssetExtract { out.flush(); out.close(); - } catch ( java.io.IOException e ) { + } catch ( IOException e ) { Log.e("python", "extracting zip", e); return false; } diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/Hardware.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/Hardware.java new file mode 100644 index 0000000..8475762 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/Hardware.java @@ -0,0 +1,279 @@ +package org.renpy.android; + +import android.content.Context; +import android.os.Vibrator; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.DisplayMetrics; +import android.view.inputmethod.InputMethodManager; +import android.view.View; + +import java.util.List; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import org.kivy.android.PythonActivity; + +/** + * Methods that are expected to be called via JNI, to access the + * device's non-screen hardware. (For example, the vibration and + * accelerometer.) + */ +public class Hardware { + + // The context. + static Context context; + static View view; + public static final float defaultRv[] = { 0f, 0f, 0f }; + + /** + * Vibrate for s seconds. + */ + public static void vibrate(double s) { + Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + if (v != null) { + v.vibrate((int) (1000 * s)); + } + } + + /** + * Get an Overview of all Hardware Sensors of an Android Device + */ + public static String getHardwareSensors() { + SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + List allSensors = sm.getSensorList(Sensor.TYPE_ALL); + + if (allSensors != null) { + String resultString = ""; + for (Sensor s : allSensors) { + resultString += String.format("Name=" + s.getName()); + resultString += String.format(",Vendor=" + s.getVendor()); + resultString += String.format(",Version=" + s.getVersion()); + resultString += String.format(",MaximumRange=" + s.getMaximumRange()); + // XXX MinDelay is not in the 2.2 + //resultString += String.format(",MinDelay=" + s.getMinDelay()); + resultString += String.format(",Power=" + s.getPower()); + resultString += String.format(",Type=" + s.getType() + "\n"); + } + return resultString; + } + return ""; + } + + + /** + * Get Access to 3 Axis Hardware Sensors Accelerometer, Orientation and Magnetic Field Sensors + */ + public static class generic3AxisSensor implements SensorEventListener { + private final SensorManager sSensorManager; + private final Sensor sSensor; + private final int sSensorType; + SensorEvent sSensorEvent; + + public generic3AxisSensor(int sensorType) { + sSensorType = sensorType; + sSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + sSensor = sSensorManager.getDefaultSensor(sSensorType); + } + + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + public void onSensorChanged(SensorEvent event) { + sSensorEvent = event; + } + + /** + * Enable or disable the Sensor by registering/unregistering + */ + public void changeStatus(boolean enable) { + if (enable) { + sSensorManager.registerListener(this, sSensor, SensorManager.SENSOR_DELAY_NORMAL); + } else { + sSensorManager.unregisterListener(this, sSensor); + } + } + + /** + * Read the Sensor + */ + public float[] readSensor() { + if (sSensorEvent != null) { + return sSensorEvent.values; + } else { + return defaultRv; + } + } + } + + public static generic3AxisSensor accelerometerSensor = null; + public static generic3AxisSensor orientationSensor = null; + public static generic3AxisSensor magneticFieldSensor = null; + + /** + * functions for backward compatibility reasons + */ + + public static void accelerometerEnable(boolean enable) { + if ( accelerometerSensor == null ) + accelerometerSensor = new generic3AxisSensor(Sensor.TYPE_ACCELEROMETER); + accelerometerSensor.changeStatus(enable); + } + public static float[] accelerometerReading() { + if ( accelerometerSensor == null ) + return defaultRv; + return (float[]) accelerometerSensor.readSensor(); + } + public static void orientationSensorEnable(boolean enable) { + if ( orientationSensor == null ) + orientationSensor = new generic3AxisSensor(Sensor.TYPE_ORIENTATION); + orientationSensor.changeStatus(enable); + } + public static float[] orientationSensorReading() { + if ( orientationSensor == null ) + return defaultRv; + return (float[]) orientationSensor.readSensor(); + } + public static void magneticFieldSensorEnable(boolean enable) { + if ( magneticFieldSensor == null ) + magneticFieldSensor = new generic3AxisSensor(Sensor.TYPE_MAGNETIC_FIELD); + magneticFieldSensor.changeStatus(enable); + } + public static float[] magneticFieldSensorReading() { + if ( magneticFieldSensor == null ) + return defaultRv; + return (float[]) magneticFieldSensor.readSensor(); + } + + static public DisplayMetrics metrics = new DisplayMetrics(); + + /** + * Get display DPI. + */ + public static int getDPI() { + // AND: Shouldn't have to get the metrics like this every time... + PythonActivity.mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + return metrics.densityDpi; + } + + // /** + // * Show the soft keyboard. + // */ + // public static void showKeyboard(int input_type) { + // //Log.i("python", "hardware.Java show_keyword " input_type); + + // InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + + // SDLSurfaceView vw = (SDLSurfaceView) view; + + // int inputType = input_type; + + // if (vw.inputType != inputType){ + // vw.inputType = inputType; + // imm.restartInput(view); + // } + + // imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); + // } + + /** + * Hide the soft keyboard. + */ + public static void hideKeyboard() { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + /** + * Scan WiFi networks + */ + static List latestResult; + + public static void enableWifiScanner() + { + IntentFilter i = new IntentFilter(); + i.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + + context.registerReceiver(new BroadcastReceiver() { + + @Override + public void onReceive(Context c, Intent i) { + // Code to execute when SCAN_RESULTS_AVAILABLE_ACTION event occurs + WifiManager w = (WifiManager) c.getSystemService(Context.WIFI_SERVICE); + latestResult = w.getScanResults(); // Returns a of scanResults + } + + }, i); + + } + + public static String scanWifi() { + + // Now you can call this and it should execute the broadcastReceiver's + // onReceive() + if (latestResult != null){ + + String latestResultString = ""; + for (ScanResult result : latestResult) + { + latestResultString += String.format("%s\t%s\t%d\n", result.SSID, result.BSSID, result.level); + } + + return latestResultString; + } + + return ""; + } + + /** + * network state + */ + + public static boolean network_state = false; + + /** + * Check network state directly + * + * (only one connection can be active at a given moment, detects all network type) + * + */ + public static boolean checkNetwork() + { + boolean state = false; + final ConnectivityManager conMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + final NetworkInfo activeNetwork = conMgr.getActiveNetworkInfo(); + if (activeNetwork != null && activeNetwork.isConnected()) { + state = true; + } else { + state = false; + } + + return state; + } + + /** + * To recieve network state changes + */ + public static void registerNetworkCheck() + { + IntentFilter i = new IntentFilter(); + i.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + context.registerReceiver(new BroadcastReceiver() { + + @Override + public void onReceive(Context c, Intent i) { + network_state = checkNetwork(); + } + + }, i); + } + +} diff --git a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/ResourceManager.java b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/ResourceManager.java index 47455ab..a170c84 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/ResourceManager.java +++ b/p4a/pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/ResourceManager.java @@ -1,8 +1,7 @@ /** * This class takes care of managing resources for us. In our code, we * can't use R, since the name of the package containing R will - * change. (This same code is used in both org.renpy.android and - * org.renpy.pygame.) So this is the next best thing. + * change. So this is the next best thing. */ package org.renpy.android; diff --git a/p4a/pythonforandroid/bootstraps/common/build/templates/Service.tmpl.java b/p4a/pythonforandroid/bootstraps/common/build/templates/Service.tmpl.java index 3ed10c2..de84ac4 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/templates/Service.tmpl.java +++ b/p4a/pythonforandroid/bootstraps/common/build/templates/Service.tmpl.java @@ -1,18 +1,11 @@ package {{ args.package }}; -import android.os.Build; -import java.lang.reflect.Method; -import java.lang.reflect.InvocationTargetException; import android.content.Intent; import android.content.Context; -import android.app.Notification; -import android.app.PendingIntent; -import android.os.Bundle; -import org.kivy.android.PythonService; -import org.kivy.android.PythonActivity; +import {{ args.service_class_name }}; -public class Service{{ name|capitalize }} extends PythonService { +public class Service{{ name|capitalize }} extends {{ base_service_class }} { {% if sticky %} @Override public int startType() { @@ -20,54 +13,35 @@ public class Service{{ name|capitalize }} extends PythonService { } {% endif %} - {% if not foreground %} @Override - public boolean canDisplayNotification() { - return false; - } - {% endif %} - - @Override - protected void doStartForeground(Bundle extras) { - Notification notification; - Context context = getApplicationContext(); - Intent contextIntent = new Intent(context, PythonActivity.class); - PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { - notification = new Notification( - context.getApplicationInfo().icon, "{{ args.name }}", System.currentTimeMillis()); - try { - // prevent using NotificationCompat, this saves 100kb on apk - Method func = notification.getClass().getMethod( - "setLatestEventInfo", Context.class, CharSequence.class, - CharSequence.class, PendingIntent.class); - func.invoke(notification, context, "{{ args.name }}", "{{ name| capitalize }}", pIntent); - } catch (NoSuchMethodException | IllegalAccessException | - IllegalArgumentException | InvocationTargetException e) { - } - } else { - Notification.Builder builder = new Notification.Builder(context); - builder.setContentTitle("{{ args.name }}"); - builder.setContentText("{{ name| capitalize }}"); - builder.setContentIntent(pIntent); - builder.setSmallIcon(context.getApplicationInfo().icon); - notification = builder.build(); - } - startForeground({{ service_id }}, notification); + protected int getServiceId() { + return {{ service_id }}; } static public void start(Context ctx, String pythonServiceArgument) { + Intent intent = getDefaultIntent(ctx, pythonServiceArgument); + ctx.startService(intent); + } + + static public Intent getDefaultIntent(Context ctx, String pythonServiceArgument) { Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); String argument = ctx.getFilesDir().getAbsolutePath() + "/app"; intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath()); intent.putExtra("androidArgument", argument); + intent.putExtra("serviceTitle", "{{ args.name }}"); + intent.putExtra("serviceDescription", "{{ name|capitalize }}"); intent.putExtra("serviceEntrypoint", "{{ entrypoint }}"); intent.putExtra("pythonName", "{{ name }}"); + intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}"); intent.putExtra("pythonHome", argument); intent.putExtra("pythonPath", argument + ":" + argument + "/lib"); intent.putExtra("pythonServiceArgument", pythonServiceArgument); - ctx.startService(intent); + return intent; + } + + @Override + protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) { + return Service{{ name|capitalize }}.getDefaultIntent(ctx, pythonServiceArgument); } static public void stop(Context ctx) { diff --git a/p4a/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle b/p4a/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle index 32bd091..bb00039 100644 --- a/p4a/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle +++ b/p4a/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.4' + classpath 'com.android.tools.build:gradle:7.1.2' } } @@ -13,23 +13,45 @@ allprojects { repositories { google() jcenter() - flatDir { - dirs 'libs' - } + {%- for repo in args.gradle_repositories %} + {{repo}} + {%- endfor %} + flatDir { + dirs 'libs' + } } } +{% if is_library %} +apply plugin: 'com.android.library' +{% else %} apply plugin: 'com.android.application' +{% endif %} android { - compileSdkVersion {{ android_api }} - buildToolsVersion '{{ build_tools_version }}' - defaultConfig { - minSdkVersion {{ args.min_sdk_version }} - targetSdkVersion {{ android_api }} - versionCode {{ args.numeric_version }} - versionName '{{ args.version }}' + compileSdkVersion {{ android_api }} + buildToolsVersion '{{ build_tools_version }}' + defaultConfig { + minSdkVersion {{ args.min_sdk_version }} + targetSdkVersion {{ android_api }} + versionCode {{ args.numeric_version }} + versionName '{{ args.version }}' + manifestPlaceholders = {{ args.manifest_placeholders}} + } + + + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + {% if debug_build -%} + doNotStrip '**/*.so' + {% else %} + exclude 'lib/**/gdbserver' + exclude 'lib/**/gdb.setup' + {%- endif %} } + {% if args.sign -%} signingConfigs { @@ -40,41 +62,73 @@ android { keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD") } } + {%- endif %} - buildTypes { - debug { - } - release { - {% if args.sign -%} - signingConfig signingConfigs.release - {%- endif %} - } - } + {% if args.packaging_options -%} + packagingOptions { + {%- for option in args.packaging_options %} + {{option}} + {%- endfor %} + } + {%- endif %} + + buildTypes { + debug { + } + release { + {% if args.sign -%} + signingConfig signingConfigs.release + {%- endif %} + } + } compileOptions { + {% if args.enable_androidx %} + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + {% else %} sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 + {% endif %} + {%- for option in args.compile_options %} + {{option}} + {%- endfor %} } sourceSets { main { jniLibs.srcDir 'libs' + java { + + {%- for adir, pattern in args.extra_source_dirs -%} + srcDir '{{adir}}' + {%- endfor -%} + } + } + } + + aaptOptions { + noCompress "tflite" } } dependencies { - {%- for aar in aars %} - compile(name: '{{ aar }}', ext: 'aar') - {%- endfor -%} - {%- for jar in jars %} - compile files('src/main/libs/{{ jar }}') - {%- endfor -%} - {%- if args.depends -%} - {%- for depend in args.depends %} - compile '{{ depend }}' - {%- endfor %} - {%- endif %} + {%- for aar in aars %} + implementation(name: '{{ aar }}', ext: 'aar') + {%- endfor -%} + {%- for jar in jars %} + implementation files('src/main/libs/{{ jar }}') + {%- endfor -%} + {%- if args.depends -%} + {%- for depend in args.depends %} + implementation '{{ depend }}' + {%- endfor %} + {%- endif %} + {% if args.presplash_lottie %} + implementation 'com.airbnb.android:lottie:3.4.0' + {%- endif %} } + diff --git a/p4a/pythonforandroid/bootstraps/common/build/templates/gradle.tmpl.properties b/p4a/pythonforandroid/bootstraps/common/build/templates/gradle.tmpl.properties new file mode 100644 index 0000000..f99dd5a --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/templates/gradle.tmpl.properties @@ -0,0 +1,4 @@ +{% if args.enable_androidx %} +android.useAndroidX=true +android.enableJetifier=true +{% endif %} \ No newline at end of file diff --git a/p4a/pythonforandroid/bootstraps/common/build/templates/lottie.xml b/p4a/pythonforandroid/bootstraps/common/build/templates/lottie.xml new file mode 100644 index 0000000..49fe8c9 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/common/build/templates/lottie.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/p4a/pythonforandroid/bootstraps/empty/__init__.py b/p4a/pythonforandroid/bootstraps/empty/__init__.py new file mode 100644 index 0000000..8d4c196 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/empty/__init__.py @@ -0,0 +1,16 @@ +from pythonforandroid.toolchain import Bootstrap + + +class EmptyBootstrap(Bootstrap): + name = 'empty' + + recipe_depends = [] + + can_be_chosen_automatically = False + + def assemble_distribution(self): + print('empty bootstrap has no distribute') + exit(1) + + +bootstrap = EmptyBootstrap() diff --git a/p4a/pythonforandroid/bootstraps/empty/build/.gitkeep b/p4a/pythonforandroid/bootstraps/empty/build/.gitkeep new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/empty/build/.gitkeep @@ -0,0 +1 @@ + diff --git a/p4a/pythonforandroid/bootstraps/lbry/__init__.py b/p4a/pythonforandroid/bootstraps/lbry/__init__.py index 0cafc21..e1ae2d8 100644 --- a/p4a/pythonforandroid/bootstraps/lbry/__init__.py +++ b/p4a/pythonforandroid/bootstraps/lbry/__init__.py @@ -6,22 +6,21 @@ from os import walk import glob import sh - EXCLUDE_EXTS = (".py", ".pyc", ".so.o", ".so.a", ".so.libs", ".pyx") class LbryBootstrap(Bootstrap): name = 'lbry' - recipe_depends = ['genericndkbuild', ('python2', 'python3crystax')] + recipe_depends = ['genericndkbuild'] - def run_distribute(self): + def assemble_distribution(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 - crystax_python_dir = join("crystax_python", "crystax_python") + python_install_dir = self.ctx.get_python_install_dir(arch.arch) + #from_crystax = self.ctx.python_recipe.from_crystax + #crystax_python_dir = join("crystax_python", "crystax_python") if len(self.ctx.archs) > 1: raise ValueError("LBRY/gradle support only one arch") @@ -39,98 +38,101 @@ class LbryBootstrap(Bootstrap): with current_directory(self.dist_dir): info("Copying Python distribution") - if not exists("private") and not from_crystax: - ensure_dir("private") - if not exists("crystax_python") and from_crystax: - ensure_dir(crystax_python_dir) + # if not exists("private"): + # ensure_dir("private") - hostpython = sh.Command(self.ctx.hostpython) - if not from_crystax: - try: - shprint(hostpython, '-OO', '-m', 'compileall', - python_install_dir, - _tail=10, _filterout="^Listing") - except sh.ErrorReturnCode: - pass - if not exists('python-install'): - shprint( - sh.cp, '-a', python_install_dir, './python-install') + # gethostpython? + # hostpython = sh.Command(self.ctx.hostpython) + # if not from_crystax: + # try: + # shprint(hostpython, '-OO', '-m', 'compileall', + # python_install_dir, + # _tail=10, _filterout="^Listing") + # except sh.ErrorReturnCode: + # pass + # if not exists('python-install'): + # shprint( + # sh.cp, '-a', python_install_dir, './python-install') self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) self.distribute_javaclasses(self.ctx.javaclass_dir, dest_dir=join("src", "main", "java")) - if not from_crystax: - info("Filling private directory") - if not exists(join("private", "lib")): - info("private/lib does not exist, making") - shprint(sh.cp, "-a", - join("python-install", "lib"), "private") - shprint(sh.mkdir, "-p", - join("private", "include", "python2.7")) + python_bundle_dir = join(f'_python_bundle__{arch.arch}', '_python_bundle') + ensure_dir(python_bundle_dir) + site_packages_dir = self.ctx.python_recipe.create_python_bundle( + join(self.dist_dir, python_bundle_dir), arch) - libpymodules_fn = join("libs", arch.arch, "libpymodules.so") - if exists(libpymodules_fn): - shprint(sh.mv, libpymodules_fn, 'private/') - shprint(sh.cp, - join('python-install', 'include', - 'python2.7', 'pyconfig.h'), - join('private', 'include', 'python2.7/')) + # if not from_crystax: + # info("Filling private directory") + # if not exists(join("private", "lib")): + # info("private/lib does not exist, making") + # shprint(sh.cp, "-a", + # join("python-install", "lib"), "private") + # shprint(sh.mkdir, "-p", + # join("private", "include", "python2.7")) - info('Removing some unwanted files') - shprint(sh.rm, '-f', join('private', 'lib', 'libpython2.7.so')) - shprint(sh.rm, '-rf', join('private', 'lib', 'pkgconfig')) + # libpymodules_fn = join("libs", arch.arch, "libpymodules.so") + # if exists(libpymodules_fn): + # shprint(sh.mv, libpymodules_fn, 'private/') + # shprint(sh.cp, + # join('python-install', 'include', + # 'python2.7', 'pyconfig.h'), + # join('private', 'include', 'python2.7/')) + # + # info('Removing some unwanted files') + # shprint(sh.rm, '-f', join('private', 'lib', 'libpython2.7.so')) + # shprint(sh.rm, '-rf', join('private', 'lib', 'pkgconfig')) - libdir = join(self.dist_dir, 'private', 'lib', 'python2.7') - site_packages_dir = join(libdir, 'site-packages') - with current_directory(libdir): - removes = [] - for dirname, root, filenames in walk("."): - for filename in filenames: - for suffix in EXCLUDE_EXTS: - if filename.endswith(suffix): - removes.append(filename) - shprint(sh.rm, '-f', *removes) + # libdir = join(self.dist_dir, 'private', 'lib', 'python2.7') + # with current_directory(libdir): + # removes = [] + # for dirname, root, filenames in walk("."): + # for filename in filenames: + # for suffix in EXCLUDE_EXTS: + # if filename.endswith(suffix): + # removes.append(filename) + # shprint(sh.rm, '-f', *removes) + # + # info('Deleting some other stuff not used on android') + # # To quote the original distribute.sh, 'well...' + # shprint(sh.rm, '-rf', 'lib2to3') + # shprint(sh.rm, '-rf', 'idlelib') + # for filename in glob.glob('config/libpython*.a'): + # shprint(sh.rm, '-f', filename) + # shprint(sh.rm, '-rf', 'config/python.o') - info('Deleting some other stuff not used on android') - # To quote the original distribute.sh, 'well...' - shprint(sh.rm, '-rf', 'lib2to3') - shprint(sh.rm, '-rf', 'idlelib') - for filename in glob.glob('config/libpython*.a'): - shprint(sh.rm, '-f', filename) - shprint(sh.rm, '-rf', 'config/python.o') - - else: # Python *is* loaded from crystax - ndk_dir = self.ctx.ndk_dir - py_recipe = self.ctx.python_recipe - python_dir = join(ndk_dir, 'sources', 'python', - py_recipe.version, 'libs', arch.arch) - shprint(sh.cp, '-r', join(python_dir, - 'stdlib.zip'), crystax_python_dir) - shprint(sh.cp, '-r', join(python_dir, - 'modules'), crystax_python_dir) - shprint(sh.cp, '-r', self.ctx.get_python_install_dir(), - join(crystax_python_dir, 'site-packages')) - - info('Renaming .so files to reflect cross-compile') - site_packages_dir = join(crystax_python_dir, "site-packages") - find_ret = shprint( - sh.find, site_packages_dir, '-iname', '*.so') - filenames = find_ret.stdout.decode('utf-8').split('\n')[:-1] - for filename in filenames: - parts = filename.split('.') - if len(parts) <= 2: - continue - shprint(sh.mv, filename, filename.split('.')[0] + '.so') - site_packages_dir = join(abspath(curdir), - site_packages_dir) + # else: # Python *is* loaded from crystax + # ndk_dir = self.ctx.ndk_dir + # py_recipe = self.ctx.python_recipe + # python_dir = join(ndk_dir, 'sources', 'python', + # py_recipe.version, 'libs', arch.arch) + # shprint(sh.cp, '-r', join(python_dir, + # 'stdlib.zip'), crystax_python_dir) + # shprint(sh.cp, '-r', join(python_dir, + # 'modules'), crystax_python_dir) + # shprint(sh.cp, '-r', self.ctx.get_python_install_dir(), + # join(crystax_python_dir, 'site-packages')) + # + # info('Renaming .so files to reflect cross-compile') + # site_packages_dir = join(crystax_python_dir, "site-packages") + # find_ret = shprint( + # sh.find, site_packages_dir, '-iname', '*.so') + # filenames = find_ret.stdout.decode('utf-8').split('\n')[:-1] + # for filename in filenames: + # parts = filename.split('.') + # if len(parts) <= 2: + # continue + # shprint(sh.mv, filename, filename.split('.')[0] + '.so') + # site_packages_dir = join(abspath(curdir), + # site_packages_dir) if 'sqlite3' not in self.ctx.recipe_build_order: with open('blacklist.txt', 'a') as fileh: fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n') self.strip_libraries(arch) self.fry_eggs(site_packages_dir) - super(LbryBootstrap, self).run_distribute() + super().assemble_distribution() bootstrap = LbryBootstrap() diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/build.py b/p4a/pythonforandroid/bootstraps/lbry/build/build.py index 59857a9..49cc5f5 100755 --- a/p4a/pythonforandroid/bootstraps/lbry/build/build.py +++ b/p4a/pythonforandroid/bootstraps/lbry/build/build.py @@ -435,7 +435,7 @@ main.py that loads it.''') if exists('build.properties'): os.remove('build.properties') -def parse_args(args=None): +def parse_args_and_make_package(args=None): global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON default_android_api = 12 import argparse @@ -505,6 +505,10 @@ tools directory of the Android SDK. default=join(curdir, 'whitelist.txt'), help=('Use a whitelist file to prevent blacklisting of ' 'file in the final APK')) + ap.add_argument('--release', dest='build_mode', action='store_const', + const='release', default='debug', + help='Build your app as a non-debug release build. ' + '(Disables gdb debugging among other things)') ap.add_argument('--add-jar', dest='add_jar', action='append', help=('Add a Java .jar to the libs, so you can access its ' 'classes with pyjnius. You can specify this ' @@ -604,4 +608,4 @@ tools directory of the Android SDK. if __name__ == "__main__": - parse_args() + parse_args_and_make_package() diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/gradle/wrapper/gradle-wrapper.properties b/p4a/pythonforandroid/bootstraps/lbry/build/gradle/wrapper/gradle-wrapper.properties index 7f81b24..dd012b8 100644 --- a/p4a/pythonforandroid/bootstraps/lbry/build/gradle/wrapper/gradle-wrapper.properties +++ b/p4a/pythonforandroid/bootstraps/lbry/build/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle b/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle index ff67d1d..0522380 100644 --- a/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle +++ b/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "https://jitpack.io" } } dependencies { - classpath 'com.android.tools.build:gradle:3.6.4' + classpath 'com.android.tools.build:gradle:7.1.2' } } @@ -69,15 +69,15 @@ android { } {%- endif %} - buildTypes { - debug { - } - release { - {% if args.sign -%} - signingConfig signingConfigs.release - {%- endif %} - } - } +// buildTypes { +// debug { +// } +// release { +// {% if args.sign -%} +// signingConfig signingConfigs.release +// {%- endif %} +// } +// } sourceSets { main { @@ -104,68 +104,68 @@ subprojects { } } -nexusPublishing { - repositories { - sonatype { - stagingProfileId = sonatypeStagingProfileId - username = ossrhUsername - password = ossrhPassword - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - } - } -} +//nexusPublishing { +// repositories { +// sonatype { +// stagingProfileId = sonatypeStagingProfileId +// username = ossrhUsername +// password = ossrhPassword +// nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) +// snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) +// } +// } +//} -afterEvaluate { - publishing { - publications { - release(MavenPublication) { - groupId 'io.lbry' - artifactId 'lbrysdk64' - version '{{ args.version }}' +//afterEvaluate { +// publishing { +// publications { +// release(MavenPublication) { +// groupId 'io.lbry' +// artifactId 'lbrysdk64' +// version '{{ args.version }}' +// +// from components.release +// +// pom { +// name = 'LBRY SDK for Android' +// description = 'The LBRY SDK packaged as an Android AAR' +// url = 'https://github.com/lbryio/lbry-android-sdk' +// licenses { +// license { +// name = 'MIT License' +// url = 'https://raw.githubusercontent.com/lbryio/lbry-android-sdk/master/LICENSE' +// } +// } +// developers { +// developer { +// id = 'akinwale' +// name = 'Akinwale Ariwodola' +// email = 'akinwale@lbry.com' +// } +// } +// +// scm { +// url = 'https://github.com/lbryio/lbry-android-sdk' +// connection = 'scm:git:github.com/lbryio/lbry-android-sdk.git' +// developerConnection = 'scm:git:ssh://github.com/lbryio/lbry-android-sdk.git' +// } +// } +// } +// } +// } +//} - from components.release - - pom { - name = 'LBRY SDK for Android' - description = 'The LBRY SDK packaged as an Android AAR' - url = 'https://github.com/lbryio/lbry-android-sdk' - licenses { - license { - name = 'MIT License' - url = 'https://raw.githubusercontent.com/lbryio/lbry-android-sdk/master/LICENSE' - } - } - developers { - developer { - id = 'akinwale' - name = 'Akinwale Ariwodola' - email = 'akinwale@lbry.com' - } - } - - scm { - url = 'https://github.com/lbryio/lbry-android-sdk' - connection = 'scm:git:github.com/lbryio/lbry-android-sdk.git' - developerConnection = 'scm:git:ssh://github.com/lbryio/lbry-android-sdk.git' - } - } - } - } - } -} - -signing { - sign publishing.publications -} +//signing { +// sign publishing.publications +//} dependencies { {%- for aar in aars %} - compile(name: '{{ aar }}', ext: 'aar') + implementation (name: '{{ aar }}', ext: 'aar') {%- endfor -%} {%- if args.depends -%} {%- for depend in args.depends %} - compile '{{ depend }}' + implementation '{{ depend }}' {%- endfor %} {%- endif %} } diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle.arm b/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle.arm index 4f4a6e8..fa54124 100644 --- a/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle.arm +++ b/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle.arm @@ -166,7 +166,7 @@ dependencies { {%- endfor -%} {%- if args.depends -%} {%- for depend in args.depends %} - compile '{{ depend }}' + implement '{{ depend }}' {%- endfor %} {%- endif %} } diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/templates/gradle.properties b/p4a/pythonforandroid/bootstraps/lbry/build/templates/gradle.properties index 88524aa..7a61dce 100644 --- a/p4a/pythonforandroid/bootstraps/lbry/build/templates/gradle.properties +++ b/p4a/pythonforandroid/bootstraps/lbry/build/templates/gradle.properties @@ -1,5 +1,6 @@ android.useAndroidX=true android.enableJetifier=true +org.gradle.jvmargs=-Xmx4096m ossrhUsername={{ env["SONATYPE_USERNAME"] }} ossrhPassword={{ env["SONATYPE_PASSWORD"] }} diff --git a/p4a/pythonforandroid/bootstraps/sdl2/__init__.py b/p4a/pythonforandroid/bootstraps/sdl2/__init__.py new file mode 100644 index 0000000..662d43c --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/__init__.py @@ -0,0 +1,52 @@ +from pythonforandroid.toolchain import ( + Bootstrap, shprint, current_directory, info, info_main) +from pythonforandroid.util import ensure_dir +from os.path import join +import sh + + +class SDL2GradleBootstrap(Bootstrap): + name = 'sdl2' + + recipe_depends = list( + set(Bootstrap.recipe_depends).union({'sdl2'}) + ) + + def assemble_distribution(self): + info_main("# Creating Android project ({})".format(self.name)) + + info("Copying SDL2/gradle build") + shprint(sh.rm, "-rf", self.dist_dir) + shprint(sh.cp, "-r", self.build_dir, self.dist_dir) + + # either the build use environment variable (ANDROID_HOME) + # or the local.properties if exists + with current_directory(self.dist_dir): + with open('local.properties', 'w') as fileh: + fileh.write('sdk.dir={}'.format(self.ctx.sdk_dir)) + + with current_directory(self.dist_dir): + info("Copying Python distribution") + + self.distribute_javaclasses(self.ctx.javaclass_dir, + dest_dir=join("src", "main", "java")) + + for arch in self.ctx.archs: + python_bundle_dir = join(f'_python_bundle__{arch.arch}', '_python_bundle') + ensure_dir(python_bundle_dir) + + self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) + site_packages_dir = self.ctx.python_recipe.create_python_bundle( + join(self.dist_dir, python_bundle_dir), arch) + if not self.ctx.with_debug_symbols: + self.strip_libraries(arch) + self.fry_eggs(site_packages_dir) + + if 'sqlite3' not in self.ctx.recipe_build_order: + with open('blacklist.txt', 'a') as fileh: + fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n') + + super().assemble_distribution() + + +bootstrap = SDL2GradleBootstrap() diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/.gitignore b/p4a/pythonforandroid/bootstraps/sdl2/build/.gitignore new file mode 100644 index 0000000..a1fc39c --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/.gitignore @@ -0,0 +1,14 @@ +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/blacklist.txt b/p4a/pythonforandroid/bootstraps/sdl2/build/blacklist.txt new file mode 100644 index 0000000..d5e230c --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/blacklist.txt @@ -0,0 +1,84 @@ +# prevent user to include invalid extensions +*.apk +*.aab +*.apks +*.pxd + +# eggs +*.egg-info + +# unit test +unittest/* + +# python config +config/makesetup + +# unused kivy files (platform specific) +kivy/input/providers/wm_* +kivy/input/providers/mactouch* +kivy/input/providers/probesysfs* +kivy/input/providers/mtdev* +kivy/input/providers/hidinput* +kivy/core/camera/camera_videocapture* +kivy/core/spelling/*osx* +kivy/core/video/video_pyglet* +kivy/tools +kivy/tests/* +kivy/*/*.h +kivy/*/*.pxi + +# unused encodings +lib-dynload/*codec* +encodings/cp*.pyo +encodings/tis* +encodings/shift* +encodings/bz2* +encodings/iso* +encodings/undefined* +encodings/johab* +encodings/p* +encodings/m* +encodings/euc* +encodings/k* +encodings/unicode_internal* +encodings/quo* +encodings/gb* +encodings/big5* +encodings/hp* +encodings/hz* + +# unused python modules +bsddb/* +wsgiref/* +hotshot/* +pydoc_data/* +tty.pyo +anydbm.pyo +nturl2path.pyo +LICENCE.txt +macurl2path.pyo +dummy_threading.pyo +audiodev.pyo +antigravity.pyo +dumbdbm.pyo +sndhdr.pyo +__phello__.foo.pyo +sunaudio.pyo +os2emxpath.pyo +multiprocessing/dummy* + +# unused binaries python modules +lib-dynload/termios.so +lib-dynload/_lsprof.so +lib-dynload/*audioop.so +lib-dynload/_hotshot.so +lib-dynload/_heapq.so +lib-dynload/_json.so +lib-dynload/grp.so +lib-dynload/resource.so +lib-dynload/pyexpat.so +lib-dynload/_ctypes_test.so +lib-dynload/_testcapi.so + +# odd files +plat-linux3/regen diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/jni/Application.mk b/p4a/pythonforandroid/bootstraps/sdl2/build/jni/Application.mk new file mode 100644 index 0000000..1559853 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/jni/Application.mk @@ -0,0 +1,8 @@ + +# Uncomment this if you're using STL in your project +# See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information +# APP_STL := stlport_static + +# APP_ABI := armeabi armeabi-v7a x86 +APP_ABI := $(ARCH) +APP_PLATFORM := $(NDK_API) diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/jni/application/src/Android_static.mk b/p4a/pythonforandroid/bootstraps/sdl2/build/jni/application/src/Android_static.mk new file mode 100644 index 0000000..517660b --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/jni/application/src/Android_static.mk @@ -0,0 +1,12 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := main + +LOCAL_SRC_FILES := start.c + +LOCAL_STATIC_LIBRARIES := SDL2_static + +include $(BUILD_SHARED_LIBRARY) +$(call import-module,SDL)LOCAL_PATH := $(call my-dir) diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/jni/application/src/bootstrap_name.h b/p4a/pythonforandroid/bootstraps/sdl2/build/jni/application/src/bootstrap_name.h new file mode 100644 index 0000000..83dec51 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/jni/application/src/bootstrap_name.h @@ -0,0 +1,5 @@ + +#define BOOTSTRAP_NAME_SDL2 + +const char bootstrap_name[] = "SDL2"; // capitalized for historic reasons + diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/assets/.gitkeep b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/.gitkeep b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java new file mode 100644 index 0000000..58a1c5e --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java @@ -0,0 +1,19 @@ +package org.kivy.android; + +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.Context; + +public class GenericBroadcastReceiver extends BroadcastReceiver { + + GenericBroadcastReceiverCallback listener; + + public GenericBroadcastReceiver(GenericBroadcastReceiverCallback listener) { + super(); + this.listener = listener; + } + + public void onReceive(Context context, Intent intent) { + this.listener.onReceive(context, intent); + } +} diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java new file mode 100644 index 0000000..1a87c98 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java @@ -0,0 +1,8 @@ +package org.kivy.android; + +import android.content.Intent; +import android.content.Context; + +public interface GenericBroadcastReceiverCallback { + void onReceive(Context context, Intent intent); +}; diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java new file mode 100644 index 0000000..361975a --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java @@ -0,0 +1,643 @@ +package org.kivy.android; + +import java.io.InputStream; +import java.io.FileWriter; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.PowerManager; +import android.util.Log; +import android.view.inputmethod.InputMethodManager; +import android.view.SurfaceView; +import android.view.ViewGroup; +import android.view.View; +import android.widget.ImageView; +import android.widget.Toast; +import android.content.res.Resources.NotFoundException; + +import org.libsdl.app.SDLActivity; + +import org.kivy.android.launcher.Project; + +import org.renpy.android.ResourceManager; + + +public class PythonActivity extends SDLActivity { + private static final String TAG = "PythonActivity"; + + public static PythonActivity mActivity = null; + + private ResourceManager resourceManager = null; + private Bundle mMetaData = null; + private PowerManager.WakeLock mWakeLock = null; + + public String getAppRoot() { + String app_root = getFilesDir().getAbsolutePath() + "/app"; + return app_root; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "PythonActivity onCreate running"); + resourceManager = new ResourceManager(this); + + Log.v(TAG, "About to do super onCreate"); + super.onCreate(savedInstanceState); + Log.v(TAG, "Did super onCreate"); + + this.mActivity = this; + this.showLoadingScreen(this.getLoadingScreen()); + + new UnpackFilesTask().execute(getAppRoot()); + } + + public void loadLibraries() { + String app_root = new String(getAppRoot()); + File app_root_file = new File(app_root); + PythonUtil.loadLibraries(app_root_file, + new File(getApplicationInfo().nativeLibraryDir)); + } + + /** + * Show an error using a toast. (Only makes sense from non-UI + * threads.) + */ + public void toastError(final String msg) { + + final Activity thisActivity = this; + + runOnUiThread(new Runnable () { + public void run() { + Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show(); + } + }); + + // Wait to show the error. + synchronized (this) { + try { + this.wait(1000); + } catch (InterruptedException e) { + } + } + } + + private class UnpackFilesTask extends AsyncTask { + @Override + protected String doInBackground(String... params) { + File app_root_file = new File(params[0]); + Log.v(TAG, "Ready to unpack"); + PythonUtil.unpackAsset(mActivity, "private", app_root_file, true); + PythonUtil.unpackPyBundle(mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false); + return null; + } + + @Override + protected void onPostExecute(String result) { + // Figure out the directory where the game is. If the game was + // given to us via an intent, then we use the scheme-specific + // part of that intent to determine the file to launch. We + // also use the android.txt file to determine the orientation. + // + // Otherwise, we use the public data, if we have it, or the + // private data if we do not. + mActivity.finishLoad(); + + // finishLoad called setContentView with the SDL view, which + // removed the loading screen. However, we still need it to + // show until the app is ready to render, so pop it back up + // on top of the SDL view. + mActivity.showLoadingScreen(getLoadingScreen()); + + String app_root_dir = getAppRoot(); + if (getIntent() != null && getIntent().getAction() != null && + getIntent().getAction().equals("org.kivy.LAUNCH")) { + File path = new File(getIntent().getData().getSchemeSpecificPart()); + + Project p = Project.scanDirectory(path); + String entry_point = getEntryPoint(p.dir); + SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", p.dir + "/" + entry_point); + SDLActivity.nativeSetenv("ANDROID_ARGUMENT", p.dir); + SDLActivity.nativeSetenv("ANDROID_APP_PATH", p.dir); + + if (p != null) { + if (p.landscape) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + } + + // Let old apps know they started. + try { + FileWriter f = new FileWriter(new File(path, ".launch")); + f.write("started"); + f.close(); + } catch (IOException e) { + // pass + } + } else { + String entry_point = getEntryPoint(app_root_dir); + SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point); + SDLActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir); + SDLActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir); + } + + String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); + Log.v(TAG, "Setting env vars for start.c and Python to use"); + SDLActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory); + SDLActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir); + SDLActivity.nativeSetenv("PYTHONHOME", app_root_dir); + SDLActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); + SDLActivity.nativeSetenv("PYTHONOPTIMIZE", "2"); + + try { + Log.v(TAG, "Access to our meta-data..."); + mActivity.mMetaData = mActivity.getPackageManager().getApplicationInfo( + mActivity.getPackageName(), PackageManager.GET_META_DATA).metaData; + + PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE); + if ( mActivity.mMetaData.getInt("wakelock") == 1 ) { + mActivity.mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Screen On"); + mActivity.mWakeLock.acquire(); + } + if ( mActivity.mMetaData.getInt("surface.transparent") != 0 ) { + Log.v(TAG, "Surface will be transparent."); + getSurface().setZOrderOnTop(true); + getSurface().getHolder().setFormat(PixelFormat.TRANSPARENT); + } else { + Log.i(TAG, "Surface will NOT be transparent"); + } + } catch (PackageManager.NameNotFoundException e) { + } + + // Launch app if that hasn't been done yet: + if (mActivity.mHasFocus && ( + // never went into proper resume state: + mActivity.mCurrentNativeState == NativeState.INIT || + ( + // resumed earlier but wasn't ready yet + mActivity.mCurrentNativeState == NativeState.RESUMED && + mActivity.mSDLThread == null + ))) { + // Because sometimes the app will get stuck here and never + // actually run, ensure that it gets launched if we're active: + mActivity.resumeNativeThread(); + } + } + + @Override + protected void onPreExecute() { + } + + @Override + protected void onProgressUpdate(Void... values) { + } + } + + public static ViewGroup getLayout() { + return mLayout; + } + + public static SurfaceView getSurface() { + return mSurface; + } + + //---------------------------------------------------------------------------- + // Listener interface for onNewIntent + // + + public interface NewIntentListener { + void onNewIntent(Intent intent); + } + + private List newIntentListeners = null; + + public void registerNewIntentListener(NewIntentListener listener) { + if ( this.newIntentListeners == null ) + this.newIntentListeners = Collections.synchronizedList(new ArrayList()); + this.newIntentListeners.add(listener); + } + + public void unregisterNewIntentListener(NewIntentListener listener) { + if ( this.newIntentListeners == null ) + return; + this.newIntentListeners.remove(listener); + } + + @Override + protected void onNewIntent(Intent intent) { + if ( this.newIntentListeners == null ) + return; + this.onResume(); + synchronized ( this.newIntentListeners ) { + Iterator iterator = this.newIntentListeners.iterator(); + while ( iterator.hasNext() ) { + (iterator.next()).onNewIntent(intent); + } + } + } + + //---------------------------------------------------------------------------- + // Listener interface for onActivityResult + // + + public interface ActivityResultListener { + void onActivityResult(int requestCode, int resultCode, Intent data); + } + + private List activityResultListeners = null; + + public void registerActivityResultListener(ActivityResultListener listener) { + if ( this.activityResultListeners == null ) + this.activityResultListeners = Collections.synchronizedList(new ArrayList()); + this.activityResultListeners.add(listener); + } + + public void unregisterActivityResultListener(ActivityResultListener listener) { + if ( this.activityResultListeners == null ) + return; + this.activityResultListeners.remove(listener); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent intent) { + if ( this.activityResultListeners == null ) + return; + this.onResume(); + synchronized ( this.activityResultListeners ) { + Iterator iterator = this.activityResultListeners.iterator(); + while ( iterator.hasNext() ) + (iterator.next()).onActivityResult(requestCode, resultCode, intent); + } + } + + public static void start_service( + String serviceTitle, + String serviceDescription, + String pythonServiceArgument + ) { + _do_start_service( + serviceTitle, serviceDescription, pythonServiceArgument, true + ); + } + + public static void start_service_not_as_foreground( + String serviceTitle, + String serviceDescription, + String pythonServiceArgument + ) { + _do_start_service( + serviceTitle, serviceDescription, pythonServiceArgument, false + ); + } + + public static void _do_start_service( + String serviceTitle, + String serviceDescription, + String pythonServiceArgument, + boolean showForegroundNotification + ) { + Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); + String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); + String app_root_dir = PythonActivity.mActivity.getAppRoot(); + String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service"); + serviceIntent.putExtra("androidPrivate", argument); + serviceIntent.putExtra("androidArgument", app_root_dir); + serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point); + serviceIntent.putExtra("pythonName", "python"); + serviceIntent.putExtra("pythonHome", app_root_dir); + serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib"); + serviceIntent.putExtra("serviceStartAsForeground", + (showForegroundNotification ? "true" : "false") + ); + serviceIntent.putExtra("serviceTitle", serviceTitle); + serviceIntent.putExtra("serviceDescription", serviceDescription); + serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument); + PythonActivity.mActivity.startService(serviceIntent); + } + + public static void stop_service() { + Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); + PythonActivity.mActivity.stopService(serviceIntent); + } + + /** Loading screen view **/ + public static ImageView mImageView = null; + public static View mLottieView = null; + /** Whether main routine/actual app has started yet **/ + protected boolean mAppConfirmedActive = false; + /** Timer for delayed loading screen removal. **/ + protected Timer loadingScreenRemovalTimer = null; + + // Overridden since it's called often, to check whether to remove the + // loading screen: + @Override + protected boolean sendCommand(int command, Object data) { + boolean result = super.sendCommand(command, data); + considerLoadingScreenRemoval(); + return result; + } + + /** Confirm that the app's main routine has been launched. + **/ + @Override + public void appConfirmedActive() { + if (!mAppConfirmedActive) { + Log.v(TAG, "appConfirmedActive() -> preparing loading screen removal"); + mAppConfirmedActive = true; + considerLoadingScreenRemoval(); + } + } + + /** This is called from various places to check whether the app's main + * routine has been launched already, and if it has, then the loading + * screen will be removed. + **/ + public void considerLoadingScreenRemoval() { + if (loadingScreenRemovalTimer != null) + return; + runOnUiThread(new Runnable() { + public void run() { + if (((PythonActivity)PythonActivity.mSingleton).mAppConfirmedActive && + loadingScreenRemovalTimer == null) { + // Remove loading screen but with a delay. + // (app can use p4a's android.loadingscreen module to + // do it quicker if it wants to) + // get a handler (call from main thread) + // this will run when timer elapses + TimerTask removalTask = new TimerTask() { + @Override + public void run() { + // post a runnable to the handler + runOnUiThread(new Runnable() { + @Override + public void run() { + PythonActivity activity = + ((PythonActivity)PythonActivity.mSingleton); + if (activity != null) + activity.removeLoadingScreen(); + } + }); + } + }; + loadingScreenRemovalTimer = new Timer(); + loadingScreenRemovalTimer.schedule(removalTask, 5000); + } + } + }); + } + + public void removeLoadingScreen() { + runOnUiThread(new Runnable() { + public void run() { + View view = mLottieView != null ? mLottieView : mImageView; + if (view != null && view.getParent() != null) { + ((ViewGroup)view.getParent()).removeView(view); + mLottieView = null; + mImageView = null; + } + } + }); + } + + public String getEntryPoint(String search_dir) { + /* Get the main file (.pyc|.py) depending on if we + * have a compiled version or not. + */ + List entryPoints = new ArrayList(); + entryPoints.add("main.pyc"); // python 3 compiled files + for (String value : entryPoints) { + File mainFile = new File(search_dir + "/" + value); + if (mainFile.exists()) { + return value; + } + } + return "main.py"; + } + + protected void showLoadingScreen(View view) { + try { + if (mLayout == null) { + setContentView(view); + } else if (view.getParent() == null) { + mLayout.addView(view); + } + } catch (IllegalStateException e) { + // The loading screen can be attempted to be applied twice if app + // is tabbed in/out, quickly. + // (Gives error "The specified child already has a parent. + // You must call removeView() on the child's parent first.") + } + } + + protected void setBackgroundColor(View view) { + /* + * Set the presplash loading screen background color + * https://developer.android.com/reference/android/graphics/Color.html + * Parse the color string, and return the corresponding color-int. + * If the string cannot be parsed, throws an IllegalArgumentException exception. + * Supported formats are: #RRGGBB #AARRGGBB or one of the following names: + * 'red', 'blue', 'green', 'black', 'white', 'gray', 'cyan', 'magenta', 'yellow', + * 'lightgray', 'darkgray', 'grey', 'lightgrey', 'darkgrey', 'aqua', 'fuchsia', + * 'lime', 'maroon', 'navy', 'olive', 'purple', 'silver', 'teal'. + */ + String backgroundColor = resourceManager.getString("presplash_color"); + if (backgroundColor != null) { + try { + view.setBackgroundColor(Color.parseColor(backgroundColor)); + } catch (IllegalArgumentException e) {} + } + } + + protected View getLoadingScreen() { + // If we have an mLottieView or mImageView already, then do + // nothing because it will have already been made the content + // view or added to the layout. + if (mLottieView != null || mImageView != null) { + // we already have a splash screen + return mLottieView != null ? mLottieView : mImageView; + } + + // first try to load the lottie one + try { + mLottieView = getLayoutInflater().inflate( + this.resourceManager.getIdentifier("lottie", "layout"), + mLayout, + false + ); + try { + if (mLayout == null) { + setContentView(mLottieView); + } else if (PythonActivity.mLottieView.getParent() == null) { + mLayout.addView(mLottieView); + } + } catch (IllegalStateException e) { + // The loading screen can be attempted to be applied twice if app + // is tabbed in/out, quickly. + // (Gives error "The specified child already has a parent. + // You must call removeView() on the child's parent first.") + } + setBackgroundColor(mLottieView); + return mLottieView; + } + catch (NotFoundException e) { + Log.v("SDL", "couldn't find lottie layout or animation, trying static splash"); + } + + // no lottie asset, try to load the static image then + int presplashId = this.resourceManager.getIdentifier("presplash", "drawable"); + InputStream is = this.getResources().openRawResource(presplashId); + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeStream(is); + } finally { + try { + is.close(); + } catch (IOException e) {}; + } + + mImageView = new ImageView(this); + mImageView.setImageBitmap(bitmap); + setBackgroundColor(mImageView); + + mImageView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); + return mImageView; + } + + @Override + protected void onPause() { + if (this.mWakeLock != null && mWakeLock.isHeld()) { + this.mWakeLock.release(); + } + + Log.v(TAG, "onPause()"); + try { + super.onPause(); + } catch (UnsatisfiedLinkError e) { + // Catch pause while still in loading screen failing to + // call native function (since it's not yet loaded) + } + } + + @Override + protected void onResume() { + if (this.mWakeLock != null) { + this.mWakeLock.acquire(); + } + Log.v(TAG, "onResume()"); + try { + super.onResume(); + } catch (UnsatisfiedLinkError e) { + // Catch resume while still in loading screen failing to + // call native function (since it's not yet loaded) + } + considerLoadingScreenRemoval(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + try { + super.onWindowFocusChanged(hasFocus); + } catch (UnsatisfiedLinkError e) { + // Catch window focus while still in loading screen failing to + // call native function (since it's not yet loaded) + } + considerLoadingScreenRemoval(); + } + + /** + * Used by android.permissions p4a module to register a call back after + * requesting runtime permissions + **/ + public interface PermissionsCallback { + void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); + } + + private PermissionsCallback permissionCallback; + private boolean havePermissionsCallback = false; + + public void addPermissionsCallback(PermissionsCallback callback) { + permissionCallback = callback; + havePermissionsCallback = true; + Log.v(TAG, "addPermissionsCallback(): Added callback for onRequestPermissionsResult"); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + Log.v(TAG, "onRequestPermissionsResult()"); + if (havePermissionsCallback) { + Log.v(TAG, "onRequestPermissionsResult passed to callback"); + permissionCallback.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + /** + * Used by android.permissions p4a module to check a permission + **/ + public boolean checkCurrentPermission(String permission) { + if (android.os.Build.VERSION.SDK_INT < 23) + return true; + + try { + java.lang.reflect.Method methodCheckPermission = + Activity.class.getMethod("checkSelfPermission", String.class); + Object resultObj = methodCheckPermission.invoke(this, permission); + int result = Integer.parseInt(resultObj.toString()); + if (result == PackageManager.PERMISSION_GRANTED) + return true; + } catch (IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + } + return false; + } + + /** + * Used by android.permissions p4a module to request runtime permissions + **/ + public void requestPermissionsWithRequestCode(String[] permissions, int requestCode) { + if (android.os.Build.VERSION.SDK_INT < 23) + return; + try { + java.lang.reflect.Method methodRequestPermission = + Activity.class.getMethod("requestPermissions", + String[].class, int.class); + methodRequestPermission.invoke(this, permissions, requestCode); + } catch (IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + } + } + + public void requestPermissions(String[] permissions) { + requestPermissionsWithRequestCode(permissions, 1); + } + + public static void changeKeyboard(int inputType) { + if (SDLActivity.keyboardInputType != inputType){ + SDLActivity.keyboardInputType = inputType; + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.restartInput(mTextEdit); + } + } +} diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/Project.java b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/Project.java new file mode 100644 index 0000000..9177b43 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/Project.java @@ -0,0 +1,99 @@ +package org.kivy.android.launcher; + +import java.io.UnsupportedEncodingException; +import java.io.File; +import java.io.FileInputStream; +import java.util.Properties; + +import android.util.Log; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + + +/** + * This represents a project we've scanned for. + */ +public class Project { + + public String dir = null; + String title = null; + String author = null; + Bitmap icon = null; + public boolean landscape = false; + + static String decode(String s) { + try { + return new String(s.getBytes("ISO-8859-1"), "UTF-8"); + } catch (UnsupportedEncodingException e) { + return s; + } + } + + /** + * Scans directory for a android.txt file. If it finds one, + * and it looks valid enough, then it creates a new Project, + * and returns that. Otherwise, returns null. + */ + public static Project scanDirectory(File dir) { + + // We might have a link file. + if (dir.getAbsolutePath().endsWith(".link")) { + try { + + // Scan the android.txt file. + File propfile = new File(dir, "android.txt"); + FileInputStream in = new FileInputStream(propfile); + Properties p = new Properties(); + p.load(in); + in.close(); + + String directory = p.getProperty("directory", null); + + if (directory == null) { + return null; + } + + dir = new File(directory); + + } catch (Exception e) { + Log.i("Project", "Couldn't open link file " + dir, e); + } + } + + // Make sure we're dealing with a directory. + if (! dir.isDirectory()) { + return null; + } + + try { + + // Scan the android.txt file. + File propfile = new File(dir, "android.txt"); + FileInputStream in = new FileInputStream(propfile); + Properties p = new Properties(); + p.load(in); + in.close(); + + // Get the various properties. + String title = decode(p.getProperty("title", "Untitled")); + String author = decode(p.getProperty("author", "")); + boolean landscape = p.getProperty("orientation", "portrait").equals("landscape"); + + // Create the project object. + Project rv = new Project(); + rv.title = title; + rv.author = author; + rv.icon = BitmapFactory.decodeFile(new File(dir, "icon.png").getAbsolutePath()); + rv.landscape = landscape; + rv.dir = dir.getAbsolutePath(); + + return rv; + + } catch (Exception e) { + Log.i("Project", "Couldn't open android.txt", e); + } + + return null; + + } +} diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/ProjectAdapter.java b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/ProjectAdapter.java new file mode 100644 index 0000000..457f83f --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/ProjectAdapter.java @@ -0,0 +1,35 @@ +package org.kivy.android.launcher; + +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import android.widget.ImageView; + +import org.renpy.android.ResourceManager; + +public class ProjectAdapter extends ArrayAdapter { + + private ResourceManager resourceManager; + + public ProjectAdapter(Activity context) { + super(context, 0); + resourceManager = new ResourceManager(context); + } + + public View getView(int position, View convertView, ViewGroup parent) { + Project p = getItem(position); + + View v = resourceManager.inflateView("chooser_item"); + TextView title = (TextView) resourceManager.getViewById(v, "title"); + TextView author = (TextView) resourceManager.getViewById(v, "author"); + ImageView icon = (ImageView) resourceManager.getViewById(v, "icon"); + + title.setText(p.title); + author.setText(p.author); + icon.setImageBitmap(p.icon); + + return v; + } +} diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/ProjectChooser.java b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/ProjectChooser.java new file mode 100644 index 0000000..486f88b --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/launcher/ProjectChooser.java @@ -0,0 +1,90 @@ +package org.kivy.android.launcher; + +import android.app.Activity; + +import android.content.Intent; +import android.view.View; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.AdapterView; +import android.os.Environment; + +import java.io.File; +import java.util.Arrays; +import android.net.Uri; + +import org.renpy.android.ResourceManager; + +public class ProjectChooser extends Activity implements AdapterView.OnItemClickListener { + + ResourceManager resourceManager; + + String urlScheme; + + @Override + public void onStart() + { + super.onStart(); + + resourceManager = new ResourceManager(this); + + urlScheme = resourceManager.getString("urlScheme"); + + // Set the window title. + setTitle(resourceManager.getString("appName")); + + // Scan the sdcard for files, and sort them. + File dir = new File(Environment.getExternalStorageDirectory(), urlScheme); + + File entries[] = dir.listFiles(); + + if (entries == null) { + entries = new File[0]; + } + + Arrays.sort(entries); + + // Create a ProjectAdapter and fill it with projects. + ProjectAdapter projectAdapter = new ProjectAdapter(this); + + // Populate it with the properties files. + for (File d : entries) { + Project p = Project.scanDirectory(d); + if (p != null) { + projectAdapter.add(p); + } + } + + if (projectAdapter.getCount() != 0) { + + View v = resourceManager.inflateView("project_chooser"); + ListView l = (ListView) resourceManager.getViewById(v, "projectList"); + + l.setAdapter(projectAdapter); + l.setOnItemClickListener(this); + + setContentView(v); + + } else { + + View v = resourceManager.inflateView("project_empty"); + TextView emptyText = (TextView) resourceManager.getViewById(v, "emptyText"); + + emptyText.setText("No projects are available to launch. Please place a project into " + dir + " and restart this application. Press the back button to exit."); + + setContentView(v); + } + } + + public void onItemClick(AdapterView parent, View view, int position, long id) { + Project p = (Project) parent.getItemAtPosition(position); + + Intent intent = new Intent( + "org.kivy.LAUNCH", + Uri.fromParts(urlScheme, p.dir, "")); + + intent.setClassName(getPackageName(), "org.kivy.android.PythonActivity"); + this.startActivity(intent); + this.finish(); + } +} diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/jniLibs/.gitkeep b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/jniLibs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/libs/.gitkeep b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/libs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-hdpi/ic_launcher.png b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..d50bdaa Binary files /dev/null and b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-mdpi/ic_launcher.png b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..0a299eb Binary files /dev/null and b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-xhdpi/ic_launcher.png b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..a336ad5 Binary files /dev/null and b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-xxhdpi/ic_launcher.png b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d423dac Binary files /dev/null and b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable/.gitkeep b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/drawable/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/chooser_item.xml b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/chooser_item.xml new file mode 100644 index 0000000..1823b13 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/chooser_item.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/main.xml b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/main.xml new file mode 100644 index 0000000..123c4b6 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/main.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/project_chooser.xml b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/project_chooser.xml new file mode 100644 index 0000000..23828e6 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/project_chooser.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/project_empty.xml b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/project_empty.xml new file mode 100644 index 0000000..ee54814 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/layout/project_empty.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/mipmap-anydpi-v26/.gitkeep b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/mipmap-anydpi-v26/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/mipmap/.gitkeep b/p4a/pythonforandroid/bootstraps/sdl2/build/src/main/res/mipmap/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/src/patches/SDLActivity.java.patch b/p4a/pythonforandroid/bootstraps/sdl2/build/src/patches/SDLActivity.java.patch new file mode 100644 index 0000000..d061be8 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/src/patches/SDLActivity.java.patch @@ -0,0 +1,75 @@ +--- a/src/main/java/org/libsdl/app/SDLActivity.java ++++ b/src/main/java/org/libsdl/app/SDLActivity.java +@@ -222,6 +222,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh + // This is what SDL runs in. It invokes SDL_main(), eventually + protected static Thread mSDLThread; + ++ public static int keyboardInputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; ++ + protected static SDLGenericMotionListener_API12 getMotionListener() { + if (mMotionListener == null) { + if (Build.VERSION.SDK_INT >= 26) { +@@ -324,6 +326,15 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh + Log.v(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + ++ SDLActivity.initialize(); ++ // So we can call stuff from static callbacks ++ mSingleton = this; ++ } ++ ++ // We don't do this in onCreate because we unpack and load the app data on a thread ++ // and we can't run setup tasks until that thread completes. ++ protected void finishLoad() { ++ + try { + Thread.currentThread().setName("SDLActivity"); + } catch (Exception e) { +@@ -835,7 +846,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh + Handler commandHandler = new SDLCommandHandler(); + + // Send a message from the SDLMain thread +- boolean sendCommand(int command, Object data) { ++ protected boolean sendCommand(int command, Object data) { + Message msg = commandHandler.obtainMessage(); + msg.arg1 = command; + msg.obj = data; +@@ -1384,6 +1395,20 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh + return SDLActivity.mSurface.getNativeSurface(); + } + ++ /** ++ * Calls turnActive() on singleton to keep loading screen active ++ */ ++ public static void triggerAppConfirmedActive() { ++ mSingleton.appConfirmedActive(); ++ } ++ ++ /** ++ * Trick needed for loading screen, overridden by PythonActivity ++ * to keep loading screen active ++ */ ++ public void appConfirmedActive() { ++ } ++ + // Input + + /** +@@ -1878,6 +1903,7 @@ class SDLMain implements Runnable { + + Log.v("SDL", "Running main function " + function + " from library " + library); + ++ SDLActivity.mSingleton.appConfirmedActive(); + SDLActivity.nativeRunMain(library, function, arguments); + + Log.v("SDL", "Finished main function"); +@@ -1935,8 +1961,7 @@ class DummyEdit extends View implements View.OnKeyListener { + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + ic = new SDLInputConnection(this, true); + +- outAttrs.inputType = InputType.TYPE_CLASS_TEXT | +- InputType.TYPE_TEXT_FLAG_MULTI_LINE; ++ outAttrs.inputType = SDLActivity.keyboardInputType | InputType.TYPE_TEXT_FLAG_MULTI_LINE; + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | + EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; + diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml b/p4a/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml new file mode 100644 index 0000000..b5ddde3 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml @@ -0,0 +1,146 @@ + + + + + = 9 %} + android:xlargeScreens="true" + {% endif %} + /> + + + + + + + + + + {% for perm in args.permissions %} + {% if '.' in perm %} + + {% else %} + + {% endif %} + {% endfor %} + + {% if args.wakelock %} + + {% endif %} + + {% if args.billing_pubkey %} + + {% endif %} + + {{ args.extra_manifest_xml }} + + + + + {% for l in args.android_used_libs %} + + {% endfor %} + + {% for m in args.meta_data %} + {% endfor %} + + + + + {% if args.launcher %} + + + + + + {% else %} + + + + + {% endif %} + + {%- if args.intent_filters -%} + {{- args.intent_filters -}} + {%- endif -%} + + + {% if args.launcher %} + + + + + + + + + {% endif %} + + {% if service or args.launcher %} + + {% endif %} + {% for name in service_names %} + + {% endfor %} + {% for name in native_services %} + + {% endfor %} + + {% if args.billing_pubkey %} + + + + + + + + + {% endif %} + {% for a in args.add_activity %} + + {% endfor %} + + + diff --git a/p4a/pythonforandroid/bootstraps/sdl2/build/templates/strings.tmpl.xml b/p4a/pythonforandroid/bootstraps/sdl2/build/templates/strings.tmpl.xml new file mode 100644 index 0000000..c802551 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/sdl2/build/templates/strings.tmpl.xml @@ -0,0 +1,7 @@ + + + {{ args.name }} + {{ private_version }} + {{ args.presplash_color }} + {{ url_scheme }} + diff --git a/p4a/pythonforandroid/bootstraps/service_library/__init__.py b/p4a/pythonforandroid/bootstraps/service_library/__init__.py new file mode 100644 index 0000000..0b41be8 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/service_library/__init__.py @@ -0,0 +1,9 @@ +from pythonforandroid.bootstraps.service_only import ServiceOnlyBootstrap + + +class ServiceLibraryBootstrap(ServiceOnlyBootstrap): + + name = 'service_library' + + +bootstrap = ServiceLibraryBootstrap() diff --git a/p4a/pythonforandroid/bootstraps/service_library/build/jni/application/src/bootstrap_name.h b/p4a/pythonforandroid/bootstraps/service_library/build/jni/application/src/bootstrap_name.h new file mode 100644 index 0000000..01fd122 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/service_library/build/jni/application/src/bootstrap_name.h @@ -0,0 +1,6 @@ + +#define BOOTSTRAP_NAME_LIBRARY +#define BOOTSTRAP_USES_NO_SDL_HEADERS + +const char bootstrap_name[] = "service_library"; + diff --git a/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java b/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java new file mode 100644 index 0000000..58a1c5e --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java @@ -0,0 +1,19 @@ +package org.kivy.android; + +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.Context; + +public class GenericBroadcastReceiver extends BroadcastReceiver { + + GenericBroadcastReceiverCallback listener; + + public GenericBroadcastReceiver(GenericBroadcastReceiverCallback listener) { + super(); + this.listener = listener; + } + + public void onReceive(Context context, Intent intent) { + this.listener.onReceive(context, intent); + } +} diff --git a/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java b/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java new file mode 100644 index 0000000..1a87c98 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java @@ -0,0 +1,8 @@ +package org.kivy.android; + +import android.content.Intent; +import android.content.Context; + +public interface GenericBroadcastReceiverCallback { + void onReceive(Context context, Intent intent); +}; diff --git a/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/PythonActivity.java b/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/PythonActivity.java new file mode 100644 index 0000000..7be751d --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/PythonActivity.java @@ -0,0 +1,9 @@ +package org.kivy.android; + +import android.app.Activity; + +// Required by PythonService class +public class PythonActivity extends Activity { + public static PythonActivity mActivity = null; +} + diff --git a/p4a/pythonforandroid/bootstraps/service_library/build/src/main/res/mipmap/.gitkeep b/p4a/pythonforandroid/bootstraps/service_library/build/src/main/res/mipmap/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/p4a/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml b/p4a/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml new file mode 100644 index 0000000..f667651 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml @@ -0,0 +1,18 @@ + + + + + + + + {% for name in service_names %} + + {% endfor %} + + + diff --git a/p4a/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java b/p4a/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java new file mode 100644 index 0000000..f1eaf07 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java @@ -0,0 +1,82 @@ +package {{ args.package }}; + +import java.io.File; + +import android.os.Build; +import android.content.Intent; +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; + +import org.kivy.android.PythonService; +import org.kivy.android.PythonUtil; + +public class Service{{ name|capitalize }} extends PythonService { + + private static final String TAG = "PythonService"; + + {% if sticky %} + @Override + public int startType() { + return START_STICKY; + } + {% endif %} + + @Override + protected int getServiceId() { + return {{ service_id }}; + } + + public static void prepare(Context ctx) { + String appRoot = PythonUtil.getAppRoot(ctx); + Log.v(TAG, "Ready to unpack"); + File app_root_file = new File(appRoot); + PythonUtil.unpackAsset(ctx, "private", app_root_file, true); + PythonUtil.unpackPyBundle(ctx, ctx.getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false); + } + + public static void start(Context ctx, String pythonServiceArgument) { + Intent intent = getDefaultIntent(ctx, pythonServiceArgument); + + //foreground: {{foreground}} + {% if foreground %} + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(intent); + } else { + ctx.startService(intent); + } + {% else %} + ctx.startService(intent); + {% endif %} + } + + static public Intent getDefaultIntent(Context ctx, String pythonServiceArgument) { + String appRoot = PythonUtil.getAppRoot(ctx); + Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); + intent.putExtra("androidPrivate", appRoot); + intent.putExtra("androidArgument", appRoot); + intent.putExtra("serviceEntrypoint", "{{ entrypoint }}"); + intent.putExtra("serviceTitle", "{{ name|capitalize }}"); + intent.putExtra("serviceDescription", ""); + intent.putExtra("pythonName", "{{ name }}"); + intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}"); + intent.putExtra("pythonHome", appRoot); + intent.putExtra("androidUnpack", appRoot); + intent.putExtra("pythonPath", appRoot + ":" + appRoot + "/lib"); + intent.putExtra("pythonServiceArgument", pythonServiceArgument); + return intent; + } + + @Override + protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) { + return Service{{ name|capitalize }}.getDefaultIntent(ctx, pythonServiceArgument); + } + + + + static public void stop(Context ctx) { + Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); + ctx.stopService(intent); + } + +} diff --git a/p4a/pythonforandroid/bootstraps/service_library/build/templates/activity_service_control.xml b/p4a/pythonforandroid/bootstraps/service_library/build/templates/activity_service_control.xml new file mode 100644 index 0000000..8ab7f21 --- /dev/null +++ b/p4a/pythonforandroid/bootstraps/service_library/build/templates/activity_service_control.xml @@ -0,0 +1,87 @@ + + + + + + + + +