import new p4a

This commit is contained in:
zeppi 2022-12-02 15:15:34 -05:00
parent d9d8195764
commit a712121c3c
3036 changed files with 445600 additions and 3262 deletions

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
def get_env(self, with_flags_in_cc=True, clang=False): return '{triplet}{ndk_api}'.format(
env = {} triplet=self.command_prefix, ndk_api=self.ctx.ndk_api
cflags = [
'-DANDROID',
'-fomit-frame-pointer',
'-D__ANDROID_API__={}'.format(self.ctx.ndk_api)]
if not clang:
cflags.append('-mandroid')
else:
cflags.append('-target ' + self.target)
toolchain = '{android_host}-{toolchain_version}'.format(
android_host=self.ctx.toolchain_prefix,
toolchain_version=self.ctx.toolchain_version)
toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain,
'prebuilt', build_platform)
cflags.append('-gcc-toolchain {}'.format(toolchain))
env['CFLAGS'] = ' '.join(cflags)
# Link the extra global link paths first before anything else
# (such that overriding system libraries with them is possible)
env['LDFLAGS'] = ' ' + " ".join([
"-L'" + l.replace("'", "'\"'\"'") + "'" # no shlex.quote in py2
for l in self.extra_global_link_paths
]) + ' '
sysroot = join(self.ctx._ndk_dir, 'sysroot')
if exists(sysroot):
# post-15 NDK per
# https://android.googlesource.com/platform/ndk/+/ndk-r15-release/docs/UnifiedHeaders.md
env['CFLAGS'] += ' -isystem {}/sysroot/usr/include/{}'.format(
self.ctx.ndk_dir, self.ctx.toolchain_prefix)
env['CFLAGS'] += ' -I{}/sysroot/usr/include/{}'.format(
self.ctx.ndk_dir, self.command_prefix)
else:
sysroot = self.ctx.ndk_platform
env['CFLAGS'] += ' -I{}'.format(self.ctx.ndk_platform)
env['CFLAGS'] += ' -isysroot {} '.format(sysroot)
env['CFLAGS'] += '-I' + join(self.ctx.get_python_install_dir(),
'include/python{}'.format(
self.ctx.python_recipe.version[0:3])
) )
env['LDFLAGS'] += '--sysroot={} '.format(self.ctx.ndk_platform) @property
def clang_exe(self):
"""Full path of the clang compiler depending on the android's ndk
version used."""
return self.get_clang_exe()
env["CXXFLAGS"] = env["CFLAGS"] @property
def clang_exe_cxx(self):
"""Full path of the clang++ compiler depending on the android's ndk
version used."""
return self.get_clang_exe(plus_plus=True)
env["LDFLAGS"] += " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)]) def get_clang_exe(self, with_target=False, plus_plus=False):
"""Returns the full path of the clang/clang++ compiler, supports two
kwargs:
if self.ctx.ndk == 'crystax': - `with_target`: prepend `target` to clang
env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch) - `plus_plus`: will return the clang++ compiler (defaults to `False`)
"""
compiler = 'clang'
if with_target:
compiler = '{target}-{compiler}'.format(
target=self.target, compiler=compiler
)
if plus_plus:
compiler += '++'
return join(self.ctx.ndk.llvm_bin_dir, compiler)
toolchain_prefix = self.ctx.toolchain_prefix def get_env(self, with_flags_in_cc=True):
toolchain_version = self.ctx.toolchain_version env = {}
command_prefix = self.command_prefix
env['TOOLCHAIN_PREFIX'] = toolchain_prefix # HOME: User's home directory
env['TOOLCHAIN_VERSION'] = toolchain_version #
# Many tools including p4a store outputs in the user's home
# directory. This is found from the HOME environment variable
# and falls back to the system account database. Setting HOME
# can be used to globally divert these tools to use a different
# path. Furthermore, in containerized environments the user may
# not exist in the account database, so if HOME isn't set than
# these tools will fail.
if 'HOME' in environ:
env['HOME'] = environ['HOME']
# CFLAGS/CXXFLAGS: the processor flags
env['CFLAGS'] = ' '.join(self.common_cflags).format(target=self.target)
if self.arch_cflags:
# each architecture may have has his own CFLAGS
env['CFLAGS'] += ' ' + ' '.join(self.arch_cflags)
env['CXXFLAGS'] = env['CFLAGS']
# CPPFLAGS (for macros and includes)
env['CPPFLAGS'] = ' '.join(self.common_cppflags).format(
ctx=self.ctx,
command_prefix=self.command_prefix,
python_includes=join(
self.ctx.get_python_install_dir(self.arch),
'include/python{}'.format(self.ctx.python_recipe.version[0:3]),
),
)
# LDFLAGS: Link the extra global link paths first before anything else
# (such that overriding system libraries with them is possible)
env['LDFLAGS'] = (
' '
+ " ".join(
[
"-L'"
+ link_path.replace("'", "'\"'\"'")
+ "'" # no shlex.quote in py2
for link_path in self.extra_global_link_paths
]
)
+ ' ' + ' '.join(self.common_ldflags).format(
ctx_libs_dir=self.ctx.get_libs_dir(self.arch)
)
)
# LDLIBS: Library flags or names given to compilers when they are
# supposed to invoke the linker.
env['LDLIBS'] = ' '.join(self.common_ldlibs)
# CCACHE
ccache = '' 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,10 +127,28 @@ 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
@ -144,5 +158,8 @@ def _set_user_options():
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()

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

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,17 +235,13 @@ main.py that loads it.''')
assets_dir = "src/main/assets" assets_dir = "src/main/assets"
# Delete the old assets. # Delete the old assets.
try_unlink(join(assets_dir, 'public.mp3')) shutil.rmtree(assets_dir, ignore_errors=True)
try_unlink(join(assets_dir, 'private.mp3'))
ensure_dir(assets_dir) ensure_dir(assets_dir)
# In order to speedup import and initial depack,
# construct a python27.zip
make_python_zip()
# Add extra environment variable file into tar-able directory: # Add extra environment variable file into tar-able directory:
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-") env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f: with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
if hasattr(args, "window"):
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n") 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")
@ -304,31 +249,141 @@ main.py that loads it.''')
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n") f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
# Package up the private data (public not supported). # Package up the private data (public not supported).
tar_dirs = [env_vars_tarpath] use_setup_py = get_dist_info_for("use_setup_py",
error_if_missing=False) is True
private_tar_dirs = [env_vars_tarpath]
_temp_dirs_to_clean = []
try:
if args.private: if args.private:
tar_dirs.append(args.private) if not use_setup_py or (
for python_bundle_dir in ('private', 'crystax_python', '_python_bundle'): not exists(join(args.private, "setup.py")) and
if exists(python_bundle_dir): not exists(join(args.private, "pyproject.toml"))
tar_dirs.append(python_bundle_dir) ):
print('No setup.py/pyproject.toml used, copying '
'full private data into .apk.')
private_tar_dirs.append(args.private)
else:
print("Copying main.py's ONLY, since other app data is "
"expected in site-packages.")
main_py_only_dir = tempfile.mkdtemp()
_temp_dirs_to_clean.append(main_py_only_dir)
# Check all main.py files we need to copy:
copy_paths = ["main.py", join("service", "main.py")]
for copy_path in copy_paths:
variants = [
copy_path,
copy_path.partition(".")[0] + ".pyc",
]
# Check in all variants with all possible endings:
for variant in variants:
if exists(join(args.private, variant)):
# Make sure surrounding directly exists:
dir_path = os.path.dirname(variant)
if (len(dir_path) > 0 and
not exists(
join(main_py_only_dir, dir_path)
)):
os.mkdir(join(main_py_only_dir, dir_path))
# Copy actual file:
shutil.copyfile(
join(args.private, variant),
join(main_py_only_dir, variant),
)
# Append directory with all main.py's to result apk paths:
private_tar_dirs.append(main_py_only_dir)
if get_bootstrap_name() == "webview": if get_bootstrap_name() == "webview":
tar_dirs.append('webview_includes') for asset in listdir('webview_includes'):
shutil.copy(join('webview_includes', asset), join(assets_dir, asset))
for asset in args.assets:
asset_src, asset_dest = asset.split(":")
if isfile(realpath(asset_src)):
ensure_dir(dirname(join(assets_dir, asset_dest)))
shutil.copy(realpath(asset_src), join(assets_dir, asset_dest))
else:
shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest))
if args.private or args.launcher: if args.private or args.launcher:
for arch in get_dist_info_for("archs"):
libs_dir = f"libs/{arch}"
make_tar( make_tar(
join(assets_dir, 'private.mp3'), tar_dirs, args.ignore_path, join(libs_dir, "libpybundle.so"),
optimize_python=args.optimize_python) [f"_python_bundle__{arch}"],
byte_compile_python=args.byte_compile_python,
optimize_python=args.optimize_python,
)
make_tar(
join(assets_dir, "private.tar"),
private_tar_dirs,
byte_compile_python=args.byte_compile_python,
optimize_python=args.optimize_python,
)
finally:
for directory in _temp_dirs_to_clean:
shutil.rmtree(directory)
# Remove extra env vars tar-able directory: # Remove extra env vars tar-able directory:
shutil.rmtree(env_vars_tarpath) shutil.rmtree(env_vars_tarpath)
# Prepare some variables for templating process # Prepare some variables for templating process
res_dir = "src/main/res" res_dir = "src/main/res"
res_dir_initial = "src/res_initial"
# make res_dir stateless
if exists(res_dir_initial):
shutil.rmtree(res_dir, ignore_errors=True)
shutil.copytree(res_dir_initial, res_dir)
else:
shutil.copytree(res_dir, res_dir_initial)
# Add user resouces
for resource in args.resources:
resource_src, resource_dest = resource.split(":")
if isfile(realpath(resource_src)):
ensure_dir(dirname(join(res_dir, resource_dest)))
shutil.copy(realpath(resource_src), join(res_dir, resource_dest))
else:
shutil.copytree(realpath(resource_src),
join(res_dir, resource_dest), dirs_exist_ok=True)
default_icon = 'templates/kivy-icon.png' default_icon = 'templates/kivy-icon.png'
default_presplash = 'templates/kivy-presplash.jpg' default_presplash = 'templates/kivy-presplash.jpg'
shutil.copy( shutil.copy(
args.icon or default_icon, args.icon or default_icon,
join(res_dir, 'drawable/icon.png') join(res_dir, 'mipmap/icon.png')
) )
if args.icon_fg and args.icon_bg:
shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png'))
shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png'))
with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd:
fd.write("""<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/icon_background"/>
<foreground android:drawable="@mipmap/icon_foreground"/>
</adaptive-icon>
""")
elif args.icon_fg or args.icon_bg:
print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. "
"Ignoring.")
if get_bootstrap_name() != "service_only": if get_bootstrap_name() != "service_only":
lottie_splashscreen = join(res_dir, 'raw/splashscreen.json')
if args.presplash_lottie:
shutil.copy(
'templates/lottie.xml',
join(res_dir, 'layout/lottie.xml')
)
ensure_dir(join(res_dir, 'raw'))
shutil.copy(
args.presplash_lottie,
join(res_dir, 'raw/splashscreen.json')
)
else:
if exists(lottie_splashscreen):
remove(lottie_splashscreen)
remove(join(res_dir, 'layout/lottie.xml'))
shutil.copy( shutil.copy(
args.presplash or default_presplash, args.presplash or default_presplash,
join(res_dir, 'drawable/presplash.jpg') join(res_dir, 'drawable/presplash.jpg')
@ -360,17 +415,17 @@ main.py that loads it.''')
version_code = 0 version_code = 0
if not args.numeric_version: if not args.numeric_version:
# Set version code in format (arch-minsdk-app_version) """
with open(join(dirname(__file__), 'dist_info.json'), 'r') as dist_info: Set version code in format (10 + minsdk + app_version)
dist_data = json.load(dist_info) Historically versioning was (arch + minsdk + app_version),
arch = dist_data["archs"][0] with arch expressed with a single digit from 6 to 9.
arch_dict = {"x86_64": "9", "arm64-v8a": "8", "armeabi-v7a": "7", "x86": "6"} Since the multi-arch support, has been changed to 10.
arch_code = arch_dict.get(arch, '1') """
min_sdk = args.min_sdk_version min_sdk = args.min_sdk_version
for i in args.version.split('.'): for i in args.version.split('.'):
version_code *= 100 version_code *= 100
version_code += int(i) version_code += int(i)
args.numeric_version = "{}{}{}".format(arch_code, min_sdk, version_code) args.numeric_version = "{}{}{}".format("10", min_sdk, version_code)
if args.intent_filters: if args.intent_filters:
with open(args.intent_filters) as fd: with open(args.intent_filters) as fd:
@ -387,6 +442,9 @@ main.py that loads it.''')
for spec in args.extra_source_dirs: for spec in args.extra_source_dirs:
if ':' in spec: if ':' in spec:
specdir, specincludes = spec.split(':') specdir, specincludes = spec.split(':')
print('WARNING: Currently gradle builds only support including source '
'directories, so when building using gradle all files in '
'{} will be included.'.format(specdir))
else: else:
specdir = spec specdir = spec
specincludes = '**' specincludes = '**'
@ -402,6 +460,7 @@ main.py that loads it.''')
service = True service = True
service_names = [] service_names = []
base_service_class = args.service_class_name.split('.')[-1]
for sid, spec in enumerate(args.services): for sid, spec in enumerate(args.services):
spec = spec.split(':') spec = spec.split(':')
name = spec[0] name = spec[0]
@ -426,6 +485,7 @@ main.py that loads it.''')
foreground=foreground, foreground=foreground,
sticky=sticky, sticky=sticky,
service_id=sid + 1, service_id=sid + 1,
base_service_class=base_service_class,
) )
# Find the SDK directory and target API # Find the SDK directory and target API
@ -447,19 +507,37 @@ main.py that loads it.''')
# Try to build with the newest available build tools # Try to build with the newest available build tools
ignored = {".DS_Store", ".ds_store"} ignored = {".DS_Store", ".ds_store"}
build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored] build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
build_tools_versions.sort(key=LooseVersion) build_tools_versions = sorted(build_tools_versions,
key=LooseVersion)
build_tools_version = build_tools_versions[-1] build_tools_version = build_tools_versions[-1]
# Folder name for launcher (used by SDL2 bootstrap) # Folder name for launcher (used by SDL2 bootstrap)
url_scheme = 'kivy' url_scheme = 'kivy'
# Copy backup rules file if specified and update the argument
res_xml_dir = join(res_dir, 'xml')
if args.backup_rules:
ensure_dir(res_xml_dir)
shutil.copy(join(args.private, args.backup_rules), res_xml_dir)
args.backup_rules = split(args.backup_rules)[1][:-4]
# Copy res_xml files to src/main/res/xml
if args.res_xmls:
ensure_dir(res_xml_dir)
for xmlpath in args.res_xmls:
if not os.path.exists(xmlpath):
xmlpath = join(args.private, xmlpath)
shutil.copy(xmlpath, res_xml_dir)
# Render out android manifest: # Render out android manifest:
manifest_path = "src/main/AndroidManifest.xml" manifest_path = "src/main/AndroidManifest.xml"
render_args = { render_args = {
"args": args, "args": args,
"service": service, "service": service,
"service_names": service_names, "service_names": service_names,
"android_api": android_api "android_api": android_api,
"debug": "debug" in args.build_mode,
"native_services": args.native_services
} }
if get_bootstrap_name() == "sdl2": if get_bootstrap_name() == "sdl2":
render_args["url_scheme"] = url_scheme render_args["url_scheme"] = url_scheme
@ -482,9 +560,17 @@ main.py that loads it.''')
aars=aars, aars=aars,
jars=jars, jars=jars,
android_api=android_api, android_api=android_api,
build_tools_version=build_tools_version build_tools_version=build_tools_version,
debug_build="debug" in args.build_mode,
is_library=(get_bootstrap_name() == 'service_library'),
) )
# gradle properties
render(
'gradle.tmpl.properties',
'gradle.properties',
args=args)
# ant build templates # ant build templates
render( render(
'build.tmpl.xml', 'build.tmpl.xml',
@ -493,9 +579,18 @@ main.py that loads it.''')
versioned_name=versioned_name) versioned_name=versioned_name)
# String resources: # String resources:
timestamp = time.time()
if 'SOURCE_DATE_EPOCH' in environ:
# for reproducible builds
timestamp = int(environ['SOURCE_DATE_EPOCH'])
private_version = "{} {} {}".format(
args.version,
args.numeric_version,
timestamp
)
render_args = { render_args = {
"args": args, "args": args,
"private_version": str(time.time()) "private_version": hashlib.sha1(private_version.encode()).hexdigest()
} }
if get_bootstrap_name() == "sdl2": if get_bootstrap_name() == "sdl2":
render_args["url_scheme"] = url_scheme render_args["url_scheme"] = url_scheme
@ -527,27 +622,31 @@ main.py that loads it.''')
for patch_name in os.listdir(join('src', 'patches')): for patch_name in os.listdir(join('src', 'patches')):
patch_path = join('src', 'patches', patch_name) patch_path = join('src', 'patches', patch_name)
print("Applying patch: " + str(patch_path)) print("Applying patch: " + str(patch_path))
try:
subprocess.check_output([ # -N: insist this is FORWARD patch, don't reverse apply
# -N: insist this is FORWARd patch, don't reverse apply
# -p1: strip first path component # -p1: strip first path component
# -t: batch mode, don't ask questions # -t: batch mode, don't ask questions
"patch", "-N", "-p1", "-t", "-i", patch_path patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path]
])
try:
# Use a dry run to establish whether the patch is already applied.
# If we don't check this, the patch may be partially applied (which is bad!)
subprocess.check_output(patch_command + ["--dry-run"])
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
if e.returncode == 1: if e.returncode == 1:
# Return code 1 means it didn't apply, this will # Return code 1 means not all hunks could be applied, this usually
# usually mean it is already applied. # means the patch is already applied.
print("Warning: failed to apply patch (" + print("Warning: failed to apply patch (exit code 1), "
"exit code 1), " + "assuming it is already applied: ",
"assuming it is already applied: " + str(patch_path))
str(patch_path)
)
else: else:
raise e raise e
else:
# The dry run worked, so do the real thing
subprocess.check_output(patch_command)
def parse_args(args=None): def parse_args_and_make_package(args=None):
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
# Get the default minsdk, equal to the NDK API that this dist is built against # Get the default minsdk, equal to the NDK API that this dist is built against
@ -602,16 +701,36 @@ tools directory of the Android SDK.
help='Custom key=value to add in application metadata') help='Custom key=value to add in application metadata')
ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[], ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml') help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
ap.add_argument('--asset', dest='assets',
action="append", default=[],
metavar="/path/to/source:dest",
help='Put this in the assets folder at assets/dest')
ap.add_argument('--resource', dest='resources',
action="append", default=[],
metavar="/path/to/source:kind/asset",
help='Put this in the res folder at res/kind')
ap.add_argument('--icon', dest='icon', ap.add_argument('--icon', dest='icon',
help=('A png file to use as the icon for ' help=('A png file to use as the icon for '
'the application.')) 'the application.'))
ap.add_argument('--icon-fg', dest='icon_fg',
help=('A png file to use as the foreground of the adaptive icon '
'for the application.'))
ap.add_argument('--icon-bg', dest='icon_bg',
help=('A png file to use as the background of the adaptive icon '
'for the application.'))
ap.add_argument('--service', dest='services', action='append', default=[], ap.add_argument('--service', dest='services', action='append', default=[],
help='Declare a new service entrypoint: ' help='Declare a new service entrypoint: '
'NAME:PATH_TO_PY[:foreground]') 'NAME:PATH_TO_PY[:foreground]')
ap.add_argument('--native-service', dest='native_services', action='append', default=[],
help='Declare a new native service: '
'package.name.service')
if get_bootstrap_name() != "service_only": if get_bootstrap_name() != "service_only":
ap.add_argument('--presplash', dest='presplash', ap.add_argument('--presplash', dest='presplash',
help=('A jpeg file to use as a screen while the ' help=('A jpeg file to use as a screen while the '
'application is loading.')) 'application is loading.'))
ap.add_argument('--presplash-lottie', dest='presplash_lottie',
help=('A lottie (json) file to use as an animation while the '
'application is loading.'))
ap.add_argument('--presplash-color', ap.add_argument('--presplash-color',
dest='presplash_color', dest='presplash_color',
default='#000000', default='#000000',
@ -636,6 +755,28 @@ tools directory of the Android SDK.
'https://developer.android.com/guide/' 'https://developer.android.com/guide/'
'topics/manifest/' 'topics/manifest/'
'activity-element.html')) 'activity-element.html'))
ap.add_argument('--enable-androidx', dest='enable_androidx',
action='store_true',
help=('Enable the AndroidX support library, '
'requires api = 28 or greater'))
ap.add_argument('--android-entrypoint', dest='android_entrypoint',
default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
help='Defines which java class will be used for startup, usually a subclass of PythonActivity')
ap.add_argument('--android-apptheme', dest='android_apptheme',
default='@android:style/Theme.NoTitleBar',
help='Defines which app theme should be selected for the main activity')
ap.add_argument('--add-compile-option', dest='compile_options', default=[],
action='append', help='add compile options to gradle.build')
ap.add_argument('--add-gradle-repository', dest='gradle_repositories',
default=[],
action='append',
help='Ddd a repository for gradle')
ap.add_argument('--add-packaging-option', dest='packaging_options',
default=[],
action='append',
help='Dndroid packaging options')
ap.add_argument('--wakelock', dest='wakelock', action='store_true', ap.add_argument('--wakelock', dest='wakelock', action='store_true',
help=('Indicate if the application needs the device ' help=('Indicate if the application needs the device '
'to stay on')) 'to stay on'))
@ -647,6 +788,13 @@ tools directory of the Android SDK.
default=join(curdir, 'whitelist.txt'), default=join(curdir, 'whitelist.txt'),
help=('Use a whitelist file to prevent blacklisting of ' help=('Use a whitelist file to prevent blacklisting of '
'file in the final APK')) 'file in the final APK'))
ap.add_argument('--release', dest='build_mode', action='store_const',
const='release', default='debug',
help='Build your app as a non-debug release build. '
'(Disables gdb debugging among other things)')
ap.add_argument('--with-debug-symbols', dest='with_debug_symbols',
action='store_const', const=True, default=False,
help='Will keep debug symbols from `.so` files.')
ap.add_argument('--add-jar', dest='add_jar', action='append', ap.add_argument('--add-jar', dest='add_jar', action='append',
help=('Add a Java .jar to the libs, so you can access its ' help=('Add a Java .jar to the libs, so you can access its '
'classes with pyjnius. You can specify this ' 'classes with pyjnius. You can specify this '
@ -674,6 +822,8 @@ tools directory of the Android SDK.
'filename containing xml. The filename should be ' 'filename containing xml. The filename should be '
'located relative to the python-for-android ' 'located relative to the python-for-android '
'directory')) 'directory'))
ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[],
help='Add files to res/xml directory (for example device-filters)', nargs='+')
ap.add_argument('--with-billing', dest='billing_pubkey', ap.add_argument('--with-billing', dest='billing_pubkey',
help='If set, the billing service will be added (not implemented)') help='If set, the billing service will be added (not implemented)')
ap.add_argument('--add-source', dest='extra_source_dirs', action='append', ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
@ -685,8 +835,6 @@ tools directory of the Android SDK.
ap.add_argument('--try-system-python-compile', dest='try_system_python_compile', ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
action='store_true', action='store_true',
help='Use the system python during compileall if possible.') help='Use the system python during compileall if possible.')
ap.add_argument('--no-compile-pyo', dest='no_compile_pyo', action='store_true',
help='Do not optimise .py files to .pyo.')
ap.add_argument('--sign', action='store_true', ap.add_argument('--sign', action='store_true',
help=('Try to sign the APK with your credentials. You must set ' help=('Try to sign the APK with your credentials. You must set '
'the appropriate environment variables.')) 'the appropriate environment variables.'))
@ -698,10 +846,33 @@ tools directory of the Android SDK.
help='Set the launch mode of the main activity in the manifest.') help='Set the launch mode of the main activity in the manifest.')
ap.add_argument('--allow-backup', dest='allow_backup', default='true', ap.add_argument('--allow-backup', dest='allow_backup', default='true',
help="if set to 'false', then android won't backup the application.") help="if set to 'false', then android won't backup the application.")
ap.add_argument('--backup-rules', dest='backup_rules', default='',
help=('Backup rules for Android Auto Backup. Argument is a '
'filename containing xml. The filename should be '
'located relative to the private directory containing your source code '
'files (containing your main.py entrypoint). '
'See https://developer.android.com/guide/topics/data/'
'autobackup#IncludingFiles for more information'))
ap.add_argument('--no-byte-compile-python', dest='byte_compile_python',
action='store_false', default=True,
help='Skip byte compile for .py files.')
ap.add_argument('--no-optimize-python', dest='optimize_python', ap.add_argument('--no-optimize-python', dest='optimize_python',
action='store_false', default=True, action='store_false', default=True,
help=('Whether to compile to optimised .pyo files, using -OO ' help=('Whether to compile to optimised .pyc files, using -OO '
'(strips docstrings and asserts)')) '(strips docstrings and asserts)'))
ap.add_argument('--extra-manifest-xml', default='',
help=('Extra xml to write directly inside the <manifest> element of'
'AndroidManifest.xml'))
ap.add_argument('--extra-manifest-application-arguments', default='',
help='Extra arguments to be added to the <manifest><application> tag of'
'AndroidManifest.xml')
ap.add_argument('--manifest-placeholders', dest='manifest_placeholders',
default='[:]', help=('Inject build variables into the manifest '
'via the manifestPlaceholders property'))
ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS,
help='Use that parameter if you need to implement your own PythonServive Java class')
ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
help='The full java class name of the main activity')
# Put together arguments, and add those from .p4a config file: # Put together arguments, and add those from .p4a config file:
if args is None: if args is None:
@ -721,7 +892,6 @@ tools directory of the Android SDK.
_read_configuration() _read_configuration()
args = ap.parse_args(args) args = ap.parse_args(args)
args.ignore_path = []
if args.name and args.name[0] == '"' and args.name[-1] == '"': if args.name and args.name[0] == '"' and args.name[-1] == '"':
args.name = args.name[1:-1] args.name = args.name[1:-1]
@ -751,10 +921,12 @@ 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'])
@ -763,10 +935,6 @@ tools directory of the Android SDK.
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:
patterns = [x.strip() for x in fd.read().splitlines() patterns = [x.strip() for x in fd.read().splitlines()
@ -791,4 +959,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-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(crystax_python_dir)) {
LOGP("crystax_python exists");
snprintf(paths, 256,
"%s/stdlib.zip:%s/modules",
crystax_python_dir, crystax_python_dir);
}
if (dir_exists(python_bundle_dir)) { if (dir_exists(python_bundle_dir)) {
LOGP("_python_bundle dir exists"); LOGP("_python_bundle dir exists");
snprintf(paths, 256, snprintf(paths, 256,
"%s/stdlib.zip:%s/modules", "%s/stdlib.zip:%s/modules",
python_bundle_dir, python_bundle_dir); 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

@ -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,7 +62,12 @@ 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;
@ -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,8 +162,11 @@ 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);
//sticky servcie runtime/restart is managed by the OS. leave it running when app is closed
if (startType() != START_STICKY) {
stopSelf(); stopSelf();
} }
}
@Override @Override
public void run(){ public void run(){

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 {
@ -74,4 +86,173 @@ 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 {
if(method == "private"){
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING); 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,13 +13,20 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
{%- for repo in args.gradle_repositories %}
{{repo}}
{%- endfor %}
flatDir { flatDir {
dirs 'libs' 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 }}
@ -29,8 +36,23 @@ android {
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 {
release { release {
@ -40,6 +62,15 @@ android {
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD") keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
} }
} }
{%- endif %}
{% if args.packaging_options -%}
packagingOptions {
{%- for option in args.packaging_options %}
{{option}}
{%- endfor %}
}
{%- endif %} {%- endif %}
buildTypes { buildTypes {
@ -53,28 +84,51 @@ android {
} }
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

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

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,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,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,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)

View file

@ -0,0 +1,20 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := main
# LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include
# 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,12 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := main
LOCAL_SRC_FILES := YourSourceHere.c
LOCAL_STATIC_LIBRARIES := SDL2_static
include $(BUILD_SHARED_LIBRARY)
$(call import-module,SDL)LOCAL_PATH := $(call my-dir)

View file

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

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,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,572 @@
package org.kivy.android;
import android.os.SystemClock;
import java.io.InputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import android.view.ViewGroup;
import android.view.KeyEvent;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.PowerManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.widget.ImageView;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.widget.AbsoluteLayout;
import android.view.ViewGroup.LayoutParams;
import android.webkit.WebBackForwardList;
import android.webkit.WebViewClient;
import android.webkit.WebView;
import android.webkit.CookieManager;
import android.net.Uri;
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;
public static boolean mOpenExternalLinksInBrowser = false;
/** If shared libraries (e.g. SDL or the native application) could not be loaded. */
public static boolean mBrokenLibraries;
protected static ViewGroup mLayout;
protected static WebView mWebView;
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 quirkyness force us to initialize everything here
// Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
mWebView = null;
mLayout = null;
mBrokenLibraries = false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.v(TAG, "My oncreate running");
resourceManager = new ResourceManager(this);
super.onCreate(savedInstanceState);
this.mActivity = this;
this.showLoadingScreen();
new UnpackFilesTask().execute(getAppRoot());
}
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) {
Log.v("Python", "Device: " + android.os.Build.DEVICE);
Log.v("Python", "Model: " + android.os.Build.MODEL);
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(PythonActivity.mActivity);
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 webview
String app_root_dir = getAppRoot();
mWebView = new WebView(PythonActivity.mActivity);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.getSettings().setDomStorageEnabled(true);
mWebView.loadUrl("file:///android_asset/_load.html");
mWebView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri u = Uri.parse(url);
if (mOpenExternalLinksInBrowser) {
if (!(u.getScheme().equals("file") || u.getHost().equals("127.0.0.1"))) {
Intent i = new Intent(Intent.ACTION_VIEW, u);
startActivity(i);
return true;
}
}
return false;
}
@Override
public void onPageFinished(WebView view, String url) {
CookieManager.getInstance().flush();
}
});
mLayout = new AbsoluteLayout(PythonActivity.mActivity);
mLayout.addView(mWebView);
setContentView(mLayout);
String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath();
String entry_point = getEntryPoint(app_root_dir);
Log.v(TAG, "Setting env vars for start.c and Python to use");
PythonActivity.nativeSetenv("ANDROID_ENTRYPOINT", 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();
final Thread wvThread = new Thread(new WebViewLoaderMain(), "WvThread");
wvThread.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));
}
public static void loadUrl(String url) {
class LoadUrl implements Runnable {
private String mUrl;
public LoadUrl(String url) {
mUrl = url;
}
public void run() {
mWebView.loadUrl(mUrl);
}
}
Log.i(TAG, "Opening URL: " + url);
mActivity.runOnUiThread(new LoadUrl(url));
}
public static void enableZoom() {
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mWebView.getSettings().setBuiltInZoomControls(true);
mWebView.getSettings().setDisplayZoomControls(false);
}
});
}
public static ViewGroup getLayout() {
return mLayout;
}
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) {
// Go back if there is web page history behind,
// but not to the start preloader
WebBackForwardList webViewBackForwardList = mWebView.copyBackForwardList();
if (webViewBackForwardList.getCurrentIndex() > 1) {
mWebView.goBack();
return true;
}
// 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);
}
// loading screen implementation
public static ImageView mImageView = null;
public void removeLoadingScreen() {
runOnUiThread(new Runnable() {
public void run() {
if (PythonActivity.mImageView != null &&
PythonActivity.mImageView.getParent() != null) {
((ViewGroup)PythonActivity.mImageView.getParent()).removeView(
PythonActivity.mImageView);
PythonActivity.mImageView = null;
}
}
});
}
protected void showLoadingScreen() {
// load the bitmap
// 1. if the image is valid and we don't have layout yet, assign this bitmap
// as main view.
// 2. if we have a layout, just set it in the layout.
// 3. If we have an mImageView already, then do nothing because it will have
// already been made the content view or added to the layout.
if (mImageView == null) {
int presplashId = this.resourceManager.getIdentifier("presplash", "drawable");
InputStream is = this.getResources().openRawResource(presplashId);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(is);
} finally {
try {
is.close();
} catch (IOException e) {};
}
mImageView = new ImageView(this);
mImageView.setImageBitmap(bitmap);
/*
* Set the presplash loading screen background color
* https://developer.android.com/reference/android/graphics/Color.html
* Parse the color string, and return the corresponding color-int.
* If the string cannot be parsed, throws an IllegalArgumentException exception.
* Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
* 'red', 'blue', 'green', 'black', 'white', 'gray', 'cyan', 'magenta', 'yellow',
* 'lightgray', 'darkgray', 'grey', 'lightgrey', 'darkgrey', 'aqua', 'fuchsia',
* 'lime', 'maroon', 'navy', 'olive', 'purple', 'silver', 'teal'.
*/
String backgroundColor = resourceManager.getString("presplash_color");
if (backgroundColor != null) {
try {
mImageView.setBackgroundColor(Color.parseColor(backgroundColor));
} catch (IllegalArgumentException e) {}
}
mImageView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.FILL_PARENT));
mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
}
if (mLayout == null) {
setContentView(mImageView);
} else if (PythonActivity.mImageView.getParent() == null){
mLayout.addView(mImageView);
}
}
//----------------------------------------------------------------------------
// 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);
/**
* 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);
}
}
class PythonMain implements Runnable {
@Override
public void run() {
PythonActivity.nativeInit(new String[0]);
}
}
class WebViewLoaderMain implements Runnable {
@Override
public void run() {
WebViewLoader.testConnection();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

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