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__ = '0.5'
__version__ = '2022.09.04'

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
* can't use R, since the name of the package containing R will
* change. (This same code is used in both org.renpy.android and
* org.renpy.pygame.) So this is the next best thing.
* change. So this is the next best thing.
*/
package org.renpy.android;

View file

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

View file

@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.android.tools.build:gradle:7.1.2'
}
}
@ -13,13 +13,20 @@ allprojects {
repositories {
google()
jcenter()
{%- for repo in args.gradle_repositories %}
{{repo}}
{%- endfor %}
flatDir {
dirs 'libs'
}
}
}
{% if is_library %}
apply plugin: 'com.android.library'
{% else %}
apply plugin: 'com.android.application'
{% endif %}
android {
compileSdkVersion {{ android_api }}
@ -29,8 +36,23 @@ android {
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }}
versionName '{{ args.version }}'
manifestPlaceholders = {{ args.manifest_placeholders}}
}
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
{% if debug_build -%}
doNotStrip '**/*.so'
{% else %}
exclude 'lib/**/gdbserver'
exclude 'lib/**/gdb.setup'
{%- endif %}
}
{% if args.sign -%}
signingConfigs {
release {
@ -40,6 +62,15 @@ android {
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
}
}
{%- endif %}
{% if args.packaging_options -%}
packagingOptions {
{%- for option in args.packaging_options %}
{{option}}
{%- endfor %}
}
{%- endif %}
buildTypes {
@ -53,28 +84,51 @@ android {
}
compileOptions {
{% if args.enable_androidx %}
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
{% else %}
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
{% endif %}
{%- for option in args.compile_options %}
{{option}}
{%- endfor %}
}
sourceSets {
main {
jniLibs.srcDir 'libs'
java {
{%- for adir, pattern in args.extra_source_dirs -%}
srcDir '{{adir}}'
{%- endfor -%}
}
}
}
aaptOptions {
noCompress "tflite"
}
}
dependencies {
{%- for aar in aars %}
compile(name: '{{ aar }}', ext: 'aar')
implementation(name: '{{ aar }}', ext: 'aar')
{%- endfor -%}
{%- for jar in jars %}
compile files('src/main/libs/{{ jar }}')
implementation files('src/main/libs/{{ jar }}')
{%- endfor -%}
{%- if args.depends -%}
{%- for depend in args.depends %}
compile '{{ depend }}'
implementation '{{ depend }}'
{%- endfor %}
{%- 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