Upgrade p4a clean #21

Open
jessopb wants to merge 43 commits from upgrade_p4a_clean into master
766 changed files with 14809 additions and 150802 deletions

View file

@ -2,7 +2,7 @@ name: Publish Assets
on: on:
push: push:
branches: [master] branches: [master, upgrade_p4a_clean]
jobs: jobs:
build_arm64_aar: build_arm64_aar:
@ -16,20 +16,14 @@ jobs:
cp -r /root/.buildozer ~/.buildozer/ cp -r /root/.buildozer ~/.buildozer/
- name: setup - name: setup
run: | 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]+\.?)+') 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 "NEXUS_SIGNING_KEYRING_FILE=$GITHUB_WORKSPACE/signing2.pgp" >> $GITHUB_ENV
echo "BUILD_VERSION=${B_VERSION}" >> $GITHUB_ENV echo "BUILD_VERSION=${B_VERSION}" >> $GITHUB_ENV
export PATH=/usr/bin:$PATH 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/ wget -q 'https://dl.google.com/android/repository/android-ndk-r25b-linux.zip' -P ~/.buildozer/android/
tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/ unzip ~/.buildozer/android/android-ndk-r25b-linux.zip -d ~/.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
mv buildozer.spec.arm64.ci buildozer.spec mv buildozer.spec.arm64.ci buildozer.spec
chmod u+x ./build-release.sh chmod u+x ./build-release.sh
- name: build release - name: build release
@ -77,6 +71,7 @@ jobs:
echo "NEXUS_SIGNING_KEYRING_FILE=$GITHUB_WORKSPACE/signing2.pgp" >> $GITHUB_ENV echo "NEXUS_SIGNING_KEYRING_FILE=$GITHUB_WORKSPACE/signing2.pgp" >> $GITHUB_ENV
export PATH=/usr/bin:$PATH 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/ 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/ 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 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 ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9

1
.gitignore vendored
View file

@ -19,3 +19,4 @@ p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
p4a/*.apk p4a/*.apk
p4a/*.aar p4a/*.aar
venv

View file

@ -39,8 +39,9 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2021.5.30, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13.3, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==21.0.0, defusedxml, netifaces, aioupnp==0.0.17, asn1crypto, mock, cryptography, aiohttp==3.6.0, multidict==4.5.2, idna, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, prometheus_client==0.8.0, "git+https://github.com/lbryio/lbry-sdk@v0.112.0#egg=lbry" requirements = 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 # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy # requirements.source.kivy = ../../kivy
@ -101,13 +102,15 @@ android.minapi = 21
android.sdk = 23 android.sdk = 23
# (str) Android NDK version to use # (str) Android NDK version to use
#android.ndk = 13b android.ndk = 25b
# (bool) Use --private data storage (True) or --dir public storage (False) # (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True #android.private_storage = True
# (str) Android NDK directory (if empty, it will be automatically downloaded.) # (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.) # (str) Android SDK directory (if empty, it will be automatically downloaded.)
#android.sdk_path = ~/.buildozer/android #android.sdk_path = ~/.buildozer/android

View file

@ -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/ && \ 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 && \ 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 && \ 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 && \ 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 && \ 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 && \ 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 .. RUN cd buildozer && python setup.py install && cd ..
CMD ["/bin/bash"] 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/[]

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,141 @@
/**
* Copyright 2012 Kamran Zafar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.kamranzafar.jtar;
/**
* @author Kamran Zafar
*
*/
public class Octal {
/**
* Parse an octal string from a header buffer. This is used for the file
* permission mode value.
*
* @param header
* The header buffer from which to parse.
* @param offset
* The offset into the buffer from which to parse.
* @param length
* The number of header bytes to parse.
*
* @return The long value of the octal string.
*/
public static long parseOctal(byte[] header, int offset, int length) {
long result = 0;
boolean stillPadding = true;
int end = offset + length;
for (int i = offset; i < end; ++i) {
if (header[i] == 0)
break;
if (header[i] == (byte) ' ' || header[i] == '0') {
if (stillPadding)
continue;
if (header[i] == (byte) ' ')
break;
}
stillPadding = false;
result = ( result << 3 ) + ( header[i] - '0' );
}
return result;
}
/**
* Parse an octal integer from a header buffer.
*
* @param value
* @param buf
* The header buffer from which to parse.
* @param offset
* The offset into the buffer from which to parse.
* @param length
* The number of header bytes to parse.
*
* @return The integer value of the octal bytes.
*/
public static int getOctalBytes(long value, byte[] buf, int offset, int length) {
int idx = length - 1;
buf[offset + idx] = 0;
--idx;
buf[offset + idx] = (byte) ' ';
--idx;
if (value == 0) {
buf[offset + idx] = (byte) '0';
--idx;
} else {
for (long val = value; idx >= 0 && val > 0; --idx) {
buf[offset + idx] = (byte) ( (byte) '0' + (byte) ( val & 7 ) );
val = val >> 3;
}
}
for (; idx >= 0; --idx) {
buf[offset + idx] = (byte) ' ';
}
return offset + length;
}
/**
* Parse the checksum octal integer from a header buffer.
*
* @param value
* @param buf
* The header buffer from which to parse.
* @param offset
* The offset into the buffer from which to parse.
* @param length
* The number of header bytes to parse.
* @return The integer value of the entry's checksum.
*/
public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) {
getOctalBytes( value, buf, offset, length );
buf[offset + length - 1] = (byte) ' ';
buf[offset + length - 2] = 0;
return offset + length;
}
/**
* Parse an octal long integer from a header buffer.
*
* @param value
* @param buf
* The header buffer from which to parse.
* @param offset
* The offset into the buffer from which to parse.
* @param length
* The number of header bytes to parse.
*
* @return The long value of the octal bytes.
*/
public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) {
byte[] temp = new byte[length + 1];
getOctalBytes( value, temp, 0, length + 1 );
System.arraycopy( temp, 0, buf, offset, length );
return offset + length;
}
}

View file

@ -0,0 +1,28 @@
/**
* Copyright 2012 Kamran Zafar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.kamranzafar.jtar;
/**
* @author Kamran Zafar
*
*/
public class TarConstants {
public static final int EOF_BLOCK = 1024;
public static final int DATA_BLOCK = 512;
public static final int HEADER_BLOCK = 512;
}

View file

@ -0,0 +1,284 @@
/**
* Copyright 2012 Kamran Zafar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.kamranzafar.jtar;
import java.io.File;
import java.util.Date;
/**
* @author Kamran Zafar
*
*/
public class TarEntry {
protected File file;
protected TarHeader header;
private TarEntry() {
this.file = null;
header = new TarHeader();
}
public TarEntry(File file, String entryName) {
this();
this.file = file;
this.extractTarHeader(entryName);
}
public TarEntry(byte[] headerBuf) {
this();
this.parseTarHeader(headerBuf);
}
/**
* Constructor to create an entry from an existing TarHeader object.
*
* This method is useful to add new entries programmatically (e.g. for
* adding files or directories that do not exist in the file system).
*
* @param header
*
*/
public TarEntry(TarHeader header) {
this.file = null;
this.header = header;
}
public boolean equals(TarEntry it) {
return header.name.toString().equals(it.header.name.toString());
}
public boolean isDescendent(TarEntry desc) {
return desc.header.name.toString().startsWith(header.name.toString());
}
public TarHeader getHeader() {
return header;
}
public String getName() {
String name = header.name.toString();
if (header.namePrefix != null && !header.namePrefix.toString().equals("")) {
name = header.namePrefix.toString() + "/" + name;
}
return name;
}
public void setName(String name) {
header.name = new StringBuffer(name);
}
public int getUserId() {
return header.userId;
}
public void setUserId(int userId) {
header.userId = userId;
}
public int getGroupId() {
return header.groupId;
}
public void setGroupId(int groupId) {
header.groupId = groupId;
}
public String getUserName() {
return header.userName.toString();
}
public void setUserName(String userName) {
header.userName = new StringBuffer(userName);
}
public String getGroupName() {
return header.groupName.toString();
}
public void setGroupName(String groupName) {
header.groupName = new StringBuffer(groupName);
}
public void setIds(int userId, int groupId) {
this.setUserId(userId);
this.setGroupId(groupId);
}
public void setModTime(long time) {
header.modTime = time / 1000;
}
public void setModTime(Date time) {
header.modTime = time.getTime() / 1000;
}
public Date getModTime() {
return new Date(header.modTime * 1000);
}
public File getFile() {
return this.file;
}
public long getSize() {
return header.size;
}
public void setSize(long size) {
header.size = size;
}
/**
* Checks if the org.kamrazafar.jtar entry is a directory
*
* @return
*/
public boolean isDirectory() {
if (this.file != null)
return this.file.isDirectory();
if (header != null) {
if (header.linkFlag == TarHeader.LF_DIR)
return true;
if (header.name.toString().endsWith("/"))
return true;
}
return false;
}
/**
* Extract header from File
*
* @param entryName
*/
public void extractTarHeader(String entryName) {
header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory());
}
/**
* Calculate checksum
*
* @param buf
* @return
*/
public long computeCheckSum(byte[] buf) {
long sum = 0;
for (int i = 0; i < buf.length; ++i) {
sum += 255 & buf[i];
}
return sum;
}
/**
* Writes the header to the byte buffer
*
* @param outbuf
*/
public void writeEntryHeader(byte[] outbuf) {
int offset = 0;
offset = TarHeader.getNameBytes(header.name, outbuf, offset, TarHeader.NAMELEN);
offset = Octal.getOctalBytes(header.mode, outbuf, offset, TarHeader.MODELEN);
offset = Octal.getOctalBytes(header.userId, outbuf, offset, TarHeader.UIDLEN);
offset = Octal.getOctalBytes(header.groupId, outbuf, offset, TarHeader.GIDLEN);
long size = header.size;
offset = Octal.getLongOctalBytes(size, outbuf, offset, TarHeader.SIZELEN);
offset = Octal.getLongOctalBytes(header.modTime, outbuf, offset, TarHeader.MODTIMELEN);
int csOffset = offset;
for (int c = 0; c < TarHeader.CHKSUMLEN; ++c)
outbuf[offset++] = (byte) ' ';
outbuf[offset++] = header.linkFlag;
offset = TarHeader.getNameBytes(header.linkName, outbuf, offset, TarHeader.NAMELEN);
offset = TarHeader.getNameBytes(header.magic, outbuf, offset, TarHeader.USTAR_MAGICLEN);
offset = TarHeader.getNameBytes(header.userName, outbuf, offset, TarHeader.USTAR_USER_NAMELEN);
offset = TarHeader.getNameBytes(header.groupName, outbuf, offset, TarHeader.USTAR_GROUP_NAMELEN);
offset = Octal.getOctalBytes(header.devMajor, outbuf, offset, TarHeader.USTAR_DEVLEN);
offset = Octal.getOctalBytes(header.devMinor, outbuf, offset, TarHeader.USTAR_DEVLEN);
offset = TarHeader.getNameBytes(header.namePrefix, outbuf, offset, TarHeader.USTAR_FILENAME_PREFIX);
for (; offset < outbuf.length;)
outbuf[offset++] = 0;
long checkSum = this.computeCheckSum(outbuf);
Octal.getCheckSumOctalBytes(checkSum, outbuf, csOffset, TarHeader.CHKSUMLEN);
}
/**
* Parses the tar header to the byte buffer
*
* @param header
* @param bh
*/
public void parseTarHeader(byte[] bh) {
int offset = 0;
header.name = TarHeader.parseName(bh, offset, TarHeader.NAMELEN);
offset += TarHeader.NAMELEN;
header.mode = (int) Octal.parseOctal(bh, offset, TarHeader.MODELEN);
offset += TarHeader.MODELEN;
header.userId = (int) Octal.parseOctal(bh, offset, TarHeader.UIDLEN);
offset += TarHeader.UIDLEN;
header.groupId = (int) Octal.parseOctal(bh, offset, TarHeader.GIDLEN);
offset += TarHeader.GIDLEN;
header.size = Octal.parseOctal(bh, offset, TarHeader.SIZELEN);
offset += TarHeader.SIZELEN;
header.modTime = Octal.parseOctal(bh, offset, TarHeader.MODTIMELEN);
offset += TarHeader.MODTIMELEN;
header.checkSum = (int) Octal.parseOctal(bh, offset, TarHeader.CHKSUMLEN);
offset += TarHeader.CHKSUMLEN;
header.linkFlag = bh[offset++];
header.linkName = TarHeader.parseName(bh, offset, TarHeader.NAMELEN);
offset += TarHeader.NAMELEN;
header.magic = TarHeader.parseName(bh, offset, TarHeader.USTAR_MAGICLEN);
offset += TarHeader.USTAR_MAGICLEN;
header.userName = TarHeader.parseName(bh, offset, TarHeader.USTAR_USER_NAMELEN);
offset += TarHeader.USTAR_USER_NAMELEN;
header.groupName = TarHeader.parseName(bh, offset, TarHeader.USTAR_GROUP_NAMELEN);
offset += TarHeader.USTAR_GROUP_NAMELEN;
header.devMajor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN);
offset += TarHeader.USTAR_DEVLEN;
header.devMinor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN);
offset += TarHeader.USTAR_DEVLEN;
header.namePrefix = TarHeader.parseName(bh, offset, TarHeader.USTAR_FILENAME_PREFIX);
}
}

View file

@ -0,0 +1,243 @@
/**
* Copyright 2012 Kamran Zafar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.kamranzafar.jtar;
import java.io.File;
/**
* Header
*
* <pre>
* Offset Size Field
* 0 100 File name
* 100 8 File mode
* 108 8 Owner's numeric user ID
* 116 8 Group's numeric user ID
* 124 12 File size in bytes
* 136 12 Last modification time in numeric Unix time format
* 148 8 Checksum for header block
* 156 1 Link indicator (file type)
* 157 100 Name of linked file
* </pre>
*
*
* File Types
*
* <pre>
* Value Meaning
* '0' Normal file
* (ASCII NUL) Normal file (now obsolete)
* '1' Hard link
* '2' Symbolic link
* '3' Character special
* '4' Block special
* '5' Directory
* '6' FIFO
* '7' Contigous
* </pre>
*
*
*
* Ustar header
*
* <pre>
* Offset Size Field
* 257 6 UStar indicator "ustar"
* 263 2 UStar version "00"
* 265 32 Owner user name
* 297 32 Owner group name
* 329 8 Device major number
* 337 8 Device minor number
* 345 155 Filename prefix
* </pre>
*/
public class TarHeader {
/*
* Header
*/
public static final int NAMELEN = 100;
public static final int MODELEN = 8;
public static final int UIDLEN = 8;
public static final int GIDLEN = 8;
public static final int SIZELEN = 12;
public static final int MODTIMELEN = 12;
public static final int CHKSUMLEN = 8;
public static final byte LF_OLDNORM = 0;
/*
* File Types
*/
public static final byte LF_NORMAL = (byte) '0';
public static final byte LF_LINK = (byte) '1';
public static final byte LF_SYMLINK = (byte) '2';
public static final byte LF_CHR = (byte) '3';
public static final byte LF_BLK = (byte) '4';
public static final byte LF_DIR = (byte) '5';
public static final byte LF_FIFO = (byte) '6';
public static final byte LF_CONTIG = (byte) '7';
/*
* Ustar header
*/
public static final String USTAR_MAGIC = "ustar"; // POSIX
public static final int USTAR_MAGICLEN = 8;
public static final int USTAR_USER_NAMELEN = 32;
public static final int USTAR_GROUP_NAMELEN = 32;
public static final int USTAR_DEVLEN = 8;
public static final int USTAR_FILENAME_PREFIX = 155;
// Header values
public StringBuffer name;
public int mode;
public int userId;
public int groupId;
public long size;
public long modTime;
public int checkSum;
public byte linkFlag;
public StringBuffer linkName;
public StringBuffer magic; // ustar indicator and version
public StringBuffer userName;
public StringBuffer groupName;
public int devMajor;
public int devMinor;
public StringBuffer namePrefix;
public TarHeader() {
this.magic = new StringBuffer(TarHeader.USTAR_MAGIC);
this.name = new StringBuffer();
this.linkName = new StringBuffer();
String user = System.getProperty("user.name", "");
if (user.length() > 31)
user = user.substring(0, 31);
this.userId = 0;
this.groupId = 0;
this.userName = new StringBuffer(user);
this.groupName = new StringBuffer("");
this.namePrefix = new StringBuffer();
}
/**
* Parse an entry name from a header buffer.
*
* @param name
* @param header
* The header buffer from which to parse.
* @param offset
* The offset into the buffer from which to parse.
* @param length
* The number of header bytes to parse.
* @return The header's entry name.
*/
public static StringBuffer parseName(byte[] header, int offset, int length) {
StringBuffer result = new StringBuffer(length);
int end = offset + length;
for (int i = offset; i < end; ++i) {
if (header[i] == 0)
break;
result.append((char) header[i]);
}
return result;
}
/**
* Determine the number of bytes in an entry name.
*
* @param name
* @param header
* The header buffer from which to parse.
* @param offset
* The offset into the buffer from which to parse.
* @param length
* The number of header bytes to parse.
* @return The number of bytes in a header's entry name.
*/
public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) {
int i;
for (i = 0; i < length && i < name.length(); ++i) {
buf[offset + i] = (byte) name.charAt(i);
}
for (; i < length; ++i) {
buf[offset + i] = 0;
}
return offset + length;
}
/**
* Creates a new header for a file/directory entry.
*
*
* @param name
* File name
* @param size
* File size in bytes
* @param modTime
* Last modification time in numeric Unix time format
* @param dir
* Is directory
*
* @return
*/
public static TarHeader createHeader(String entryName, long size, long modTime, boolean dir) {
String name = entryName;
name = TarUtils.trim(name.replace(File.separatorChar, '/'), '/');
TarHeader header = new TarHeader();
header.linkName = new StringBuffer("");
if (name.length() > 100) {
header.namePrefix = new StringBuffer(name.substring(0, name.lastIndexOf('/')));
header.name = new StringBuffer(name.substring(name.lastIndexOf('/') + 1));
} else {
header.name = new StringBuffer(name);
}
if (dir) {
header.mode = 040755;
header.linkFlag = TarHeader.LF_DIR;
if (header.name.charAt(header.name.length() - 1) != '/') {
header.name.append("/");
}
header.size = 0;
} else {
header.mode = 0100644;
header.linkFlag = TarHeader.LF_NORMAL;
header.size = size;
}
header.modTime = modTime;
header.checkSum = 0;
header.devMajor = 0;
header.devMinor = 0;
return header;
}
}

View file

@ -0,0 +1,249 @@
/**
* Copyright 2012 Kamran Zafar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.kamranzafar.jtar;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @author Kamran Zafar
*
*/
public class TarInputStream extends FilterInputStream {
private static final int SKIP_BUFFER_SIZE = 2048;
private TarEntry currentEntry;
private long currentFileSize;
private long bytesRead;
private boolean defaultSkip = false;
public TarInputStream(InputStream in) {
super(in);
currentFileSize = 0;
bytesRead = 0;
}
@Override
public boolean markSupported() {
return false;
}
/**
* Not supported
*
*/
@Override
public synchronized void mark(int readlimit) {
}
/**
* Not supported
*
*/
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
/**
* Read a byte
*
* @see java.io.FilterInputStream#read()
*/
@Override
public int read() throws IOException {
byte[] buf = new byte[1];
int res = this.read(buf, 0, 1);
if (res != -1) {
return 0xFF & buf[0];
}
return res;
}
/**
* Checks if the bytes being read exceed the entry size and adjusts the byte
* array length. Updates the byte counters
*
*
* @see java.io.FilterInputStream#read(byte[], int, int)
*/
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (currentEntry != null) {
if (currentFileSize == currentEntry.getSize()) {
return -1;
} else if ((currentEntry.getSize() - currentFileSize) < len) {
len = (int) (currentEntry.getSize() - currentFileSize);
}
}
int br = super.read(b, off, len);
if (br != -1) {
if (currentEntry != null) {
currentFileSize += br;
}
bytesRead += br;
}
return br;
}
/**
* Returns the next entry in the tar file
*
* @return TarEntry
* @throws IOException
*/
public TarEntry getNextEntry() throws IOException {
closeCurrentEntry();
byte[] header = new byte[TarConstants.HEADER_BLOCK];
byte[] theader = new byte[TarConstants.HEADER_BLOCK];
int tr = 0;
// Read full header
while (tr < TarConstants.HEADER_BLOCK) {
int res = read(theader, 0, TarConstants.HEADER_BLOCK - tr);
if (res < 0) {
break;
}
System.arraycopy(theader, 0, header, tr, res);
tr += res;
}
// Check if record is null
boolean eof = true;
for (byte b : header) {
if (b != 0) {
eof = false;
break;
}
}
if (!eof) {
currentEntry = new TarEntry(header);
}
return currentEntry;
}
/**
* Returns the current offset (in bytes) from the beginning of the stream.
* This can be used to find out at which point in a tar file an entry's content begins, for instance.
*/
public long getCurrentOffset() {
return bytesRead;
}
/**
* Closes the current tar entry
*
* @throws IOException
*/
protected void closeCurrentEntry() throws IOException {
if (currentEntry != null) {
if (currentEntry.getSize() > currentFileSize) {
// Not fully read, skip rest of the bytes
long bs = 0;
while (bs < currentEntry.getSize() - currentFileSize) {
long res = skip(currentEntry.getSize() - currentFileSize - bs);
if (res == 0 && currentEntry.getSize() - currentFileSize > 0) {
// I suspect file corruption
throw new IOException("Possible tar file corruption");
}
bs += res;
}
}
currentEntry = null;
currentFileSize = 0L;
skipPad();
}
}
/**
* Skips the pad at the end of each tar entry file content
*
* @throws IOException
*/
protected void skipPad() throws IOException {
if (bytesRead > 0) {
int extra = (int) (bytesRead % TarConstants.DATA_BLOCK);
if (extra > 0) {
long bs = 0;
while (bs < TarConstants.DATA_BLOCK - extra) {
long res = skip(TarConstants.DATA_BLOCK - extra - bs);
bs += res;
}
}
}
}
/**
* Skips 'n' bytes on the InputStream<br>
* Overrides default implementation of skip
*
*/
@Override
public long skip(long n) throws IOException {
if (defaultSkip) {
// use skip method of parent stream
// may not work if skip not implemented by parent
long bs = super.skip(n);
bytesRead += bs;
return bs;
}
if (n <= 0) {
return 0;
}
long left = n;
byte[] sBuff = new byte[SKIP_BUFFER_SIZE];
while (left > 0) {
int res = read(sBuff, 0, (int) (left < SKIP_BUFFER_SIZE ? left : SKIP_BUFFER_SIZE));
if (res < 0) {
break;
}
left -= res;
}
return n - left;
}
public boolean isDefaultSkip() {
return defaultSkip;
}
public void setDefaultSkip(boolean defaultSkip) {
this.defaultSkip = defaultSkip;
}
}

View file

@ -0,0 +1,163 @@
/**
* Copyright 2012 Kamran Zafar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.kamranzafar.jtar;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
/**
* @author Kamran Zafar
*
*/
public class TarOutputStream extends OutputStream {
private final OutputStream out;
private long bytesWritten;
private long currentFileSize;
private TarEntry currentEntry;
public TarOutputStream(OutputStream out) {
this.out = out;
bytesWritten = 0;
currentFileSize = 0;
}
public TarOutputStream(final File fout) throws FileNotFoundException {
this.out = new BufferedOutputStream(new FileOutputStream(fout));
bytesWritten = 0;
currentFileSize = 0;
}
/**
* Opens a file for writing.
*/
public TarOutputStream(final File fout, final boolean append) throws IOException {
@SuppressWarnings("resource")
RandomAccessFile raf = new RandomAccessFile(fout, "rw");
final long fileSize = fout.length();
if (append && fileSize > TarConstants.EOF_BLOCK) {
raf.seek(fileSize - TarConstants.EOF_BLOCK);
}
out = new BufferedOutputStream(new FileOutputStream(raf.getFD()));
}
/**
* Appends the EOF record and closes the stream
*
* @see java.io.FilterOutputStream#close()
*/
@Override
public void close() throws IOException {
closeCurrentEntry();
write( new byte[TarConstants.EOF_BLOCK] );
out.close();
}
/**
* Writes a byte to the stream and updates byte counters
*
* @see java.io.FilterOutputStream#write(int)
*/
@Override
public void write(int b) throws IOException {
out.write( b );
bytesWritten += 1;
if (currentEntry != null) {
currentFileSize += 1;
}
}
/**
* Checks if the bytes being written exceed the current entry size.
*
* @see java.io.FilterOutputStream#write(byte[], int, int)
*/
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (currentEntry != null && !currentEntry.isDirectory()) {
if (currentEntry.getSize() < currentFileSize + len) {
throw new IOException( "The current entry[" + currentEntry.getName() + "] size["
+ currentEntry.getSize() + "] is smaller than the bytes[" + ( currentFileSize + len )
+ "] being written." );
}
}
out.write( b, off, len );
bytesWritten += len;
if (currentEntry != null) {
currentFileSize += len;
}
}
/**
* Writes the next tar entry header on the stream
*
* @param entry
* @throws IOException
*/
public void putNextEntry(TarEntry entry) throws IOException {
closeCurrentEntry();
byte[] header = new byte[TarConstants.HEADER_BLOCK];
entry.writeEntryHeader( header );
write( header );
currentEntry = entry;
}
/**
* Closes the current tar entry
*
* @throws IOException
*/
protected void closeCurrentEntry() throws IOException {
if (currentEntry != null) {
if (currentEntry.getSize() > currentFileSize) {
throw new IOException( "The current entry[" + currentEntry.getName() + "] of size["
+ currentEntry.getSize() + "] has not been fully written." );
}
currentEntry = null;
currentFileSize = 0;
pad();
}
}
/**
* Pads the last content block
*
* @throws IOException
*/
protected void pad() throws IOException {
if (bytesWritten > 0) {
int extra = (int) ( bytesWritten % TarConstants.DATA_BLOCK );
if (extra > 0) {
write( new byte[TarConstants.DATA_BLOCK - extra] );
}
}
}
}

View file

@ -0,0 +1,96 @@
/**
* Copyright 2012 Kamran Zafar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.kamranzafar.jtar;
import java.io.File;
/**
* @author Kamran
*
*/
public class TarUtils {
/**
* Determines the tar file size of the given folder/file path
*
* @param path
* @return
*/
public static long calculateTarSize(File path) {
return tarSize(path) + TarConstants.EOF_BLOCK;
}
private static long tarSize(File dir) {
long size = 0;
if (dir.isFile()) {
return entrySize(dir.length());
} else {
File[] subFiles = dir.listFiles();
if (subFiles != null && subFiles.length > 0) {
for (File file : subFiles) {
if (file.isFile()) {
size += entrySize(file.length());
} else {
size += tarSize(file);
}
}
} else {
// Empty folder header
return TarConstants.HEADER_BLOCK;
}
}
return size;
}
private static long entrySize(long fileSize) {
long size = 0;
size += TarConstants.HEADER_BLOCK; // Header
size += fileSize; // File size
long extra = size % TarConstants.DATA_BLOCK;
if (extra > 0) {
size += (TarConstants.DATA_BLOCK - extra); // pad
}
return size;
}
public static String trim(String s, char c) {
StringBuffer tmp = new StringBuffer(s);
for (int i = 0; i < tmp.length(); i++) {
if (tmp.charAt(i) != c) {
break;
} else {
tmp.deleteCharAt(i);
}
}
for (int i = tmp.length() - 1; i >= 0; i--) {
if (tmp.charAt(i) != c) {
break;
} else {
tmp.deleteCharAt(i);
}
}
return tmp.toString();
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,279 @@
package org.renpy.android;
import android.content.Context;
import android.os.Vibrator;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.DisplayMetrics;
import android.view.inputmethod.InputMethodManager;
import android.view.View;
import java.util.List;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import org.kivy.android.PythonActivity;
/**
* Methods that are expected to be called via JNI, to access the
* device's non-screen hardware. (For example, the vibration and
* accelerometer.)
*/
public class Hardware {
// The context.
static Context context;
static View view;
public static final float defaultRv[] = { 0f, 0f, 0f };
/**
* Vibrate for s seconds.
*/
public static void vibrate(double s) {
Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (v != null) {
v.vibrate((int) (1000 * s));
}
}
/**
* Get an Overview of all Hardware Sensors of an Android Device
*/
public static String getHardwareSensors() {
SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
List<Sensor> allSensors = sm.getSensorList(Sensor.TYPE_ALL);
if (allSensors != null) {
String resultString = "";
for (Sensor s : allSensors) {
resultString += String.format("Name=" + s.getName());
resultString += String.format(",Vendor=" + s.getVendor());
resultString += String.format(",Version=" + s.getVersion());
resultString += String.format(",MaximumRange=" + s.getMaximumRange());
// XXX MinDelay is not in the 2.2
//resultString += String.format(",MinDelay=" + s.getMinDelay());
resultString += String.format(",Power=" + s.getPower());
resultString += String.format(",Type=" + s.getType() + "\n");
}
return resultString;
}
return "";
}
/**
* Get Access to 3 Axis Hardware Sensors Accelerometer, Orientation and Magnetic Field Sensors
*/
public static class generic3AxisSensor implements SensorEventListener {
private final SensorManager sSensorManager;
private final Sensor sSensor;
private final int sSensorType;
SensorEvent sSensorEvent;
public generic3AxisSensor(int sensorType) {
sSensorType = sensorType;
sSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
sSensor = sSensorManager.getDefaultSensor(sSensorType);
}
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
public void onSensorChanged(SensorEvent event) {
sSensorEvent = event;
}
/**
* Enable or disable the Sensor by registering/unregistering
*/
public void changeStatus(boolean enable) {
if (enable) {
sSensorManager.registerListener(this, sSensor, SensorManager.SENSOR_DELAY_NORMAL);
} else {
sSensorManager.unregisterListener(this, sSensor);
}
}
/**
* Read the Sensor
*/
public float[] readSensor() {
if (sSensorEvent != null) {
return sSensorEvent.values;
} else {
return defaultRv;
}
}
}
public static generic3AxisSensor accelerometerSensor = null;
public static generic3AxisSensor orientationSensor = null;
public static generic3AxisSensor magneticFieldSensor = null;
/**
* functions for backward compatibility reasons
*/
public static void accelerometerEnable(boolean enable) {
if ( accelerometerSensor == null )
accelerometerSensor = new generic3AxisSensor(Sensor.TYPE_ACCELEROMETER);
accelerometerSensor.changeStatus(enable);
}
public static float[] accelerometerReading() {
if ( accelerometerSensor == null )
return defaultRv;
return (float[]) accelerometerSensor.readSensor();
}
public static void orientationSensorEnable(boolean enable) {
if ( orientationSensor == null )
orientationSensor = new generic3AxisSensor(Sensor.TYPE_ORIENTATION);
orientationSensor.changeStatus(enable);
}
public static float[] orientationSensorReading() {
if ( orientationSensor == null )
return defaultRv;
return (float[]) orientationSensor.readSensor();
}
public static void magneticFieldSensorEnable(boolean enable) {
if ( magneticFieldSensor == null )
magneticFieldSensor = new generic3AxisSensor(Sensor.TYPE_MAGNETIC_FIELD);
magneticFieldSensor.changeStatus(enable);
}
public static float[] magneticFieldSensorReading() {
if ( magneticFieldSensor == null )
return defaultRv;
return (float[]) magneticFieldSensor.readSensor();
}
static public DisplayMetrics metrics = new DisplayMetrics();
/**
* Get display DPI.
*/
public static int getDPI() {
// AND: Shouldn't have to get the metrics like this every time...
PythonActivity.mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
return metrics.densityDpi;
}
// /**
// * Show the soft keyboard.
// */
// public static void showKeyboard(int input_type) {
// //Log.i("python", "hardware.Java show_keyword " input_type);
// InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
// SDLSurfaceView vw = (SDLSurfaceView) view;
// int inputType = input_type;
// if (vw.inputType != inputType){
// vw.inputType = inputType;
// imm.restartInput(view);
// }
// imm.showSoftInput(view, InputMethodManager.SHOW_FORCED);
// }
/**
* Hide the soft keyboard.
*/
public static void hideKeyboard() {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
/**
* Scan WiFi networks
*/
static List<ScanResult> latestResult;
public static void enableWifiScanner()
{
IntentFilter i = new IntentFilter();
i.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context c, Intent i) {
// Code to execute when SCAN_RESULTS_AVAILABLE_ACTION event occurs
WifiManager w = (WifiManager) c.getSystemService(Context.WIFI_SERVICE);
latestResult = w.getScanResults(); // Returns a <list> of scanResults
}
}, i);
}
public static String scanWifi() {
// Now you can call this and it should execute the broadcastReceiver's
// onReceive()
if (latestResult != null){
String latestResultString = "";
for (ScanResult result : latestResult)
{
latestResultString += String.format("%s\t%s\t%d\n", result.SSID, result.BSSID, result.level);
}
return latestResultString;
}
return "";
}
/**
* network state
*/
public static boolean network_state = false;
/**
* Check network state directly
*
* (only one connection can be active at a given moment, detects all network type)
*
*/
public static boolean checkNetwork()
{
boolean state = false;
final ConnectivityManager conMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo activeNetwork = conMgr.getActiveNetworkInfo();
if (activeNetwork != null && activeNetwork.isConnected()) {
state = true;
} else {
state = false;
}
return state;
}
/**
* To recieve network state changes
*/
public static void registerNetworkCheck()
{
IntentFilter i = new IntentFilter();
i.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context c, Intent i) {
network_state = checkNetwork();
}
}, i);
}
}

View file

@ -1,8 +1,7 @@
/** /**
* This class takes care of managing resources for us. In our code, we * This class takes care of managing resources for us. In our code, we
* can't use R, since the name of the package containing R will * can't use R, since the name of the package containing R will
* change. (This same code is used in both org.renpy.android and * change. So this is the next best thing.
* org.renpy.pygame.) So this is the next best thing.
*/ */
package org.renpy.android; package org.renpy.android;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@

View file

@ -6,22 +6,21 @@ from os import walk
import glob import glob
import sh import sh
EXCLUDE_EXTS = (".py", ".pyc", ".so.o", ".so.a", ".so.libs", ".pyx") EXCLUDE_EXTS = (".py", ".pyc", ".so.o", ".so.a", ".so.libs", ".pyx")
class LbryBootstrap(Bootstrap): class LbryBootstrap(Bootstrap):
name = 'lbry' name = 'lbry'
recipe_depends = ['genericndkbuild', ('python2', 'python3crystax')] recipe_depends = ['genericndkbuild']
def run_distribute(self): def assemble_distribution(self):
info_main("# Creating Android project ({})".format(self.name)) info_main("# Creating Android project ({})".format(self.name))
arch = self.ctx.archs[0] arch = self.ctx.archs[0]
python_install_dir = self.ctx.get_python_install_dir() python_install_dir = self.ctx.get_python_install_dir(arch.arch)
from_crystax = self.ctx.python_recipe.from_crystax #from_crystax = self.ctx.python_recipe.from_crystax
crystax_python_dir = join("crystax_python", "crystax_python") #crystax_python_dir = join("crystax_python", "crystax_python")
if len(self.ctx.archs) > 1: if len(self.ctx.archs) > 1:
raise ValueError("LBRY/gradle support only one arch") raise ValueError("LBRY/gradle support only one arch")
@ -39,98 +38,101 @@ class LbryBootstrap(Bootstrap):
with current_directory(self.dist_dir): with current_directory(self.dist_dir):
info("Copying Python distribution") info("Copying Python distribution")
if not exists("private") and not from_crystax: # if not exists("private"):
ensure_dir("private") # ensure_dir("private")
if not exists("crystax_python") and from_crystax:
ensure_dir(crystax_python_dir)
hostpython = sh.Command(self.ctx.hostpython) # gethostpython?
if not from_crystax: # hostpython = sh.Command(self.ctx.hostpython)
try: # if not from_crystax:
shprint(hostpython, '-OO', '-m', 'compileall', # try:
python_install_dir, # shprint(hostpython, '-OO', '-m', 'compileall',
_tail=10, _filterout="^Listing") # python_install_dir,
except sh.ErrorReturnCode: # _tail=10, _filterout="^Listing")
pass # except sh.ErrorReturnCode:
if not exists('python-install'): # pass
shprint( # if not exists('python-install'):
sh.cp, '-a', python_install_dir, './python-install') # shprint(
# sh.cp, '-a', python_install_dir, './python-install')
self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)])
self.distribute_javaclasses(self.ctx.javaclass_dir, self.distribute_javaclasses(self.ctx.javaclass_dir,
dest_dir=join("src", "main", "java")) dest_dir=join("src", "main", "java"))
if not from_crystax: python_bundle_dir = join(f'_python_bundle__{arch.arch}', '_python_bundle')
info("Filling private directory") ensure_dir(python_bundle_dir)
if not exists(join("private", "lib")): site_packages_dir = self.ctx.python_recipe.create_python_bundle(
info("private/lib does not exist, making") join(self.dist_dir, python_bundle_dir), arch)
shprint(sh.cp, "-a",
join("python-install", "lib"), "private")
shprint(sh.mkdir, "-p",
join("private", "include", "python2.7"))
libpymodules_fn = join("libs", arch.arch, "libpymodules.so") # if not from_crystax:
if exists(libpymodules_fn): # info("Filling private directory")
shprint(sh.mv, libpymodules_fn, 'private/') # if not exists(join("private", "lib")):
shprint(sh.cp, # info("private/lib does not exist, making")
join('python-install', 'include', # shprint(sh.cp, "-a",
'python2.7', 'pyconfig.h'), # join("python-install", "lib"), "private")
join('private', 'include', 'python2.7/')) # shprint(sh.mkdir, "-p",
# join("private", "include", "python2.7"))
info('Removing some unwanted files') # libpymodules_fn = join("libs", arch.arch, "libpymodules.so")
shprint(sh.rm, '-f', join('private', 'lib', 'libpython2.7.so')) # if exists(libpymodules_fn):
shprint(sh.rm, '-rf', join('private', 'lib', 'pkgconfig')) # 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') # libdir = join(self.dist_dir, 'private', 'lib', 'python2.7')
site_packages_dir = join(libdir, 'site-packages') # with current_directory(libdir):
with current_directory(libdir): # removes = []
removes = [] # for dirname, root, filenames in walk("."):
for dirname, root, filenames in walk("."): # for filename in filenames:
for filename in filenames: # for suffix in EXCLUDE_EXTS:
for suffix in EXCLUDE_EXTS: # if filename.endswith(suffix):
if filename.endswith(suffix): # removes.append(filename)
removes.append(filename) # shprint(sh.rm, '-f', *removes)
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') # else: # Python *is* loaded from crystax
# To quote the original distribute.sh, 'well...' # ndk_dir = self.ctx.ndk_dir
shprint(sh.rm, '-rf', 'lib2to3') # py_recipe = self.ctx.python_recipe
shprint(sh.rm, '-rf', 'idlelib') # python_dir = join(ndk_dir, 'sources', 'python',
for filename in glob.glob('config/libpython*.a'): # py_recipe.version, 'libs', arch.arch)
shprint(sh.rm, '-f', filename) # shprint(sh.cp, '-r', join(python_dir,
shprint(sh.rm, '-rf', 'config/python.o') # 'stdlib.zip'), crystax_python_dir)
# shprint(sh.cp, '-r', join(python_dir,
else: # Python *is* loaded from crystax # 'modules'), crystax_python_dir)
ndk_dir = self.ctx.ndk_dir # shprint(sh.cp, '-r', self.ctx.get_python_install_dir(),
py_recipe = self.ctx.python_recipe # join(crystax_python_dir, 'site-packages'))
python_dir = join(ndk_dir, 'sources', 'python', #
py_recipe.version, 'libs', arch.arch) # info('Renaming .so files to reflect cross-compile')
shprint(sh.cp, '-r', join(python_dir, # site_packages_dir = join(crystax_python_dir, "site-packages")
'stdlib.zip'), crystax_python_dir) # find_ret = shprint(
shprint(sh.cp, '-r', join(python_dir, # sh.find, site_packages_dir, '-iname', '*.so')
'modules'), crystax_python_dir) # filenames = find_ret.stdout.decode('utf-8').split('\n')[:-1]
shprint(sh.cp, '-r', self.ctx.get_python_install_dir(), # for filename in filenames:
join(crystax_python_dir, 'site-packages')) # parts = filename.split('.')
# if len(parts) <= 2:
info('Renaming .so files to reflect cross-compile') # continue
site_packages_dir = join(crystax_python_dir, "site-packages") # shprint(sh.mv, filename, filename.split('.')[0] + '.so')
find_ret = shprint( # site_packages_dir = join(abspath(curdir),
sh.find, site_packages_dir, '-iname', '*.so') # site_packages_dir)
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: if 'sqlite3' not in self.ctx.recipe_build_order:
with open('blacklist.txt', 'a') as fileh: with open('blacklist.txt', 'a') as fileh:
fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n') fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n')
self.strip_libraries(arch) self.strip_libraries(arch)
self.fry_eggs(site_packages_dir) self.fry_eggs(site_packages_dir)
super(LbryBootstrap, self).run_distribute() super().assemble_distribution()
bootstrap = LbryBootstrap() bootstrap = LbryBootstrap()

View file

@ -435,7 +435,7 @@ main.py that loads it.''')
if exists('build.properties'): if exists('build.properties'):
os.remove('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 global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
default_android_api = 12 default_android_api = 12
import argparse import argparse
@ -505,6 +505,10 @@ tools directory of the Android SDK.
default=join(curdir, 'whitelist.txt'), default=join(curdir, 'whitelist.txt'),
help=('Use a whitelist file to prevent blacklisting of ' help=('Use a whitelist file to prevent blacklisting of '
'file in the final APK')) 'file in the final APK'))
ap.add_argument('--release', dest='build_mode', action='store_const',
const='release', default='debug',
help='Build your app as a non-debug release build. '
'(Disables gdb debugging among other things)')
ap.add_argument('--add-jar', dest='add_jar', action='append', ap.add_argument('--add-jar', dest='add_jar', action='append',
help=('Add a Java .jar to the libs, so you can access its ' help=('Add a Java .jar to the libs, so you can access its '
'classes with pyjnius. You can specify this ' 'classes with pyjnius. You can specify this '
@ -604,4 +608,4 @@ tools directory of the Android SDK.
if __name__ == "__main__": if __name__ == "__main__":
parse_args() parse_args_and_make_package()

View file

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

View file

@ -7,7 +7,7 @@ buildscript {
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
} }
dependencies { 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 %} {%- endif %}
buildTypes { // buildTypes {
debug { // debug {
} // }
release { // release {
{% if args.sign -%} // {% if args.sign -%}
signingConfig signingConfigs.release // signingConfig signingConfigs.release
{%- endif %} // {%- endif %}
} // }
} // }
sourceSets { sourceSets {
main { main {
@ -104,68 +104,68 @@ subprojects {
} }
} }
nexusPublishing { //nexusPublishing {
repositories { // repositories {
sonatype { // sonatype {
stagingProfileId = sonatypeStagingProfileId // stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername // username = ossrhUsername
password = ossrhPassword // password = ossrhPassword
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) // nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) // snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
} // }
} // }
} //}
afterEvaluate { //afterEvaluate {
publishing { // publishing {
publications { // publications {
release(MavenPublication) { // release(MavenPublication) {
groupId 'io.lbry' // groupId 'io.lbry'
artifactId 'lbrysdk64' // artifactId 'lbrysdk64'
version '{{ args.version }}' // 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 //signing {
// sign publishing.publications
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
}
dependencies { dependencies {
{%- for aar in aars %} {%- for aar in aars %}
compile(name: '{{ aar }}', ext: 'aar') implementation (name: '{{ aar }}', ext: 'aar')
{%- endfor -%} {%- endfor -%}
{%- if args.depends -%} {%- if args.depends -%}
{%- for depend in args.depends %} {%- for depend in args.depends %}
compile '{{ depend }}' implementation '{{ depend }}'
{%- endfor %} {%- endfor %}
{%- endif %} {%- endif %}
} }

View file

@ -166,7 +166,7 @@ dependencies {
{%- endfor -%} {%- endfor -%}
{%- if args.depends -%} {%- if args.depends -%}
{%- for depend in args.depends %} {%- for depend in args.depends %}
compile '{{ depend }}' implement '{{ depend }}'
{%- endfor %} {%- endfor %}
{%- endif %} {%- endif %}
} }

View file

@ -1,5 +1,6 @@
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx4096m
ossrhUsername={{ env["SONATYPE_USERNAME"] }} ossrhUsername={{ env["SONATYPE_USERNAME"] }}
ossrhPassword={{ env["SONATYPE_PASSWORD"] }} ossrhPassword={{ env["SONATYPE_PASSWORD"] }}

View file

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

View file

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

View file

@ -0,0 +1,84 @@
# prevent user to include invalid extensions
*.apk
*.aab
*.apks
*.pxd
# eggs
*.egg-info
# unit test
unittest/*
# python config
config/makesetup
# unused kivy files (platform specific)
kivy/input/providers/wm_*
kivy/input/providers/mactouch*
kivy/input/providers/probesysfs*
kivy/input/providers/mtdev*
kivy/input/providers/hidinput*
kivy/core/camera/camera_videocapture*
kivy/core/spelling/*osx*
kivy/core/video/video_pyglet*
kivy/tools
kivy/tests/*
kivy/*/*.h
kivy/*/*.pxi
# unused encodings
lib-dynload/*codec*
encodings/cp*.pyo
encodings/tis*
encodings/shift*
encodings/bz2*
encodings/iso*
encodings/undefined*
encodings/johab*
encodings/p*
encodings/m*
encodings/euc*
encodings/k*
encodings/unicode_internal*
encodings/quo*
encodings/gb*
encodings/big5*
encodings/hp*
encodings/hz*
# unused python modules
bsddb/*
wsgiref/*
hotshot/*
pydoc_data/*
tty.pyo
anydbm.pyo
nturl2path.pyo
LICENCE.txt
macurl2path.pyo
dummy_threading.pyo
audiodev.pyo
antigravity.pyo
dumbdbm.pyo
sndhdr.pyo
__phello__.foo.pyo
sunaudio.pyo
os2emxpath.pyo
multiprocessing/dummy*
# unused binaries python modules
lib-dynload/termios.so
lib-dynload/_lsprof.so
lib-dynload/*audioop.so
lib-dynload/_hotshot.so
lib-dynload/_heapq.so
lib-dynload/_json.so
lib-dynload/grp.so
lib-dynload/resource.so
lib-dynload/pyexpat.so
lib-dynload/_ctypes_test.so
lib-dynload/_testcapi.so
# odd files
plat-linux3/regen

View file

@ -0,0 +1,8 @@
# Uncomment this if you're using STL in your project
# See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information
# APP_STL := stlport_static
# APP_ABI := armeabi armeabi-v7a x86
APP_ABI := $(ARCH)
APP_PLATFORM := $(NDK_API)

View file

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

View file

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

View file

@ -0,0 +1,19 @@
package org.kivy.android;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.Context;
public class GenericBroadcastReceiver extends BroadcastReceiver {
GenericBroadcastReceiverCallback listener;
public GenericBroadcastReceiver(GenericBroadcastReceiverCallback listener) {
super();
this.listener = listener;
}
public void onReceive(Context context, Intent intent) {
this.listener.onReceive(context, intent);
}
}

View file

@ -0,0 +1,8 @@
package org.kivy.android;
import android.content.Intent;
import android.content.Context;
public interface GenericBroadcastReceiverCallback {
void onReceive(Context context, Intent intent);
};

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,19 @@
package org.kivy.android;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.Context;
public class GenericBroadcastReceiver extends BroadcastReceiver {
GenericBroadcastReceiverCallback listener;
public GenericBroadcastReceiver(GenericBroadcastReceiverCallback listener) {
super();
this.listener = listener;
}
public void onReceive(Context context, Intent intent) {
this.listener.onReceive(context, intent);
}
}

View file

@ -0,0 +1,8 @@
package org.kivy.android;
import android.content.Intent;
import android.content.Context;
public interface GenericBroadcastReceiverCallback {
void onReceive(Context context, Intent intent);
};

View file

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

View file

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

View file

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

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context="io.lbry.browser.ServiceControlActivity"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="5">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="@string/service_status"
/>
<TextView
android:id="@+id/text_service_status"
android:textColor="@color/red"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:gravity="right"
android:layout_weight="2"
android:text="@string/stopped"
/>
</LinearLayout>
<Button
android:id="@+id/btn_start_stop"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="12dp"
android:text="@string/start"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginTop="24dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="3">
<TextView
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:layout_gravity="center_vertical"
android:text="@string/unit_tests"
/>
<Button
android:id="@+id/btn_run_tests"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="right|center_vertical"
android:text="@string/run_tests"/>
</LinearLayout>
<ScrollView
android:layout_marginTop="12dp"
android:padding="6dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/test_runner_output"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
/>
</ScrollView>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,172 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
}
}
plugins {
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
}
allprojects {
repositories {
jcenter()
maven {
url 'https://maven.google.com'
}
flatDir {
dirs 'libs'
}
}
}
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
group = "io.lbry"
version = "{{ args.version }}"
android {
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
defaultConfig {
minSdkVersion {{ args.min_sdk_version }}
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }} * 10 + 2
versionName '{{ args.version }}'
multiDexEnabled true
ndk {
abiFilters "arm64-v8a"
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
dexOptions {
jumboMode true
}
{% if args.sign -%}
signingConfigs {
release {
storeFile file(System.getenv("P4A_RELEASE_KEYSTORE"))
keyAlias System.getenv("P4A_RELEASE_KEYALIAS")
storePassword System.getenv("P4A_RELEASE_KEYSTORE_PASSWD")
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
}
}
{%- endif %}
buildTypes {
debug {
}
release {
{% if args.sign -%}
signingConfig signingConfigs.release
{%- endif %}
}
}
sourceSets {
main {
jniLibs.srcDir 'libs'
}
}
}
ext {
compileSdkVersion = {{ android_api }}
buildToolsVersion = '{{ build_tools_version }}'
minSdkVersion = {{ args.min_sdk_version }}
targetSdkVersion = {{ android_api }}
}
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
}
}
}
}
//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 {
println("Components: " + components*.name)
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'
}
}
}
}
}
}
signing {
sign publishing.publications
}
dependencies {
{%- for aar in aars %}
compile(name: '{{ aar }}', ext: 'aar')
{%- endfor -%}
{%- if args.depends -%}
{%- for depend in args.depends %}
implementation '{{ depend }}'
{%- endfor %}
{%- endif %}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#40B89A</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FFFFFF</color>
<color name="red">#FF0000</color>
<color name="green">#00C000</color>
<color name="white">#FFFFFF</color>
<color name="lbryGreen">#2F9176</color>
<color name="nextLbryGreen">#38D9A9</color>
</resources>

View file

@ -0,0 +1,10 @@
android.useAndroidX=true
android.enableJetifier=true
ossrhUsername={{ env["SONATYPE_USERNAME"] }}
ossrhPassword={{ env["SONATYPE_PASSWORD"] }}
sonatypeStagingProfileId={{ env["SONATYPE_STAGING_PROFILE_ID"] }}
signing.keyId={{ env["NEXUS_SIGNING_KEY_ID"] }}
signing.password={{ env["NEXUS_SIGNING_KEY_PASSWORD"] }}
signing.secretKeyRingFile={{ env["NEXUS_SIGNING_KEYRING_FILE"] }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,52 @@
import sh
from os.path import join
from pythonforandroid.toolchain import (
Bootstrap, current_directory, info, info_main, shprint)
from pythonforandroid.util import ensure_dir
class ServiceOnlyBootstrap(Bootstrap):
name = 'service_only'
recipe_depends = list(
set(Bootstrap.recipe_depends).union({'genericndkbuild'})
)
def assemble_distribution(self):
info_main('# Creating Android project from build and {} bootstrap'.format(
self.name))
info('This currently just copies the build stuff straight from the build dir.')
shprint(sh.rm, '-rf', self.dist_dir)
shprint(sh.cp, '-r', self.build_dir, self.dist_dir)
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:
self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)])
self.distribute_aars(arch)
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)
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 = ServiceOnlyBootstrap()

View file

@ -0,0 +1,91 @@
# 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
#>sqlite3
# conditionnal include depending if some recipes are included or not.
sqlite3/*
lib-dynload/_sqlite3.so
#<sqlite3

View file

@ -0,0 +1 @@
include $(call all-subdir-makefiles)

View file

@ -0,0 +1,7 @@
# 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)

View file

@ -0,0 +1,18 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := main
# Add your application source files here...
LOCAL_SRC_FILES := start.c pyjniusjni.c
LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS)
LOCAL_SHARED_LIBRARIES := python_shared
LOCAL_LDLIBS := -llog $(EXTRA_LDLIBS)
LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS)
include $(BUILD_SHARED_LIBRARY)

View file

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

View file

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

View file

@ -0,0 +1,103 @@
#include <pthread.h>
#include <jni.h>
#define LOGI(...) do {} while (0)
#define LOGE(...) do {} while (0)
#include "android/log.h"
/* These JNI management functions are taken from SDL2, but modified to refer to pyjnius */
/* #define LOG(n, x) __android_log_write(ANDROID_LOG_INFO, (n), (x)) */
/* #define LOGP(x) LOG("python", (x)) */
#define LOG_TAG "Python_android"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
/* Function headers */
JNIEnv* Android_JNI_GetEnv(void);
static void Android_JNI_ThreadDestroyed(void*);
static pthread_key_t mThreadKey;
static JavaVM* mJavaVM;
int Android_JNI_SetupThread(void)
{
Android_JNI_GetEnv();
return 1;
}
/* Library init */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv *env;
mJavaVM = vm;
LOGI("JNI_OnLoad called");
if ((*mJavaVM)->GetEnv(mJavaVM, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("Failed to get the environment using GetEnv()");
return -1;
}
/*
* Create mThreadKey so we can keep track of the JNIEnv assigned to each thread
* Refer to http://developer.android.com/guide/practices/design/jni.html for the rationale behind this
*/
if (pthread_key_create(&mThreadKey, Android_JNI_ThreadDestroyed) != 0) {
__android_log_print(ANDROID_LOG_ERROR, "pyjniusjni", "Error initializing pthread key");
}
Android_JNI_SetupThread();
return JNI_VERSION_1_4;
}
JNIEnv* Android_JNI_GetEnv(void)
{
/* From http://developer.android.com/guide/practices/jni.html
* All threads are Linux threads, scheduled by the kernel.
* They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then
* attached to the JavaVM. For example, a thread started with pthread_create can be attached with the
* JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv,
* and cannot make JNI calls.
* Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main"
* ThreadGroup, making it visible to the debugger. Calling AttachCurrentThread on an already-attached thread
* is a no-op.
* Note: You can call this function any number of times for the same thread, there's no harm in it
*/
JNIEnv *env;
int status = (*mJavaVM)->AttachCurrentThread(mJavaVM, &env, NULL);
if(status < 0) {
LOGE("failed to attach current thread");
return 0;
}
/* From http://developer.android.com/guide/practices/jni.html
* Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward,
* in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be
* called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific
* to store the JNIEnv in thread-local-storage; that way it'll be passed into your destructor as the argument.)
* Note: The destructor is not called unless the stored value is != NULL
* Note: You can call this function any number of times for the same thread, there's no harm in it
* (except for some lost CPU cycles)
*/
pthread_setspecific(mThreadKey, (void*) env);
return env;
}
static void Android_JNI_ThreadDestroyed(void* value)
{
/* The thread is being destroyed, detach it from the Java VM and set the mThreadKey value to NULL as required */
JNIEnv *env = (JNIEnv*) value;
if (env != NULL) {
(*mJavaVM)->DetachCurrentThread(mJavaVM);
pthread_setspecific(mThreadKey, NULL);
}
}
void *WebView_AndroidGetJNIEnv()
{
return Android_JNI_GetEnv();
}

View file

@ -0,0 +1,330 @@
package org.kivy.android;
import android.os.SystemClock;
import java.io.File;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.view.KeyEvent;
import android.util.Log;
import android.widget.Toast;
import android.os.Bundle;
import android.os.PowerManager;
import android.content.Context;
import android.content.pm.PackageManager;
import org.renpy.android.ResourceManager;
public class PythonActivity extends Activity {
// This activity is modified from a mixture of the SDLActivity and
// PythonActivity in the SDL2 bootstrap, but removing all the SDL2
// specifics.
private static final String TAG = "PythonActivity";
public static PythonActivity mActivity = null;
/** If shared libraries (e.g. the native application) could not be loaded. */
public static boolean mBrokenLibraries;
protected static Thread mPythonThread;
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;
}
public String getEntryPoint(String search_dir) {
/* Get the main file (.pyc|.py) depending on if we
* have a compiled version or not.
*/
List<String> entryPoints = new ArrayList<String>();
entryPoints.add("main.pyc"); // python 3 compiled files
for (String value : entryPoints) {
File mainFile = new File(search_dir + "/" + value);
if (mainFile.exists()) {
return value;
}
}
return "main.py";
}
public static void initialize() {
// The static nature of the singleton and Android quirkiness force us to initialize everything here
// Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
mBrokenLibraries = false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.v(TAG, "My oncreate running");
resourceManager = new ResourceManager(this);
Log.v(TAG, "Ready to unpack");
File app_root_file = new File(getAppRoot());
PythonUtil.unpackAsset(mActivity, "private", app_root_file, true);
PythonUtil.unpackPyBundle(mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false);
Log.v(TAG, "About to do super onCreate");
super.onCreate(savedInstanceState);
Log.v(TAG, "Did super onCreate");
this.mActivity = this;
//this.showLoadingScreen();
Log.v("Python", "Device: " + android.os.Build.DEVICE);
Log.v("Python", "Model: " + android.os.Build.MODEL);
//Log.v(TAG, "Ready to unpack");
//new UnpackFilesTask().execute(getAppRoot());
PythonActivity.initialize();
// Load shared libraries
String errorMsgBrokenLib = "";
try {
loadLibraries();
} catch(UnsatisfiedLinkError e) {
System.err.println(e.getMessage());
mBrokenLibraries = true;
errorMsgBrokenLib = e.getMessage();
} catch(Exception e) {
System.err.println(e.getMessage());
mBrokenLibraries = true;
errorMsgBrokenLib = e.getMessage();
}
if (mBrokenLibraries)
{
AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
dlgAlert.setMessage("An error occurred while trying to load the application libraries. Please try again and/or reinstall."
+ System.getProperty("line.separator")
+ System.getProperty("line.separator")
+ "Error: " + errorMsgBrokenLib);
dlgAlert.setTitle("Python Error");
dlgAlert.setPositiveButton("Exit",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,int id) {
// if this button is clicked, close current activity
PythonActivity.mActivity.finish();
}
});
dlgAlert.setCancelable(false);
dlgAlert.create().show();
return;
}
// Set up the Python environment
String app_root_dir = getAppRoot();
String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath();
String entry_point = getEntryPoint(app_root_dir);
Log.v(TAG, "Setting env vars for start.c and Python to use");
PythonActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point);
PythonActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir);
PythonActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir);
PythonActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory);
PythonActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir);
PythonActivity.nativeSetenv("PYTHONHOME", app_root_dir);
PythonActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib");
PythonActivity.nativeSetenv("PYTHONOPTIMIZE", "2");
try {
Log.v(TAG, "Access to our meta-data...");
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();
}
} catch (PackageManager.NameNotFoundException e) {
}
final Thread pythonThread = new Thread(new PythonMain(), "PythonThread");
PythonActivity.mPythonThread = pythonThread;
pythonThread.start();
}
@Override
public void onDestroy() {
Log.i("Destroy", "end of app");
super.onDestroy();
// make sure all child threads (python_thread) are stopped
android.os.Process.killProcess(android.os.Process.myPid());
}
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));
}
long lastBackClick = 0;
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Check if the key event was the Back button
if (keyCode == KeyEvent.KEYCODE_BACK) {
// If there's no web page history, bubble up to the default
// system behavior (probably exit the activity)
if (SystemClock.elapsedRealtime() - lastBackClick > 2000){
lastBackClick = SystemClock.elapsedRealtime();
Toast.makeText(this, "Tap again to close the app", Toast.LENGTH_LONG).show();
return true;
}
lastBackClick = SystemClock.elapsedRealtime();
}
return super.onKeyDown(keyCode, event);
}
//----------------------------------------------------------------------------
// Listener interface for onNewIntent
//
public interface NewIntentListener {
void onNewIntent(Intent intent);
}
private List<NewIntentListener> newIntentListeners = null;
public void registerNewIntentListener(NewIntentListener listener) {
if ( this.newIntentListeners == null )
this.newIntentListeners = Collections.synchronizedList(new ArrayList<NewIntentListener>());
this.newIntentListeners.add(listener);
}
public void unregisterNewIntentListener(NewIntentListener listener) {
if ( this.newIntentListeners == null )
return;
this.newIntentListeners.remove(listener);
}
@Override
protected void onNewIntent(Intent intent) {
if ( this.newIntentListeners == null )
return;
this.onResume();
synchronized ( this.newIntentListeners ) {
Iterator<NewIntentListener> iterator = this.newIntentListeners.iterator();
while ( iterator.hasNext() ) {
(iterator.next()).onNewIntent(intent);
}
}
}
//----------------------------------------------------------------------------
// Listener interface for onActivityResult
//
public interface ActivityResultListener {
void onActivityResult(int requestCode, int resultCode, Intent data);
}
private List<ActivityResultListener> activityResultListeners = null;
public void registerActivityResultListener(ActivityResultListener listener) {
if ( this.activityResultListeners == null )
this.activityResultListeners = Collections.synchronizedList(new ArrayList<ActivityResultListener>());
this.activityResultListeners.add(listener);
}
public void unregisterActivityResultListener(ActivityResultListener listener) {
if ( this.activityResultListeners == null )
return;
this.activityResultListeners.remove(listener);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
if ( this.activityResultListeners == null )
return;
this.onResume();
synchronized ( this.activityResultListeners ) {
Iterator<ActivityResultListener> iterator = this.activityResultListeners.iterator();
while ( iterator.hasNext() )
(iterator.next()).onActivityResult(requestCode, resultCode, intent);
}
}
public static void start_service(
String serviceTitle,
String serviceDescription,
String pythonServiceArgument
) {
_do_start_service(
serviceTitle, serviceDescription, pythonServiceArgument, true
);
}
public static void start_service_not_as_foreground(
String serviceTitle,
String serviceDescription,
String pythonServiceArgument
) {
_do_start_service(
serviceTitle, serviceDescription, pythonServiceArgument, false
);
}
public static void _do_start_service(
String serviceTitle,
String serviceDescription,
String pythonServiceArgument,
boolean showForegroundNotification
) {
Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath();
String app_root_dir = PythonActivity.mActivity.getAppRoot();
String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service");
serviceIntent.putExtra("androidPrivate", argument);
serviceIntent.putExtra("androidArgument", app_root_dir);
serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point);
serviceIntent.putExtra("pythonName", "python");
serviceIntent.putExtra("pythonHome", app_root_dir);
serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib");
serviceIntent.putExtra("serviceStartAsForeground",
(showForegroundNotification ? "true" : "false")
);
serviceIntent.putExtra("serviceTitle", serviceTitle);
serviceIntent.putExtra("serviceDescription", serviceDescription);
serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument);
PythonActivity.mActivity.startService(serviceIntent);
}
public static void stop_service() {
Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
PythonActivity.mActivity.stopService(serviceIntent);
}
public static native void nativeSetenv(String name, String value);
public static native int nativeInit(Object arguments);
}
class PythonMain implements Runnable {
@Override
public void run() {
PythonActivity.nativeInit(new String[0]);
}
}

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{ args.package }}"
android:versionCode="{{ args.numeric_version }}"
android:versionName="{{ args.version }}"
android:installLocation="auto">
<supports-screens
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true"
{% if args.min_sdk_version >= 9 %}
android:xlargeScreens="true"
{% endif %}
/>
<!-- Android 2.3.3 -->
<uses-sdk android:minSdkVersion="{{ args.min_sdk_version }}" android:targetSdkVersion="{{ android_api }}" />
<!-- Set permissions -->
{% for perm in args.permissions %}
{% if '.' in perm %}
<uses-permission android:name="{{ perm }}" />
{% else %}
<uses-permission android:name="android.permission.{{ perm }}" />
{% endif %}
{% endfor %}
{% if args.wakelock %}
<uses-permission android:name="android.permission.WAKE_LOCK" />
{% endif %}
{% if args.billing_pubkey %}
<uses-permission android:name="com.android.vending.BILLING" />
{% endif %}
<!-- Create a Java class extending SDLActivity and place it in a
directory under src matching the package, e.g.
src/com/gamemaker/game/MyGame.java
then replace "SDLActivity" with the name of your class (e.g. "MyGame")
in the XML below.
An example Java class can be found in README-android.txt
-->
<application android:label="@string/app_name"
{% if debug %}android:debuggable="true"{% endif %}
android:icon="@mipmap/icon"
android:allowBackup="{{ args.allow_backup }}"
{% if args.backup_rules %}android:fullBackupContent="@xml/{{ args.backup_rules }}"{% endif %}
android:theme="{{args.android_apptheme}}{% if not args.window %}.Fullscreen{% endif %}"
android:hardwareAccelerated="true"
android:extractNativeLibs="true" >
{% for l in args.android_used_libs %}
<uses-library android:name="{{ l }}" />
{% endfor %}
{% for m in args.meta_data %}
<meta-data android:name="{{ m.split('=', 1)[0] }}" android:value="{{ m.split('=', 1)[-1] }}"/>{% endfor %}
<meta-data android:name="wakelock" android:value="{% if args.wakelock %}1{% else %}0{% endif %}"/>
<activity android:name="org.kivy.android.PythonActivity"
android:label="@string/app_name"
android:configChanges="keyboardHidden|orientation{% if args.min_sdk_version >= 13 %}|screenSize{% endif %}"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
{%- if args.intent_filters -%}
{{- args.intent_filters -}}
{%- endif -%}
</activity>
{% if service %}
<service android:name="org.kivy.android.PythonService"
android:process=":pythonservice"
android:exported="true"/>
{% endif %}
{% for name in service_names %}
<service android:name="{{ args.package }}.Service{{ name|capitalize }}"
android:process=":service_{{ name }}"
android:exported="true" />
{% endfor %}
{% if args.billing_pubkey %}
<service android:name="org.kivy.android.billing.BillingReceiver"
android:process=":pythonbilling" />
<receiver android:name="org.kivy.android.billing.BillingReceiver"
android:process=":pythonbillingreceiver"
android:exported="false">
<intent-filter>
<action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
<action android:name="com.android.vending.billing.RESPONSE_CODE" />
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
</intent-filter>
</receiver>
{% endif %}
</application>
</manifest>

View file

@ -0,0 +1,70 @@
package {{ args.package }};
import android.os.Binder;
import android.os.IBinder;
import android.content.Intent;
import android.content.Context;
import org.kivy.android.PythonService;
public class Service{{ name|capitalize }} extends PythonService {
/**
* Binder given to clients
*/
private final IBinder mBinder = new Service{{ name|capitalize }}Binder();
{% if sticky %}
/**
* {@inheritDoc}
*/
@Override
public int startType() {
return START_STICKY;
}
{% endif %}
@Override
protected int getServiceId() {
return {{ service_id }};
}
public static void start(Context ctx, String pythonServiceArgument) {
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
intent.putExtra("androidPrivate", argument);
intent.putExtra("androidArgument", argument);
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
intent.putExtra("serviceTitle", "{{ name|capitalize }}");
intent.putExtra("serviceDescription", "");
intent.putExtra("pythonName", "{{ name }}");
intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}");
intent.putExtra("pythonHome", argument);
intent.putExtra("androidUnpack", argument);
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
ctx.startService(intent);
}
public static void stop(Context ctx) {
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
ctx.stopService(intent);
}
/**
* {@inheritDoc}
*/
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
public class Service{{ name|capitalize }}Binder extends Binder {
Service{{ name|capitalize }} getService() {
// Return this instance of Service{{ name|capitalize }} so clients can call public methods
return Service{{ name|capitalize }}.this;
}
}
}

View file

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

View file

@ -0,0 +1,49 @@
from pythonforandroid.toolchain import Bootstrap, current_directory, info, info_main, shprint
from pythonforandroid.util import ensure_dir
from os.path import join
import sh
class WebViewBootstrap(Bootstrap):
name = 'webview'
recipe_depends = list(
set(Bootstrap.recipe_depends).union({'genericndkbuild'})
)
def assemble_distribution(self):
info_main('# Creating Android project from build and {} bootstrap'.format(
self.name))
shprint(sh.rm, '-rf', self.dist_dir)
shprint(sh.cp, '-r', self.build_dir, self.dist_dir)
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:
self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)])
self.distribute_aars(arch)
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)
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 = WebViewBootstrap()

View file

@ -0,0 +1,91 @@
# 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
#>sqlite3
# conditionnal include depending if some recipes are included or not.
sqlite3/*
lib-dynload/_sqlite3.so
#<sqlite3

View file

@ -0,0 +1 @@
include $(call all-subdir-makefiles)

View file

@ -0,0 +1,7 @@
# 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)

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