import new p4a
|
@ -1,2 +1 @@
|
||||||
|
__version__ = '2022.09.04'
|
||||||
__version__ = '0.5'
|
|
||||||
|
|
83
p4a/pythonforandroid/androidndk.py
Normal 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")
|
|
@ -1,22 +1,46 @@
|
||||||
from distutils.spawn import find_executable
|
from distutils.spawn import find_executable
|
||||||
from os import environ
|
from os import environ
|
||||||
from os.path import (exists, join, dirname, split)
|
from os.path import join
|
||||||
from glob import glob
|
from multiprocessing import cpu_count
|
||||||
|
|
||||||
from pythonforandroid.recipe import Recipe
|
from pythonforandroid.recipe import Recipe
|
||||||
from pythonforandroid.util import BuildInterruptingException, build_platform
|
from pythonforandroid.util import BuildInterruptingException, build_platform
|
||||||
|
|
||||||
|
|
||||||
class Arch(object):
|
class Arch:
|
||||||
|
|
||||||
toolchain_prefix = None
|
|
||||||
'''The prefix for the toolchain dir in the NDK.'''
|
|
||||||
|
|
||||||
command_prefix = None
|
command_prefix = None
|
||||||
'''The prefix for NDK commands such as gcc.'''
|
'''The prefix for NDK commands such as gcc.'''
|
||||||
|
|
||||||
|
arch = ""
|
||||||
|
'''Name of the arch such as: `armeabi-v7a`, `arm64-v8a`, `x86`...'''
|
||||||
|
|
||||||
|
arch_cflags = []
|
||||||
|
'''Specific arch `cflags`, expect to be overwrote in subclass if needed.'''
|
||||||
|
|
||||||
|
common_cflags = [
|
||||||
|
'-target {target}',
|
||||||
|
'-fomit-frame-pointer'
|
||||||
|
]
|
||||||
|
|
||||||
|
common_cppflags = [
|
||||||
|
'-DANDROID',
|
||||||
|
'-I{ctx.ndk.sysroot_include_dir}',
|
||||||
|
'-I{python_includes}',
|
||||||
|
]
|
||||||
|
|
||||||
|
common_ldflags = ['-L{ctx_libs_dir}']
|
||||||
|
|
||||||
|
common_ldlibs = ['-lm']
|
||||||
|
|
||||||
|
common_ldshared = [
|
||||||
|
'-pthread',
|
||||||
|
'-shared',
|
||||||
|
'-Wl,-O1',
|
||||||
|
'-Wl,-Bsymbolic-functions',
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, ctx):
|
def __init__(self, ctx):
|
||||||
super(Arch, self).__init__()
|
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
|
|
||||||
# Allows injecting additional linker paths used by any recipe.
|
# Allows injecting additional linker paths used by any recipe.
|
||||||
|
@ -28,6 +52,14 @@ class Arch(object):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.arch
|
return self.arch
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ndk_lib_dir(self):
|
||||||
|
return join(self.ctx.ndk.sysroot_lib_dir, self.command_prefix)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ndk_lib_dir_versioned(self):
|
||||||
|
return join(self.ndk_lib_dir, str(self.ctx.ndk_api))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def include_dirs(self):
|
def include_dirs(self):
|
||||||
return [
|
return [
|
||||||
|
@ -38,216 +70,235 @@ class Arch(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self):
|
def target(self):
|
||||||
target_data = self.command_prefix.split('-')
|
# As of NDK r19, the toolchains installed by default with the
|
||||||
return '-'.join(
|
# NDK may be used in-place. The make_standalone_toolchain.py script
|
||||||
[target_data[0], 'none', target_data[1], target_data[2]])
|
# is no longer needed for interfacing with arbitrary build systems.
|
||||||
|
# See: https://developer.android.com/ndk/guides/other_build_systems
|
||||||
|
return '{triplet}{ndk_api}'.format(
|
||||||
|
triplet=self.command_prefix, ndk_api=self.ctx.ndk_api
|
||||||
|
)
|
||||||
|
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
@property
|
||||||
|
def clang_exe(self):
|
||||||
|
"""Full path of the clang compiler depending on the android's ndk
|
||||||
|
version used."""
|
||||||
|
return self.get_clang_exe()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def clang_exe_cxx(self):
|
||||||
|
"""Full path of the clang++ compiler depending on the android's ndk
|
||||||
|
version used."""
|
||||||
|
return self.get_clang_exe(plus_plus=True)
|
||||||
|
|
||||||
|
def get_clang_exe(self, with_target=False, plus_plus=False):
|
||||||
|
"""Returns the full path of the clang/clang++ compiler, supports two
|
||||||
|
kwargs:
|
||||||
|
|
||||||
|
- `with_target`: prepend `target` to clang
|
||||||
|
- `plus_plus`: will return the clang++ compiler (defaults to `False`)
|
||||||
|
"""
|
||||||
|
compiler = 'clang'
|
||||||
|
if with_target:
|
||||||
|
compiler = '{target}-{compiler}'.format(
|
||||||
|
target=self.target, compiler=compiler
|
||||||
|
)
|
||||||
|
if plus_plus:
|
||||||
|
compiler += '++'
|
||||||
|
return join(self.ctx.ndk.llvm_bin_dir, compiler)
|
||||||
|
|
||||||
|
def get_env(self, with_flags_in_cc=True):
|
||||||
env = {}
|
env = {}
|
||||||
|
|
||||||
cflags = [
|
# HOME: User's home directory
|
||||||
'-DANDROID',
|
#
|
||||||
'-fomit-frame-pointer',
|
# Many tools including p4a store outputs in the user's home
|
||||||
'-D__ANDROID_API__={}'.format(self.ctx.ndk_api)]
|
# directory. This is found from the HOME environment variable
|
||||||
if not clang:
|
# and falls back to the system account database. Setting HOME
|
||||||
cflags.append('-mandroid')
|
# can be used to globally divert these tools to use a different
|
||||||
else:
|
# path. Furthermore, in containerized environments the user may
|
||||||
cflags.append('-target ' + self.target)
|
# not exist in the account database, so if HOME isn't set than
|
||||||
toolchain = '{android_host}-{toolchain_version}'.format(
|
# these tools will fail.
|
||||||
android_host=self.ctx.toolchain_prefix,
|
if 'HOME' in environ:
|
||||||
toolchain_version=self.ctx.toolchain_version)
|
env['HOME'] = environ['HOME']
|
||||||
toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain,
|
|
||||||
'prebuilt', build_platform)
|
|
||||||
cflags.append('-gcc-toolchain {}'.format(toolchain))
|
|
||||||
|
|
||||||
env['CFLAGS'] = ' '.join(cflags)
|
# CFLAGS/CXXFLAGS: the processor flags
|
||||||
|
env['CFLAGS'] = ' '.join(self.common_cflags).format(target=self.target)
|
||||||
|
if self.arch_cflags:
|
||||||
|
# each architecture may have has his own CFLAGS
|
||||||
|
env['CFLAGS'] += ' ' + ' '.join(self.arch_cflags)
|
||||||
|
env['CXXFLAGS'] = env['CFLAGS']
|
||||||
|
|
||||||
# Link the extra global link paths first before anything else
|
# CPPFLAGS (for macros and includes)
|
||||||
|
env['CPPFLAGS'] = ' '.join(self.common_cppflags).format(
|
||||||
|
ctx=self.ctx,
|
||||||
|
command_prefix=self.command_prefix,
|
||||||
|
python_includes=join(
|
||||||
|
self.ctx.get_python_install_dir(self.arch),
|
||||||
|
'include/python{}'.format(self.ctx.python_recipe.version[0:3]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# LDFLAGS: Link the extra global link paths first before anything else
|
||||||
# (such that overriding system libraries with them is possible)
|
# (such that overriding system libraries with them is possible)
|
||||||
env['LDFLAGS'] = ' ' + " ".join([
|
env['LDFLAGS'] = (
|
||||||
"-L'" + l.replace("'", "'\"'\"'") + "'" # no shlex.quote in py2
|
' '
|
||||||
for l in self.extra_global_link_paths
|
+ " ".join(
|
||||||
]) + ' '
|
[
|
||||||
|
"-L'"
|
||||||
|
+ link_path.replace("'", "'\"'\"'")
|
||||||
|
+ "'" # no shlex.quote in py2
|
||||||
|
for link_path in self.extra_global_link_paths
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ ' ' + ' '.join(self.common_ldflags).format(
|
||||||
|
ctx_libs_dir=self.ctx.get_libs_dir(self.arch)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
sysroot = join(self.ctx._ndk_dir, 'sysroot')
|
# LDLIBS: Library flags or names given to compilers when they are
|
||||||
if exists(sysroot):
|
# supposed to invoke the linker.
|
||||||
# post-15 NDK per
|
env['LDLIBS'] = ' '.join(self.common_ldlibs)
|
||||||
# https://android.googlesource.com/platform/ndk/+/ndk-r15-release/docs/UnifiedHeaders.md
|
|
||||||
env['CFLAGS'] += ' -isystem {}/sysroot/usr/include/{}'.format(
|
|
||||||
self.ctx.ndk_dir, self.ctx.toolchain_prefix)
|
|
||||||
env['CFLAGS'] += ' -I{}/sysroot/usr/include/{}'.format(
|
|
||||||
self.ctx.ndk_dir, self.command_prefix)
|
|
||||||
else:
|
|
||||||
sysroot = self.ctx.ndk_platform
|
|
||||||
env['CFLAGS'] += ' -I{}'.format(self.ctx.ndk_platform)
|
|
||||||
env['CFLAGS'] += ' -isysroot {} '.format(sysroot)
|
|
||||||
env['CFLAGS'] += '-I' + join(self.ctx.get_python_install_dir(),
|
|
||||||
'include/python{}'.format(
|
|
||||||
self.ctx.python_recipe.version[0:3])
|
|
||||||
)
|
|
||||||
|
|
||||||
env['LDFLAGS'] += '--sysroot={} '.format(self.ctx.ndk_platform)
|
|
||||||
|
|
||||||
env["CXXFLAGS"] = env["CFLAGS"]
|
|
||||||
|
|
||||||
env["LDFLAGS"] += " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)])
|
|
||||||
|
|
||||||
if self.ctx.ndk == 'crystax':
|
|
||||||
env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch)
|
|
||||||
|
|
||||||
toolchain_prefix = self.ctx.toolchain_prefix
|
|
||||||
toolchain_version = self.ctx.toolchain_version
|
|
||||||
command_prefix = self.command_prefix
|
|
||||||
|
|
||||||
env['TOOLCHAIN_PREFIX'] = toolchain_prefix
|
|
||||||
env['TOOLCHAIN_VERSION'] = toolchain_version
|
|
||||||
|
|
||||||
|
# CCACHE
|
||||||
ccache = ''
|
ccache = ''
|
||||||
if self.ctx.ccache and bool(int(environ.get('USE_CCACHE', '1'))):
|
if self.ctx.ccache and bool(int(environ.get('USE_CCACHE', '1'))):
|
||||||
# print('ccache found, will optimize builds')
|
# print('ccache found, will optimize builds')
|
||||||
ccache = self.ctx.ccache + ' '
|
ccache = self.ctx.ccache + ' '
|
||||||
env['USE_CCACHE'] = '1'
|
env['USE_CCACHE'] = '1'
|
||||||
env['NDK_CCACHE'] = self.ctx.ccache
|
env['NDK_CCACHE'] = self.ctx.ccache
|
||||||
env.update({k: v for k, v in environ.items() if k.startswith('CCACHE_')})
|
env.update(
|
||||||
|
{k: v for k, v in environ.items() if k.startswith('CCACHE_')}
|
||||||
|
)
|
||||||
|
|
||||||
if clang:
|
# Compiler: `CC` and `CXX` (and make sure that the compiler exists)
|
||||||
llvm_dirname = split(
|
env['PATH'] = self.ctx.env['PATH']
|
||||||
glob(join(self.ctx.ndk_dir, 'toolchains', 'llvm*'))[-1])[-1]
|
cc = find_executable(self.clang_exe, path=env['PATH'])
|
||||||
clang_path = join(self.ctx.ndk_dir, 'toolchains', llvm_dirname,
|
|
||||||
'prebuilt', build_platform, 'bin')
|
|
||||||
environ['PATH'] = '{clang_path}:{path}'.format(
|
|
||||||
clang_path=clang_path, path=environ['PATH'])
|
|
||||||
exe = join(clang_path, 'clang')
|
|
||||||
execxx = join(clang_path, 'clang++')
|
|
||||||
else:
|
|
||||||
exe = '{command_prefix}-gcc'.format(command_prefix=command_prefix)
|
|
||||||
execxx = '{command_prefix}-g++'.format(command_prefix=command_prefix)
|
|
||||||
|
|
||||||
cc = find_executable(exe, path=environ['PATH'])
|
|
||||||
if cc is None:
|
if cc is None:
|
||||||
print('Searching path are: {!r}'.format(environ['PATH']))
|
print('Searching path are: {!r}'.format(env['PATH']))
|
||||||
raise BuildInterruptingException(
|
raise BuildInterruptingException(
|
||||||
'Couldn\'t find executable for CC. This indicates a '
|
'Couldn\'t find executable for CC. This indicates a '
|
||||||
'problem locating the {} executable in the Android '
|
'problem locating the {} executable in the Android '
|
||||||
'NDK, not that you don\'t have a normal compiler '
|
'NDK, not that you don\'t have a normal compiler '
|
||||||
'installed. Exiting.'.format(exe))
|
'installed. Exiting.'.format(self.clang_exe))
|
||||||
|
|
||||||
if with_flags_in_cc:
|
if with_flags_in_cc:
|
||||||
env['CC'] = '{ccache}{exe} {cflags}'.format(
|
env['CC'] = '{ccache}{exe} {cflags}'.format(
|
||||||
exe=exe,
|
exe=self.clang_exe,
|
||||||
ccache=ccache,
|
ccache=ccache,
|
||||||
cflags=env['CFLAGS'])
|
cflags=env['CFLAGS'])
|
||||||
env['CXX'] = '{ccache}{execxx} {cxxflags}'.format(
|
env['CXX'] = '{ccache}{execxx} {cxxflags}'.format(
|
||||||
execxx=execxx,
|
execxx=self.clang_exe_cxx,
|
||||||
ccache=ccache,
|
ccache=ccache,
|
||||||
cxxflags=env['CXXFLAGS'])
|
cxxflags=env['CXXFLAGS'])
|
||||||
else:
|
else:
|
||||||
env['CC'] = '{ccache}{exe}'.format(
|
env['CC'] = '{ccache}{exe}'.format(
|
||||||
exe=exe,
|
exe=self.clang_exe,
|
||||||
ccache=ccache)
|
ccache=ccache)
|
||||||
env['CXX'] = '{ccache}{execxx}'.format(
|
env['CXX'] = '{ccache}{execxx}'.format(
|
||||||
execxx=execxx,
|
execxx=self.clang_exe_cxx,
|
||||||
ccache=ccache)
|
ccache=ccache)
|
||||||
|
|
||||||
env['AR'] = '{}-ar'.format(command_prefix)
|
# Android's LLVM binutils
|
||||||
env['RANLIB'] = '{}-ranlib'.format(command_prefix)
|
env['AR'] = self.ctx.ndk.llvm_ar
|
||||||
env['LD'] = '{}-ld'.format(command_prefix)
|
env['RANLIB'] = self.ctx.ndk.llvm_ranlib
|
||||||
env['LDSHARED'] = env["CC"] + " -pthread -shared " +\
|
env['STRIP'] = f'{self.ctx.ndk.llvm_strip} --strip-unneeded'
|
||||||
"-Wl,-O1 -Wl,-Bsymbolic-functions "
|
env['READELF'] = self.ctx.ndk.llvm_readelf
|
||||||
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
|
env['OBJCOPY'] = self.ctx.ndk.llvm_objcopy
|
||||||
# For crystax python, we can't use the host python headers:
|
|
||||||
env["CFLAGS"] += ' -I{}/sources/python/{}/include/python/'.\
|
|
||||||
format(self.ctx.ndk_dir, self.ctx.python_recipe.version[0:3])
|
|
||||||
env['STRIP'] = '{}-strip --strip-unneeded'.format(command_prefix)
|
|
||||||
env['MAKE'] = 'make -j5'
|
|
||||||
env['READELF'] = '{}-readelf'.format(command_prefix)
|
|
||||||
env['NM'] = '{}-nm'.format(command_prefix)
|
|
||||||
|
|
||||||
|
env['MAKE'] = 'make -j{}'.format(str(cpu_count()))
|
||||||
|
|
||||||
|
# Android's arch/toolchain
|
||||||
|
env['ARCH'] = self.arch
|
||||||
|
env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api))
|
||||||
|
|
||||||
|
# Custom linker options
|
||||||
|
env['LDSHARED'] = env['CC'] + ' ' + ' '.join(self.common_ldshared)
|
||||||
|
|
||||||
|
# Host python (used by some recipes)
|
||||||
hostpython_recipe = Recipe.get_recipe(
|
hostpython_recipe = Recipe.get_recipe(
|
||||||
'host' + self.ctx.python_recipe.name, self.ctx)
|
'host' + self.ctx.python_recipe.name, self.ctx)
|
||||||
env['BUILDLIB_PATH'] = join(
|
env['BUILDLIB_PATH'] = join(
|
||||||
hostpython_recipe.get_build_dir(self.arch),
|
hostpython_recipe.get_build_dir(self.arch),
|
||||||
'build', 'lib.{}-{}'.format(
|
'native-build',
|
||||||
build_platform, self.ctx.python_recipe.major_minor_version_string)
|
'build',
|
||||||
|
'lib.{}-{}'.format(
|
||||||
|
build_platform,
|
||||||
|
self.ctx.python_recipe.major_minor_version_string,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
env['PATH'] = environ['PATH']
|
# for reproducible builds
|
||||||
|
if 'SOURCE_DATE_EPOCH' in environ:
|
||||||
env['ARCH'] = self.arch
|
for k in 'LC_ALL TZ SOURCE_DATE_EPOCH PYTHONHASHSEED BUILD_DATE BUILD_TIME'.split():
|
||||||
env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api))
|
if k in environ:
|
||||||
|
env[k] = environ[k]
|
||||||
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
|
|
||||||
env['CRYSTAX_PYTHON_VERSION'] = self.ctx.python_recipe.version
|
|
||||||
|
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
class ArchARM(Arch):
|
class ArchARM(Arch):
|
||||||
arch = "armeabi"
|
arch = "armeabi"
|
||||||
toolchain_prefix = 'arm-linux-androideabi'
|
|
||||||
command_prefix = 'arm-linux-androideabi'
|
command_prefix = 'arm-linux-androideabi'
|
||||||
platform_dir = 'arch-arm'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self):
|
def target(self):
|
||||||
target_data = self.command_prefix.split('-')
|
target_data = self.command_prefix.split('-')
|
||||||
return '-'.join(
|
return '{triplet}{ndk_api}'.format(
|
||||||
['armv7a', 'none', target_data[1], target_data[2]])
|
triplet='-'.join(['armv7a', target_data[1], target_data[2]]),
|
||||||
|
ndk_api=self.ctx.ndk_api,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArchARMv7_a(ArchARM):
|
class ArchARMv7_a(ArchARM):
|
||||||
arch = 'armeabi-v7a'
|
arch = 'armeabi-v7a'
|
||||||
|
arch_cflags = [
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
'-march=armv7-a',
|
||||||
env = super(ArchARMv7_a, self).get_env(with_flags_in_cc, clang=clang)
|
'-mfloat-abi=softfp',
|
||||||
env['CFLAGS'] = (env['CFLAGS'] +
|
'-mfpu=vfp',
|
||||||
(' -march=armv7-a -mfloat-abi=softfp '
|
'-mthumb',
|
||||||
'-mfpu=vfp -mthumb'))
|
'-fPIC',
|
||||||
env['CXXFLAGS'] = env['CFLAGS']
|
]
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
class Archx86(Arch):
|
class Archx86(Arch):
|
||||||
arch = 'x86'
|
arch = 'x86'
|
||||||
toolchain_prefix = 'x86'
|
|
||||||
command_prefix = 'i686-linux-android'
|
command_prefix = 'i686-linux-android'
|
||||||
platform_dir = 'arch-x86'
|
arch_cflags = [
|
||||||
|
'-march=i686',
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
'-mssse3',
|
||||||
env = super(Archx86, self).get_env(with_flags_in_cc, clang=clang)
|
'-mfpmath=sse',
|
||||||
env['CFLAGS'] = (env['CFLAGS'] +
|
'-m32',
|
||||||
' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32')
|
'-fPIC',
|
||||||
env['CXXFLAGS'] = env['CFLAGS']
|
]
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
class Archx86_64(Arch):
|
class Archx86_64(Arch):
|
||||||
arch = 'x86_64'
|
arch = 'x86_64'
|
||||||
toolchain_prefix = 'x86_64'
|
|
||||||
command_prefix = 'x86_64-linux-android'
|
command_prefix = 'x86_64-linux-android'
|
||||||
platform_dir = 'arch-x86_64'
|
arch_cflags = [
|
||||||
|
'-march=x86-64',
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
'-msse4.2',
|
||||||
env = super(Archx86_64, self).get_env(with_flags_in_cc, clang=clang)
|
'-mpopcnt',
|
||||||
env['CFLAGS'] = (env['CFLAGS'] +
|
'-m64',
|
||||||
' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel')
|
'-fPIC',
|
||||||
env['CXXFLAGS'] = env['CFLAGS']
|
]
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
class ArchAarch_64(Arch):
|
class ArchAarch_64(Arch):
|
||||||
arch = 'arm64-v8a'
|
arch = 'arm64-v8a'
|
||||||
toolchain_prefix = 'aarch64-linux-android'
|
|
||||||
command_prefix = 'aarch64-linux-android'
|
command_prefix = 'aarch64-linux-android'
|
||||||
platform_dir = 'arch-arm64'
|
arch_cflags = [
|
||||||
|
'-march=armv8-a',
|
||||||
|
'-fPIC'
|
||||||
|
# '-I' + join(dirname(__file__), 'includes', 'arm64-v8a'),
|
||||||
|
]
|
||||||
|
|
||||||
def get_env(self, with_flags_in_cc=True, clang=False):
|
# Note: This `EXTRA_CFLAGS` below should target the commented `include`
|
||||||
env = super(ArchAarch_64, self).get_env(with_flags_in_cc, clang=clang)
|
# above in `arch_cflags`. The original lines were added during the Sdl2's
|
||||||
incpath = ' -I' + join(dirname(__file__), 'includes', 'arm64-v8a')
|
# bootstrap creation, and modified/commented during the migration to the
|
||||||
env['EXTRA_CFLAGS'] = incpath
|
# NDK r19 build system, because it seems that we don't need it anymore,
|
||||||
env['CFLAGS'] += incpath
|
# do we need them?
|
||||||
env['CXXFLAGS'] += incpath
|
# def get_env(self, with_flags_in_cc=True):
|
||||||
if with_flags_in_cc:
|
# env = super().get_env(with_flags_in_cc)
|
||||||
env['CC'] += incpath
|
# env['EXTRA_CFLAGS'] = self.arch_cflags[-1]
|
||||||
env['CXX'] += incpath
|
# return env
|
||||||
return env
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
from __future__ import print_function
|
|
||||||
from setuptools import Command
|
from setuptools import Command
|
||||||
from pythonforandroid import toolchain
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from os.path import realpath, join, exists, dirname, curdir, basename, split
|
from os.path import realpath, join, exists, dirname, curdir, basename, split
|
||||||
|
@ -16,16 +14,16 @@ def argv_contains(t):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BdistAPK(Command):
|
class Bdist(Command):
|
||||||
description = 'Create an APK with python-for-android'
|
|
||||||
|
|
||||||
user_options = []
|
user_options = []
|
||||||
|
package_type = None
|
||||||
|
|
||||||
def initialize_options(self):
|
def initialize_options(self):
|
||||||
for option in self.user_options:
|
for option in self.user_options:
|
||||||
setattr(self, option[0].strip('=').replace('-', '_'), None)
|
setattr(self, option[0].strip('=').replace('-', '_'), None)
|
||||||
|
|
||||||
option_dict = self.distribution.get_option_dict('apk')
|
option_dict = self.distribution.get_option_dict(self.package_type)
|
||||||
|
|
||||||
# This is a hack, we probably aren't supposed to loop through
|
# This is a hack, we probably aren't supposed to loop through
|
||||||
# the option_dict so early because distutils does exactly the
|
# the option_dict so early because distutils does exactly the
|
||||||
|
@ -34,10 +32,9 @@ class BdistAPK(Command):
|
||||||
for (option, (source, value)) in option_dict.items():
|
for (option, (source, value)) in option_dict.items():
|
||||||
setattr(self, option, str(value))
|
setattr(self, option, str(value))
|
||||||
|
|
||||||
|
|
||||||
def finalize_options(self):
|
def finalize_options(self):
|
||||||
|
|
||||||
setup_options = self.distribution.get_option_dict('apk')
|
setup_options = self.distribution.get_option_dict(self.package_type)
|
||||||
for (option, (source, value)) in setup_options.items():
|
for (option, (source, value)) in setup_options.items():
|
||||||
if source == 'command line':
|
if source == 'command line':
|
||||||
continue
|
continue
|
||||||
|
@ -70,16 +67,15 @@ class BdistAPK(Command):
|
||||||
sys.argv.append('--version={}'.format(version))
|
sys.argv.append('--version={}'.format(version))
|
||||||
|
|
||||||
if not argv_contains('--arch'):
|
if not argv_contains('--arch'):
|
||||||
arch = 'arm64-v8a'
|
arch = 'armeabi-v7a'
|
||||||
self.arch = arch
|
self.arch = arch
|
||||||
sys.argv.append('--arch={}'.format(arch))
|
sys.argv.append('--arch={}'.format(arch))
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|
||||||
self.prepare_build_dir()
|
self.prepare_build_dir()
|
||||||
|
|
||||||
from pythonforandroid.toolchain import main
|
from pythonforandroid.entrypoints import main
|
||||||
sys.argv[1] = 'apk'
|
sys.argv[1] = self.package_type
|
||||||
main()
|
main()
|
||||||
|
|
||||||
def prepare_build_dir(self):
|
def prepare_build_dir(self):
|
||||||
|
@ -112,7 +108,7 @@ class BdistAPK(Command):
|
||||||
makedirs(new_dir)
|
makedirs(new_dir)
|
||||||
print('Including {}'.format(filen))
|
print('Including {}'.format(filen))
|
||||||
copyfile(filen, join(bdist_dir, filen))
|
copyfile(filen, join(bdist_dir, filen))
|
||||||
if basename(filen) in ('main.py', 'main.pyo'):
|
if basename(filen) in ('main.py', 'main.pyc'):
|
||||||
main_py_dirs.append(filen)
|
main_py_dirs.append(filen)
|
||||||
|
|
||||||
# This feels ridiculous, but how else to define the main.py dir?
|
# This feels ridiculous, but how else to define the main.py dir?
|
||||||
|
@ -123,7 +119,7 @@ class BdistAPK(Command):
|
||||||
exit(1)
|
exit(1)
|
||||||
if len(main_py_dirs) > 1:
|
if len(main_py_dirs) > 1:
|
||||||
print('WARNING: Multiple main.py dirs found, using the shortest path')
|
print('WARNING: Multiple main.py dirs found, using the shortest path')
|
||||||
main_py_dirs.sort(key=lambda j: len(split(j)))
|
main_py_dirs = sorted(main_py_dirs, key=lambda j: len(split(j)))
|
||||||
|
|
||||||
if not argv_contains('--launcher'):
|
if not argv_contains('--launcher'):
|
||||||
sys.argv.append('--private={}'.format(
|
sys.argv.append('--private={}'.format(
|
||||||
|
@ -131,18 +127,39 @@ class BdistAPK(Command):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BdistAPK(Bdist):
|
||||||
|
"""distutil command handler for 'apk'."""
|
||||||
|
description = 'Create an APK with python-for-android'
|
||||||
|
package_type = 'apk'
|
||||||
|
|
||||||
|
|
||||||
|
class BdistAAR(Bdist):
|
||||||
|
"""distutil command handler for 'aar'."""
|
||||||
|
description = 'Create an AAR with python-for-android'
|
||||||
|
package_type = 'aar'
|
||||||
|
|
||||||
|
|
||||||
|
class BdistAAB(Bdist):
|
||||||
|
"""distutil command handler for 'aab'."""
|
||||||
|
description = 'Create an AAB with python-for-android'
|
||||||
|
package_type = 'aab'
|
||||||
|
|
||||||
|
|
||||||
def _set_user_options():
|
def _set_user_options():
|
||||||
# This seems like a silly way to do things, but not sure if there's a
|
# This seems like a silly way to do things, but not sure if there's a
|
||||||
# better way to pass arbitrary options onwards to p4a
|
# better way to pass arbitrary options onwards to p4a
|
||||||
user_options = [('requirements=', None, None),]
|
user_options = [('requirements=', None, None), ]
|
||||||
for i, arg in enumerate(sys.argv):
|
for i, arg in enumerate(sys.argv):
|
||||||
if arg.startswith('--'):
|
if arg.startswith('--'):
|
||||||
if ('=' in arg or
|
if ('=' in arg or
|
||||||
(i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))):
|
(i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))):
|
||||||
user_options.append((arg[2:].split('=')[0] + '=', None, None))
|
user_options.append((arg[2:].split('=')[0] + '=', None, None))
|
||||||
else:
|
else:
|
||||||
user_options.append((arg[2:], None, None))
|
user_options.append((arg[2:], None, None))
|
||||||
|
|
||||||
BdistAPK.user_options = user_options
|
BdistAPK.user_options = user_options
|
||||||
|
BdistAAB.user_options = user_options
|
||||||
|
BdistAAR.user_options = user_options
|
||||||
|
|
||||||
|
|
||||||
_set_user_options()
|
_set_user_options()
|
||||||
|
|
259
p4a/pythonforandroid/bootstrap.py
Normal file → Executable file
|
@ -1,20 +1,20 @@
|
||||||
|
import functools
|
||||||
|
import glob
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
from os.path import (join, dirname, isdir, normpath, splitext, basename)
|
from os.path import (join, dirname, isdir, normpath, splitext, basename)
|
||||||
from os import listdir, walk, sep
|
from os import listdir, walk, sep
|
||||||
import sh
|
import sh
|
||||||
import shlex
|
import shlex
|
||||||
import glob
|
|
||||||
import importlib
|
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pythonforandroid.logger import (warning, shprint, info, logger,
|
from pythonforandroid.logger import (shprint, info, logger, debug)
|
||||||
debug)
|
from pythonforandroid.util import (
|
||||||
from pythonforandroid.util import (current_directory, ensure_dir,
|
current_directory, ensure_dir, temp_directory, BuildInterruptingException)
|
||||||
temp_directory)
|
|
||||||
from pythonforandroid.recipe import Recipe
|
from pythonforandroid.recipe import Recipe
|
||||||
|
|
||||||
|
|
||||||
def copy_files(src_root, dest_root, override=True):
|
def copy_files(src_root, dest_root, override=True, symlink=False):
|
||||||
for root, dirnames, filenames in walk(src_root):
|
for root, dirnames, filenames in walk(src_root):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
subdir = normpath(root.replace(src_root, ""))
|
subdir = normpath(root.replace(src_root, ""))
|
||||||
|
@ -29,12 +29,44 @@ def copy_files(src_root, dest_root, override=True):
|
||||||
if override and os.path.exists(dest_file):
|
if override and os.path.exists(dest_file):
|
||||||
os.unlink(dest_file)
|
os.unlink(dest_file)
|
||||||
if not os.path.exists(dest_file):
|
if not os.path.exists(dest_file):
|
||||||
shutil.copy(src_file, dest_file)
|
if symlink:
|
||||||
|
os.symlink(src_file, dest_file)
|
||||||
|
else:
|
||||||
|
shutil.copy(src_file, dest_file)
|
||||||
else:
|
else:
|
||||||
os.makedirs(dest_file)
|
os.makedirs(dest_file)
|
||||||
|
|
||||||
|
|
||||||
class Bootstrap(object):
|
default_recipe_priorities = [
|
||||||
|
"webview", "sdl2", "service_only" # last is highest
|
||||||
|
]
|
||||||
|
# ^^ NOTE: these are just the default priorities if no special rules
|
||||||
|
# apply (which you can find in the code below), so basically if no
|
||||||
|
# known graphical lib or web lib is used - in which case service_only
|
||||||
|
# is the most reasonable guess.
|
||||||
|
|
||||||
|
|
||||||
|
def _cmp_bootstraps_by_priority(a, b):
|
||||||
|
def rank_bootstrap(bootstrap):
|
||||||
|
""" Returns a ranking index for each bootstrap,
|
||||||
|
with higher priority ranked with higher number. """
|
||||||
|
if bootstrap.name in default_recipe_priorities:
|
||||||
|
return default_recipe_priorities.index(bootstrap.name) + 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Rank bootstraps in order:
|
||||||
|
rank_a = rank_bootstrap(a)
|
||||||
|
rank_b = rank_bootstrap(b)
|
||||||
|
if rank_a != rank_b:
|
||||||
|
return (rank_b - rank_a)
|
||||||
|
else:
|
||||||
|
if a.name < b.name: # alphabetic sort for determinism
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class Bootstrap:
|
||||||
'''An Android project template, containing recipe stuff for
|
'''An Android project template, containing recipe stuff for
|
||||||
compilation and templated fields for APK info.
|
compilation and templated fields for APK info.
|
||||||
'''
|
'''
|
||||||
|
@ -45,15 +77,11 @@ class Bootstrap(object):
|
||||||
bootstrap_dir = None
|
bootstrap_dir = None
|
||||||
|
|
||||||
build_dir = None
|
build_dir = None
|
||||||
dist_dir = None
|
|
||||||
dist_name = None
|
dist_name = None
|
||||||
distribution = None
|
distribution = None
|
||||||
|
|
||||||
# All bootstraps should include Python in some way:
|
# All bootstraps should include Python in some way:
|
||||||
recipe_depends = [
|
recipe_depends = ['python3', 'android']
|
||||||
("python2", "python2legacy", "python3", "python3crystax"),
|
|
||||||
'android',
|
|
||||||
]
|
|
||||||
|
|
||||||
can_be_chosen_automatically = True
|
can_be_chosen_automatically = True
|
||||||
'''Determines whether the bootstrap can be chosen as one that
|
'''Determines whether the bootstrap can be chosen as one that
|
||||||
|
@ -70,9 +98,9 @@ class Bootstrap(object):
|
||||||
def dist_dir(self):
|
def dist_dir(self):
|
||||||
'''The dist dir at which to place the finished distribution.'''
|
'''The dist dir at which to place the finished distribution.'''
|
||||||
if self.distribution is None:
|
if self.distribution is None:
|
||||||
warning('Tried to access {}.dist_dir, but {}.distribution '
|
raise BuildInterruptingException(
|
||||||
'is None'.format(self, self))
|
'Internal error: tried to access {}.dist_dir, but {}.distribution '
|
||||||
exit(1)
|
'is None'.format(self, self))
|
||||||
return self.distribution.dist_dir
|
return self.distribution.dist_dir
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -84,7 +112,7 @@ class Bootstrap(object):
|
||||||
and optional dependencies are being used,
|
and optional dependencies are being used,
|
||||||
and returns a list of these.'''
|
and returns a list of these.'''
|
||||||
recipes = []
|
recipes = []
|
||||||
built_recipes = self.ctx.recipe_build_order
|
built_recipes = self.ctx.recipe_build_order or []
|
||||||
for recipe in self.recipe_depends:
|
for recipe in self.recipe_depends:
|
||||||
if isinstance(recipe, (tuple, list)):
|
if isinstance(recipe, (tuple, list)):
|
||||||
for alternative in recipe:
|
for alternative in recipe:
|
||||||
|
@ -104,70 +132,102 @@ class Bootstrap(object):
|
||||||
def get_dist_dir(self, name):
|
def get_dist_dir(self, name):
|
||||||
return join(self.ctx.dist_dir, name)
|
return join(self.ctx.dist_dir, name)
|
||||||
|
|
||||||
def get_common_dir(self):
|
|
||||||
return os.path.abspath(join(self.bootstrap_dir, "..", 'common'))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
modname = self.__class__.__module__
|
modname = self.__class__.__module__
|
||||||
return modname.split(".", 2)[-1]
|
return modname.split(".", 2)[-1]
|
||||||
|
|
||||||
|
def get_bootstrap_dirs(self):
|
||||||
|
"""get all bootstrap directories, following the MRO path"""
|
||||||
|
|
||||||
|
# get all bootstrap names along the __mro__, cutting off Bootstrap and object
|
||||||
|
classes = self.__class__.__mro__[:-2]
|
||||||
|
bootstrap_names = [cls.name for cls in classes] + ['common']
|
||||||
|
bootstrap_dirs = [
|
||||||
|
join(self.ctx.root_dir, 'bootstraps', bootstrap_name)
|
||||||
|
for bootstrap_name in reversed(bootstrap_names)
|
||||||
|
]
|
||||||
|
return bootstrap_dirs
|
||||||
|
|
||||||
|
def _copy_in_final_files(self):
|
||||||
|
if self.name == "sdl2":
|
||||||
|
# Get the paths for copying SDL2's java source code:
|
||||||
|
sdl2_recipe = Recipe.get_recipe("sdl2", self.ctx)
|
||||||
|
sdl2_build_dir = sdl2_recipe.get_jni_dir()
|
||||||
|
src_dir = join(sdl2_build_dir, "SDL", "android-project",
|
||||||
|
"app", "src", "main", "java",
|
||||||
|
"org", "libsdl", "app")
|
||||||
|
target_dir = join(self.dist_dir, 'src', 'main', 'java', 'org',
|
||||||
|
'libsdl', 'app')
|
||||||
|
|
||||||
|
# Do actual copying:
|
||||||
|
info('Copying in SDL2 .java files from: ' + str(src_dir))
|
||||||
|
if not os.path.exists(target_dir):
|
||||||
|
os.makedirs(target_dir)
|
||||||
|
copy_files(src_dir, target_dir, override=True)
|
||||||
|
|
||||||
def prepare_build_dir(self):
|
def prepare_build_dir(self):
|
||||||
'''Ensure that a build dir exists for the recipe. This same single
|
"""Ensure that a build dir exists for the recipe. This same single
|
||||||
dir will be used for building all different archs.'''
|
dir will be used for building all different archs."""
|
||||||
|
bootstrap_dirs = self.get_bootstrap_dirs()
|
||||||
|
# now do a cumulative copy of all bootstrap dirs
|
||||||
self.build_dir = self.get_build_dir()
|
self.build_dir = self.get_build_dir()
|
||||||
self.common_dir = self.get_common_dir()
|
for bootstrap_dir in bootstrap_dirs:
|
||||||
copy_files(join(self.bootstrap_dir, 'build'), self.build_dir)
|
copy_files(join(bootstrap_dir, 'build'), self.build_dir, symlink=self.ctx.symlink_bootstrap_files)
|
||||||
copy_files(join(self.common_dir, 'build'), self.build_dir,
|
|
||||||
override=False)
|
|
||||||
if self.ctx.symlink_java_src:
|
|
||||||
info('Symlinking java src instead of copying')
|
|
||||||
shprint(sh.rm, '-r', join(self.build_dir, 'src'))
|
|
||||||
shprint(sh.mkdir, join(self.build_dir, 'src'))
|
|
||||||
for dirn in listdir(join(self.bootstrap_dir, 'build', 'src')):
|
|
||||||
shprint(sh.ln, '-s', join(self.bootstrap_dir, 'build', 'src', dirn),
|
|
||||||
join(self.build_dir, 'src'))
|
|
||||||
with current_directory(self.build_dir):
|
with current_directory(self.build_dir):
|
||||||
with open('project.properties', 'w') as fileh:
|
with open('project.properties', 'w') as fileh:
|
||||||
fileh.write('target=android-{}'.format(self.ctx.android_api))
|
fileh.write('target=android-{}'.format(self.ctx.android_api))
|
||||||
|
|
||||||
def prepare_dist_dir(self, name):
|
def prepare_dist_dir(self):
|
||||||
ensure_dir(self.dist_dir)
|
ensure_dir(self.dist_dir)
|
||||||
|
|
||||||
def run_distribute(self):
|
def assemble_distribution(self):
|
||||||
|
''' Copies all the files into the distribution (this function is
|
||||||
|
overridden by the specific bootstrap classes to do this)
|
||||||
|
and add in the distribution info.
|
||||||
|
'''
|
||||||
|
self._copy_in_final_files()
|
||||||
self.distribution.save_info(self.dist_dir)
|
self.distribution.save_info(self.dist_dir)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list_bootstraps(cls):
|
def all_bootstraps(cls):
|
||||||
'''Find all the available bootstraps and return them.'''
|
'''Find all the available bootstraps and return them.'''
|
||||||
forbidden_dirs = ('__pycache__', 'common')
|
forbidden_dirs = ('__pycache__', 'common')
|
||||||
bootstraps_dir = join(dirname(__file__), 'bootstraps')
|
bootstraps_dir = join(dirname(__file__), 'bootstraps')
|
||||||
|
result = set()
|
||||||
for name in listdir(bootstraps_dir):
|
for name in listdir(bootstraps_dir):
|
||||||
if name in forbidden_dirs:
|
if name in forbidden_dirs:
|
||||||
continue
|
continue
|
||||||
filen = join(bootstraps_dir, name)
|
filen = join(bootstraps_dir, name)
|
||||||
if isdir(filen):
|
if isdir(filen):
|
||||||
yield name
|
result.add(name)
|
||||||
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_bootstrap_from_recipes(cls, recipes, ctx):
|
def get_usable_bootstraps_for_recipes(cls, recipes, ctx):
|
||||||
'''Returns a bootstrap whose recipe requirements do not conflict with
|
'''Returns all bootstrap whose recipe requirements do not conflict
|
||||||
the given recipes.'''
|
with the given recipes, in no particular order.'''
|
||||||
info('Trying to find a bootstrap that matches the given recipes.')
|
info('Trying to find a bootstrap that matches the given recipes.')
|
||||||
bootstraps = [cls.get_bootstrap(name, ctx)
|
bootstraps = [cls.get_bootstrap(name, ctx)
|
||||||
for name in cls.list_bootstraps()]
|
for name in cls.all_bootstraps()]
|
||||||
acceptable_bootstraps = []
|
acceptable_bootstraps = set()
|
||||||
|
|
||||||
|
# Find out which bootstraps are acceptable:
|
||||||
for bs in bootstraps:
|
for bs in bootstraps:
|
||||||
if not bs.can_be_chosen_automatically:
|
if not bs.can_be_chosen_automatically:
|
||||||
continue
|
continue
|
||||||
possible_dependency_lists = expand_dependencies(bs.recipe_depends)
|
possible_dependency_lists = expand_dependencies(bs.recipe_depends, ctx)
|
||||||
for possible_dependencies in possible_dependency_lists:
|
for possible_dependencies in possible_dependency_lists:
|
||||||
ok = True
|
ok = True
|
||||||
|
# Check if the bootstap's dependencies have an internal conflict:
|
||||||
for recipe in possible_dependencies:
|
for recipe in possible_dependencies:
|
||||||
recipe = Recipe.get_recipe(recipe, ctx)
|
recipe = Recipe.get_recipe(recipe, ctx)
|
||||||
if any([conflict in recipes for conflict in recipe.conflicts]):
|
if any(conflict in recipes for conflict in recipe.conflicts):
|
||||||
ok = False
|
ok = False
|
||||||
break
|
break
|
||||||
|
# Check if bootstrap's dependencies conflict with chosen
|
||||||
|
# packages:
|
||||||
for recipe in recipes:
|
for recipe in recipes:
|
||||||
try:
|
try:
|
||||||
recipe = Recipe.get_recipe(recipe, ctx)
|
recipe = Recipe.get_recipe(recipe, ctx)
|
||||||
|
@ -175,19 +235,63 @@ class Bootstrap(object):
|
||||||
conflicts = []
|
conflicts = []
|
||||||
else:
|
else:
|
||||||
conflicts = recipe.conflicts
|
conflicts = recipe.conflicts
|
||||||
if any([conflict in possible_dependencies
|
if any(conflict in possible_dependencies
|
||||||
for conflict in conflicts]):
|
for conflict in conflicts):
|
||||||
ok = False
|
ok = False
|
||||||
break
|
break
|
||||||
if ok and bs not in acceptable_bootstraps:
|
if ok and bs not in acceptable_bootstraps:
|
||||||
acceptable_bootstraps.append(bs)
|
acceptable_bootstraps.add(bs)
|
||||||
|
|
||||||
info('Found {} acceptable bootstraps: {}'.format(
|
info('Found {} acceptable bootstraps: {}'.format(
|
||||||
len(acceptable_bootstraps),
|
len(acceptable_bootstraps),
|
||||||
[bs.name for bs in acceptable_bootstraps]))
|
[bs.name for bs in acceptable_bootstraps]))
|
||||||
if acceptable_bootstraps:
|
return acceptable_bootstraps
|
||||||
info('Using the first of these: {}'
|
|
||||||
.format(acceptable_bootstraps[0].name))
|
@classmethod
|
||||||
return acceptable_bootstraps[0]
|
def get_bootstrap_from_recipes(cls, recipes, ctx):
|
||||||
|
'''Picks a single recommended default bootstrap out of
|
||||||
|
all_usable_bootstraps_from_recipes() for the given reicpes,
|
||||||
|
and returns it.'''
|
||||||
|
|
||||||
|
known_web_packages = {"flask"} # to pick webview over service_only
|
||||||
|
recipes_with_deps_lists = expand_dependencies(recipes, ctx)
|
||||||
|
acceptable_bootstraps = cls.get_usable_bootstraps_for_recipes(
|
||||||
|
recipes, ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
def have_dependency_in_recipes(dep):
|
||||||
|
for dep_list in recipes_with_deps_lists:
|
||||||
|
if dep in dep_list:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Special rule: return SDL2 bootstrap if there's an sdl2 dep:
|
||||||
|
if (have_dependency_in_recipes("sdl2") and
|
||||||
|
"sdl2" in [b.name for b in acceptable_bootstraps]
|
||||||
|
):
|
||||||
|
info('Using sdl2 bootstrap since it is in dependencies')
|
||||||
|
return cls.get_bootstrap("sdl2", ctx)
|
||||||
|
|
||||||
|
# Special rule: return "webview" if we depend on common web recipe:
|
||||||
|
for possible_web_dep in known_web_packages:
|
||||||
|
if have_dependency_in_recipes(possible_web_dep):
|
||||||
|
# We have a web package dep!
|
||||||
|
if "webview" in [b.name for b in acceptable_bootstraps]:
|
||||||
|
info('Using webview bootstrap since common web packages '
|
||||||
|
'were found {}'.format(
|
||||||
|
known_web_packages.intersection(recipes)
|
||||||
|
))
|
||||||
|
return cls.get_bootstrap("webview", ctx)
|
||||||
|
|
||||||
|
prioritized_acceptable_bootstraps = sorted(
|
||||||
|
list(acceptable_bootstraps),
|
||||||
|
key=functools.cmp_to_key(_cmp_bootstraps_by_priority)
|
||||||
|
)
|
||||||
|
|
||||||
|
if prioritized_acceptable_bootstraps:
|
||||||
|
info('Using the highest ranked/first of these: {}'
|
||||||
|
.format(prioritized_acceptable_bootstraps[0].name))
|
||||||
|
return prioritized_acceptable_bootstraps[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -218,15 +322,16 @@ class Bootstrap(object):
|
||||||
tgt_dir = join(dest_dir, arch.arch)
|
tgt_dir = join(dest_dir, arch.arch)
|
||||||
ensure_dir(tgt_dir)
|
ensure_dir(tgt_dir)
|
||||||
for src_dir in src_dirs:
|
for src_dir in src_dirs:
|
||||||
for lib in glob.glob(join(src_dir, wildcard)):
|
libs = glob.glob(join(src_dir, wildcard))
|
||||||
shprint(sh.cp, '-a', lib, tgt_dir)
|
if libs:
|
||||||
|
shprint(sh.cp, '-a', *libs, tgt_dir)
|
||||||
|
|
||||||
def distribute_javaclasses(self, javaclass_dir, dest_dir="src"):
|
def distribute_javaclasses(self, javaclass_dir, dest_dir="src"):
|
||||||
'''Copy existing javaclasses from build dir to current dist dir.'''
|
'''Copy existing javaclasses from build dir to current dist dir.'''
|
||||||
info('Copying java files')
|
info('Copying java files')
|
||||||
ensure_dir(dest_dir)
|
ensure_dir(dest_dir)
|
||||||
for filename in glob.glob(javaclass_dir):
|
filenames = glob.glob(javaclass_dir)
|
||||||
shprint(sh.cp, '-a', filename, dest_dir)
|
shprint(sh.cp, '-a', *filenames, dest_dir)
|
||||||
|
|
||||||
def distribute_aars(self, arch):
|
def distribute_aars(self, arch):
|
||||||
'''Process existing .aar bundles and copy to current dist dir.'''
|
'''Process existing .aar bundles and copy to current dist dir.'''
|
||||||
|
@ -259,24 +364,18 @@ class Bootstrap(object):
|
||||||
debug(" to {}".format(so_tgt_dir))
|
debug(" to {}".format(so_tgt_dir))
|
||||||
ensure_dir(so_tgt_dir)
|
ensure_dir(so_tgt_dir)
|
||||||
so_files = glob.glob(join(so_src_dir, '*.so'))
|
so_files = glob.glob(join(so_src_dir, '*.so'))
|
||||||
for f in so_files:
|
shprint(sh.cp, '-a', *so_files, so_tgt_dir)
|
||||||
shprint(sh.cp, '-a', f, so_tgt_dir)
|
|
||||||
|
|
||||||
def strip_libraries(self, arch):
|
def strip_libraries(self, arch):
|
||||||
info('Stripping libraries')
|
info('Stripping libraries')
|
||||||
if self.ctx.python_recipe.from_crystax:
|
|
||||||
info('Python was loaded from CrystaX, skipping strip')
|
|
||||||
return
|
|
||||||
env = arch.get_env()
|
env = arch.get_env()
|
||||||
tokens = shlex.split(env['STRIP'])
|
tokens = shlex.split(env['STRIP'])
|
||||||
strip = sh.Command(tokens[0])
|
strip = sh.Command(tokens[0])
|
||||||
if len(tokens) > 1:
|
if len(tokens) > 1:
|
||||||
strip = strip.bake(tokens[1:])
|
strip = strip.bake(tokens[1:])
|
||||||
|
|
||||||
libs_dir = join(self.dist_dir, '_python_bundle',
|
libs_dir = join(self.dist_dir, f'_python_bundle__{arch.arch}',
|
||||||
'_python_bundle', 'modules')
|
'_python_bundle', 'modules')
|
||||||
if self.ctx.python_recipe.name == 'python2legacy':
|
|
||||||
libs_dir = join(self.dist_dir, 'private')
|
|
||||||
filens = shprint(sh.find, libs_dir, join(self.dist_dir, 'libs'),
|
filens = shprint(sh.find, libs_dir, join(self.dist_dir, 'libs'),
|
||||||
'-iname', '*.so', _env=env).stdout.decode('utf-8')
|
'-iname', '*.so', _env=env).stdout.decode('utf-8')
|
||||||
|
|
||||||
|
@ -301,9 +400,31 @@ class Bootstrap(object):
|
||||||
shprint(sh.rm, '-rf', d)
|
shprint(sh.rm, '-rf', d)
|
||||||
|
|
||||||
|
|
||||||
def expand_dependencies(recipes):
|
def expand_dependencies(recipes, ctx):
|
||||||
|
""" This function expands to lists of all different available
|
||||||
|
alternative recipe combinations, with the dependencies added in
|
||||||
|
ONLY for all the not-with-alternative recipes.
|
||||||
|
(So this is like the deps graph very simplified and incomplete, but
|
||||||
|
hopefully good enough for most basic bootstrap compatibility checks)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add in all the deps of recipes where there is no alternative:
|
||||||
|
recipes_with_deps = list(recipes)
|
||||||
|
for entry in recipes:
|
||||||
|
if not isinstance(entry, (tuple, list)) or len(entry) == 1:
|
||||||
|
if isinstance(entry, (tuple, list)):
|
||||||
|
entry = entry[0]
|
||||||
|
try:
|
||||||
|
recipe = Recipe.get_recipe(entry, ctx)
|
||||||
|
recipes_with_deps += recipe.depends
|
||||||
|
except ValueError:
|
||||||
|
# it's a pure python package without a recipe, so we
|
||||||
|
# don't know the dependencies...skipping for now
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Split up lists by available alternatives:
|
||||||
recipe_lists = [[]]
|
recipe_lists = [[]]
|
||||||
for recipe in recipes:
|
for recipe in recipes_with_deps:
|
||||||
if isinstance(recipe, (tuple, list)):
|
if isinstance(recipe, (tuple, list)):
|
||||||
new_recipe_lists = []
|
new_recipe_lists = []
|
||||||
for alternative in recipe:
|
for alternative in recipe:
|
||||||
|
@ -313,6 +434,6 @@ def expand_dependencies(recipes):
|
||||||
new_recipe_lists.append(new_list)
|
new_recipe_lists.append(new_list)
|
||||||
recipe_lists = new_recipe_lists
|
recipe_lists = new_recipe_lists
|
||||||
else:
|
else:
|
||||||
for old_list in recipe_lists:
|
for existing_list in recipe_lists:
|
||||||
old_list.append(recipe)
|
existing_list.append(recipe)
|
||||||
return recipe_lists
|
return recipe_lists
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
#!/usr/bin/env python2.7
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
|
from gzip import GzipFile
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from os.path import (
|
from os.path import (
|
||||||
dirname, join, isfile, realpath,
|
dirname, join, isfile, realpath,
|
||||||
relpath, split, exists, basename
|
relpath, split, exists, basename
|
||||||
)
|
)
|
||||||
from os import listdir, makedirs, remove
|
from os import environ, listdir, makedirs, remove
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -16,19 +16,20 @@ import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from zipfile import ZipFile
|
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
|
|
||||||
def get_dist_info_for(key):
|
def get_dist_info_for(key, error_if_missing=True):
|
||||||
try:
|
try:
|
||||||
with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
|
with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
|
||||||
info = json.load(fileh)
|
info = json.load(fileh)
|
||||||
value = str(info[key])
|
value = info[key]
|
||||||
except (OSError, KeyError) as e:
|
except (OSError, KeyError) as e:
|
||||||
|
if not error_if_missing:
|
||||||
|
return None
|
||||||
print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
|
print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
|
||||||
"from dist_info.json: " + str(e))
|
"from dist_info.json: " + str(e))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -39,10 +40,6 @@ def get_hostpython():
|
||||||
return get_dist_info_for('hostpython')
|
return get_dist_info_for('hostpython')
|
||||||
|
|
||||||
|
|
||||||
def get_python_version():
|
|
||||||
return get_dist_info_for('python_version')
|
|
||||||
|
|
||||||
|
|
||||||
def get_bootstrap_name():
|
def get_bootstrap_name():
|
||||||
return get_dist_info_for('bootstrap')
|
return get_dist_info_for('bootstrap')
|
||||||
|
|
||||||
|
@ -57,7 +54,6 @@ else:
|
||||||
curdir = dirname(__file__)
|
curdir = dirname(__file__)
|
||||||
|
|
||||||
PYTHON = get_hostpython()
|
PYTHON = get_hostpython()
|
||||||
PYTHON_VERSION = get_python_version()
|
|
||||||
if PYTHON is not None and not exists(PYTHON):
|
if PYTHON is not None and not exists(PYTHON):
|
||||||
PYTHON = None
|
PYTHON = None
|
||||||
|
|
||||||
|
@ -72,29 +68,23 @@ BLACKLIST_PATTERNS = [
|
||||||
'~',
|
'~',
|
||||||
'*.bak',
|
'*.bak',
|
||||||
'*.swp',
|
'*.swp',
|
||||||
|
|
||||||
|
# Android artifacts
|
||||||
|
'*.apk',
|
||||||
|
'*.aab',
|
||||||
]
|
]
|
||||||
# pyc/py
|
|
||||||
if PYTHON is not None:
|
|
||||||
BLACKLIST_PATTERNS.append('*.py')
|
|
||||||
if PYTHON_VERSION and int(PYTHON_VERSION[0]) == 2:
|
|
||||||
# we only blacklist `.pyc` for python2 because in python3 the compiled
|
|
||||||
# extension is `.pyc` (.pyo files not exists for python >= 3.6)
|
|
||||||
BLACKLIST_PATTERNS.append('*.pyc')
|
|
||||||
|
|
||||||
WHITELIST_PATTERNS = []
|
WHITELIST_PATTERNS = []
|
||||||
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
|
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
|
||||||
WHITELIST_PATTERNS.append('pyconfig.h')
|
WHITELIST_PATTERNS.append('pyconfig.h')
|
||||||
|
|
||||||
python_files = []
|
|
||||||
|
|
||||||
|
|
||||||
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
|
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
|
||||||
join(curdir, 'templates')))
|
join(curdir, 'templates')))
|
||||||
|
|
||||||
|
|
||||||
def try_unlink(fn):
|
DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity'
|
||||||
if exists(fn):
|
DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService'
|
||||||
os.unlink(fn)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_dir(path):
|
def ensure_dir(path):
|
||||||
|
@ -154,75 +144,33 @@ def listfiles(d):
|
||||||
yield fn
|
yield fn
|
||||||
|
|
||||||
|
|
||||||
def make_python_zip():
|
def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True):
|
||||||
'''
|
|
||||||
Search for all the python related files, and construct the pythonXX.zip
|
|
||||||
According to
|
|
||||||
# http://randomsplat.com/id5-cross-compiling-python-for-embedded-linux.html
|
|
||||||
site-packages, config and lib-dynload will be not included.
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not exists('private'):
|
|
||||||
print('No compiled python is present to zip, skipping.')
|
|
||||||
return
|
|
||||||
|
|
||||||
global python_files
|
|
||||||
d = realpath(join('private', 'lib', 'python2.7'))
|
|
||||||
|
|
||||||
def select(fn):
|
|
||||||
if is_blacklist(fn):
|
|
||||||
return False
|
|
||||||
fn = realpath(fn)
|
|
||||||
assert(fn.startswith(d))
|
|
||||||
fn = fn[len(d):]
|
|
||||||
if (fn.startswith('/site-packages/')
|
|
||||||
or fn.startswith('/config/')
|
|
||||||
or fn.startswith('/lib-dynload/')
|
|
||||||
or fn.startswith('/libpymodules.so')):
|
|
||||||
return False
|
|
||||||
return fn
|
|
||||||
|
|
||||||
# get a list of all python file
|
|
||||||
python_files = [x for x in listfiles(d) if select(x)]
|
|
||||||
|
|
||||||
# create the final zipfile
|
|
||||||
zfn = join('private', 'lib', 'python27.zip')
|
|
||||||
zf = ZipFile(zfn, 'w')
|
|
||||||
|
|
||||||
# put all the python files in it
|
|
||||||
for fn in python_files:
|
|
||||||
afn = fn[len(d):]
|
|
||||||
zf.write(fn, afn)
|
|
||||||
zf.close()
|
|
||||||
|
|
||||||
|
|
||||||
def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True):
|
|
||||||
'''
|
'''
|
||||||
Make a zip file `fn` from the contents of source_dis.
|
Make a zip file `fn` from the contents of source_dis.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# selector function
|
def clean(tinfo):
|
||||||
def select(fn):
|
"""cleaning function (for reproducible builds)"""
|
||||||
rfn = realpath(fn)
|
tinfo.uid = tinfo.gid = 0
|
||||||
for p in ignore_path:
|
tinfo.uname = tinfo.gname = ''
|
||||||
if p.endswith('/'):
|
tinfo.mtime = 0
|
||||||
p = p[:-1]
|
return tinfo
|
||||||
if rfn.startswith(p):
|
|
||||||
return False
|
|
||||||
if rfn in python_files:
|
|
||||||
return False
|
|
||||||
return not is_blacklist(fn)
|
|
||||||
|
|
||||||
# get the files and relpath file of all the directory we asked for
|
# get the files and relpath file of all the directory we asked for
|
||||||
files = []
|
files = []
|
||||||
for sd in source_dirs:
|
for sd in source_dirs:
|
||||||
sd = realpath(sd)
|
sd = realpath(sd)
|
||||||
compile_dir(sd, optimize_python=optimize_python)
|
for fn in listfiles(sd):
|
||||||
files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd)
|
if is_blacklist(fn):
|
||||||
if select(x)]
|
continue
|
||||||
|
if fn.endswith('.py') and byte_compile_python:
|
||||||
|
fn = compile_py_file(fn, optimize_python=optimize_python)
|
||||||
|
files.append((fn, relpath(realpath(fn), sd)))
|
||||||
|
files.sort() # deterministic
|
||||||
|
|
||||||
# create tar.gz of thoses files
|
# create tar.gz of thoses files
|
||||||
tf = tarfile.open(tfn, 'w:gz', format=tarfile.USTAR_FORMAT)
|
gf = GzipFile(tfn, 'wb', mtime=0) # deterministic
|
||||||
|
tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT)
|
||||||
dirs = []
|
dirs = []
|
||||||
for fn, afn in files:
|
for fn, afn in files:
|
||||||
dn = dirname(afn)
|
dn = dirname(afn)
|
||||||
|
@ -238,25 +186,24 @@ def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True):
|
||||||
dirs.append(d)
|
dirs.append(d)
|
||||||
tinfo = tarfile.TarInfo(d)
|
tinfo = tarfile.TarInfo(d)
|
||||||
tinfo.type = tarfile.DIRTYPE
|
tinfo.type = tarfile.DIRTYPE
|
||||||
|
clean(tinfo)
|
||||||
tf.addfile(tinfo)
|
tf.addfile(tinfo)
|
||||||
|
|
||||||
# put the file
|
# put the file
|
||||||
tf.add(fn, afn)
|
tf.add(fn, afn, filter=clean)
|
||||||
tf.close()
|
tf.close()
|
||||||
|
gf.close()
|
||||||
|
|
||||||
|
|
||||||
def compile_dir(dfn, optimize_python=True):
|
def compile_py_file(python_file, optimize_python=True):
|
||||||
'''
|
'''
|
||||||
Compile *.py in directory `dfn` to *.pyo
|
Compile python_file to *.pyc and return the filename of the *.pyc file.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if PYTHON is None:
|
if PYTHON is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if int(PYTHON_VERSION[0]) >= 3:
|
args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file]
|
||||||
args = [PYTHON, '-m', 'compileall', '-b', '-f', dfn]
|
|
||||||
else:
|
|
||||||
args = [PYTHON, '-m', 'compileall', '-f', dfn]
|
|
||||||
if optimize_python:
|
if optimize_python:
|
||||||
# -OO = strip docstrings
|
# -OO = strip docstrings
|
||||||
args.insert(1, '-OO')
|
args.insert(1, '-OO')
|
||||||
|
@ -268,16 +215,18 @@ def compile_dir(dfn, optimize_python=True):
|
||||||
'error, see logs above')
|
'error, see logs above')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
return ".".join([os.path.splitext(python_file)[0], "pyc"])
|
||||||
|
|
||||||
|
|
||||||
def make_package(args):
|
def make_package(args):
|
||||||
# If no launcher is specified, require a main.py/main.pyo:
|
# If no launcher is specified, require a main.py/main.pyc:
|
||||||
if (get_bootstrap_name() != "sdl" or args.launcher is None) and \
|
if (get_bootstrap_name() != "sdl" or args.launcher is None) and \
|
||||||
get_bootstrap_name() != "webview":
|
get_bootstrap_name() not in ["webview", "service_library"]:
|
||||||
# (webview doesn't need an entrypoint, apparently)
|
# (webview doesn't need an entrypoint, apparently)
|
||||||
if args.private is None or (
|
if args.private is None or (
|
||||||
not exists(join(realpath(args.private), 'main.py')) and
|
not exists(join(realpath(args.private), 'main.py')) and
|
||||||
not exists(join(realpath(args.private), 'main.pyo'))):
|
not exists(join(realpath(args.private), 'main.pyc'))):
|
||||||
print('''BUILD FAILURE: No main.py(o) found in your app directory. This
|
print('''BUILD FAILURE: No main.py(c) found in your app directory. This
|
||||||
file must exist to act as the entry point for you app. If your app is
|
file must exist to act as the entry point for you app. If your app is
|
||||||
started by a file with a different name, rename it to main.py or add a
|
started by a file with a different name, rename it to main.py or add a
|
||||||
main.py that loads it.''')
|
main.py that loads it.''')
|
||||||
|
@ -286,53 +235,159 @@ main.py that loads it.''')
|
||||||
assets_dir = "src/main/assets"
|
assets_dir = "src/main/assets"
|
||||||
|
|
||||||
# Delete the old assets.
|
# Delete the old assets.
|
||||||
try_unlink(join(assets_dir, 'public.mp3'))
|
shutil.rmtree(assets_dir, ignore_errors=True)
|
||||||
try_unlink(join(assets_dir, 'private.mp3'))
|
|
||||||
ensure_dir(assets_dir)
|
ensure_dir(assets_dir)
|
||||||
|
|
||||||
# In order to speedup import and initial depack,
|
|
||||||
# construct a python27.zip
|
|
||||||
make_python_zip()
|
|
||||||
|
|
||||||
# Add extra environment variable file into tar-able directory:
|
# Add extra environment variable file into tar-able directory:
|
||||||
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
|
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
|
||||||
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
|
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
|
||||||
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
|
if hasattr(args, "window"):
|
||||||
|
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
|
||||||
if hasattr(args, "orientation"):
|
if hasattr(args, "orientation"):
|
||||||
f.write("P4A_ORIENTATION=" + str(args.orientation) + "\n")
|
f.write("P4A_ORIENTATION=" + str(args.orientation) + "\n")
|
||||||
f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
|
f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
|
||||||
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
|
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
|
||||||
|
|
||||||
# Package up the private data (public not supported).
|
# Package up the private data (public not supported).
|
||||||
tar_dirs = [env_vars_tarpath]
|
use_setup_py = get_dist_info_for("use_setup_py",
|
||||||
if args.private:
|
error_if_missing=False) is True
|
||||||
tar_dirs.append(args.private)
|
private_tar_dirs = [env_vars_tarpath]
|
||||||
for python_bundle_dir in ('private', 'crystax_python', '_python_bundle'):
|
_temp_dirs_to_clean = []
|
||||||
if exists(python_bundle_dir):
|
try:
|
||||||
tar_dirs.append(python_bundle_dir)
|
if args.private:
|
||||||
if get_bootstrap_name() == "webview":
|
if not use_setup_py or (
|
||||||
tar_dirs.append('webview_includes')
|
not exists(join(args.private, "setup.py")) and
|
||||||
if args.private or args.launcher:
|
not exists(join(args.private, "pyproject.toml"))
|
||||||
make_tar(
|
):
|
||||||
join(assets_dir, 'private.mp3'), tar_dirs, args.ignore_path,
|
print('No setup.py/pyproject.toml used, copying '
|
||||||
optimize_python=args.optimize_python)
|
'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":
|
||||||
|
for asset in listdir('webview_includes'):
|
||||||
|
shutil.copy(join('webview_includes', asset), join(assets_dir, asset))
|
||||||
|
|
||||||
|
for asset in args.assets:
|
||||||
|
asset_src, asset_dest = asset.split(":")
|
||||||
|
if isfile(realpath(asset_src)):
|
||||||
|
ensure_dir(dirname(join(assets_dir, asset_dest)))
|
||||||
|
shutil.copy(realpath(asset_src), join(assets_dir, asset_dest))
|
||||||
|
else:
|
||||||
|
shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest))
|
||||||
|
|
||||||
|
if args.private or args.launcher:
|
||||||
|
for arch in get_dist_info_for("archs"):
|
||||||
|
libs_dir = f"libs/{arch}"
|
||||||
|
make_tar(
|
||||||
|
join(libs_dir, "libpybundle.so"),
|
||||||
|
[f"_python_bundle__{arch}"],
|
||||||
|
byte_compile_python=args.byte_compile_python,
|
||||||
|
optimize_python=args.optimize_python,
|
||||||
|
)
|
||||||
|
make_tar(
|
||||||
|
join(assets_dir, "private.tar"),
|
||||||
|
private_tar_dirs,
|
||||||
|
byte_compile_python=args.byte_compile_python,
|
||||||
|
optimize_python=args.optimize_python,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
for directory in _temp_dirs_to_clean:
|
||||||
|
shutil.rmtree(directory)
|
||||||
|
|
||||||
# Remove extra env vars tar-able directory:
|
# Remove extra env vars tar-able directory:
|
||||||
shutil.rmtree(env_vars_tarpath)
|
shutil.rmtree(env_vars_tarpath)
|
||||||
|
|
||||||
# Prepare some variables for templating process
|
# Prepare some variables for templating process
|
||||||
res_dir = "src/main/res"
|
res_dir = "src/main/res"
|
||||||
|
res_dir_initial = "src/res_initial"
|
||||||
|
# make res_dir stateless
|
||||||
|
if exists(res_dir_initial):
|
||||||
|
shutil.rmtree(res_dir, ignore_errors=True)
|
||||||
|
shutil.copytree(res_dir_initial, res_dir)
|
||||||
|
else:
|
||||||
|
shutil.copytree(res_dir, res_dir_initial)
|
||||||
|
|
||||||
|
# Add user resouces
|
||||||
|
for resource in args.resources:
|
||||||
|
resource_src, resource_dest = resource.split(":")
|
||||||
|
if isfile(realpath(resource_src)):
|
||||||
|
ensure_dir(dirname(join(res_dir, resource_dest)))
|
||||||
|
shutil.copy(realpath(resource_src), join(res_dir, resource_dest))
|
||||||
|
else:
|
||||||
|
shutil.copytree(realpath(resource_src),
|
||||||
|
join(res_dir, resource_dest), dirs_exist_ok=True)
|
||||||
|
|
||||||
default_icon = 'templates/kivy-icon.png'
|
default_icon = 'templates/kivy-icon.png'
|
||||||
default_presplash = 'templates/kivy-presplash.jpg'
|
default_presplash = 'templates/kivy-presplash.jpg'
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
args.icon or default_icon,
|
args.icon or default_icon,
|
||||||
join(res_dir, 'drawable/icon.png')
|
join(res_dir, 'mipmap/icon.png')
|
||||||
)
|
)
|
||||||
|
if args.icon_fg and args.icon_bg:
|
||||||
|
shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png'))
|
||||||
|
shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png'))
|
||||||
|
with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd:
|
||||||
|
fd.write("""<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/icon_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/icon_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
""")
|
||||||
|
elif args.icon_fg or args.icon_bg:
|
||||||
|
print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. "
|
||||||
|
"Ignoring.")
|
||||||
|
|
||||||
if get_bootstrap_name() != "service_only":
|
if get_bootstrap_name() != "service_only":
|
||||||
shutil.copy(
|
lottie_splashscreen = join(res_dir, 'raw/splashscreen.json')
|
||||||
args.presplash or default_presplash,
|
if args.presplash_lottie:
|
||||||
join(res_dir, 'drawable/presplash.jpg')
|
shutil.copy(
|
||||||
)
|
'templates/lottie.xml',
|
||||||
|
join(res_dir, 'layout/lottie.xml')
|
||||||
|
)
|
||||||
|
ensure_dir(join(res_dir, 'raw'))
|
||||||
|
shutil.copy(
|
||||||
|
args.presplash_lottie,
|
||||||
|
join(res_dir, 'raw/splashscreen.json')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if exists(lottie_splashscreen):
|
||||||
|
remove(lottie_splashscreen)
|
||||||
|
remove(join(res_dir, 'layout/lottie.xml'))
|
||||||
|
|
||||||
|
shutil.copy(
|
||||||
|
args.presplash or default_presplash,
|
||||||
|
join(res_dir, 'drawable/presplash.jpg')
|
||||||
|
)
|
||||||
|
|
||||||
# If extra Java jars were requested, copy them into the libs directory
|
# If extra Java jars were requested, copy them into the libs directory
|
||||||
jars = []
|
jars = []
|
||||||
|
@ -360,17 +415,17 @@ main.py that loads it.''')
|
||||||
|
|
||||||
version_code = 0
|
version_code = 0
|
||||||
if not args.numeric_version:
|
if not args.numeric_version:
|
||||||
# Set version code in format (arch-minsdk-app_version)
|
"""
|
||||||
with open(join(dirname(__file__), 'dist_info.json'), 'r') as dist_info:
|
Set version code in format (10 + minsdk + app_version)
|
||||||
dist_data = json.load(dist_info)
|
Historically versioning was (arch + minsdk + app_version),
|
||||||
arch = dist_data["archs"][0]
|
with arch expressed with a single digit from 6 to 9.
|
||||||
arch_dict = {"x86_64": "9", "arm64-v8a": "8", "armeabi-v7a": "7", "x86": "6"}
|
Since the multi-arch support, has been changed to 10.
|
||||||
arch_code = arch_dict.get(arch, '1')
|
"""
|
||||||
min_sdk = args.min_sdk_version
|
min_sdk = args.min_sdk_version
|
||||||
for i in args.version.split('.'):
|
for i in args.version.split('.'):
|
||||||
version_code *= 100
|
version_code *= 100
|
||||||
version_code += int(i)
|
version_code += int(i)
|
||||||
args.numeric_version = "{}{}{}".format(arch_code, min_sdk, version_code)
|
args.numeric_version = "{}{}{}".format("10", min_sdk, version_code)
|
||||||
|
|
||||||
if args.intent_filters:
|
if args.intent_filters:
|
||||||
with open(args.intent_filters) as fd:
|
with open(args.intent_filters) as fd:
|
||||||
|
@ -387,6 +442,9 @@ main.py that loads it.''')
|
||||||
for spec in args.extra_source_dirs:
|
for spec in args.extra_source_dirs:
|
||||||
if ':' in spec:
|
if ':' in spec:
|
||||||
specdir, specincludes = spec.split(':')
|
specdir, specincludes = spec.split(':')
|
||||||
|
print('WARNING: Currently gradle builds only support including source '
|
||||||
|
'directories, so when building using gradle all files in '
|
||||||
|
'{} will be included.'.format(specdir))
|
||||||
else:
|
else:
|
||||||
specdir = spec
|
specdir = spec
|
||||||
specincludes = '**'
|
specincludes = '**'
|
||||||
|
@ -402,6 +460,7 @@ main.py that loads it.''')
|
||||||
service = True
|
service = True
|
||||||
|
|
||||||
service_names = []
|
service_names = []
|
||||||
|
base_service_class = args.service_class_name.split('.')[-1]
|
||||||
for sid, spec in enumerate(args.services):
|
for sid, spec in enumerate(args.services):
|
||||||
spec = spec.split(':')
|
spec = spec.split(':')
|
||||||
name = spec[0]
|
name = spec[0]
|
||||||
|
@ -426,6 +485,7 @@ main.py that loads it.''')
|
||||||
foreground=foreground,
|
foreground=foreground,
|
||||||
sticky=sticky,
|
sticky=sticky,
|
||||||
service_id=sid + 1,
|
service_id=sid + 1,
|
||||||
|
base_service_class=base_service_class,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find the SDK directory and target API
|
# Find the SDK directory and target API
|
||||||
|
@ -447,19 +507,37 @@ main.py that loads it.''')
|
||||||
# Try to build with the newest available build tools
|
# Try to build with the newest available build tools
|
||||||
ignored = {".DS_Store", ".ds_store"}
|
ignored = {".DS_Store", ".ds_store"}
|
||||||
build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
|
build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
|
||||||
build_tools_versions.sort(key=LooseVersion)
|
build_tools_versions = sorted(build_tools_versions,
|
||||||
|
key=LooseVersion)
|
||||||
build_tools_version = build_tools_versions[-1]
|
build_tools_version = build_tools_versions[-1]
|
||||||
|
|
||||||
# Folder name for launcher (used by SDL2 bootstrap)
|
# Folder name for launcher (used by SDL2 bootstrap)
|
||||||
url_scheme = 'kivy'
|
url_scheme = 'kivy'
|
||||||
|
|
||||||
|
# Copy backup rules file if specified and update the argument
|
||||||
|
res_xml_dir = join(res_dir, 'xml')
|
||||||
|
if args.backup_rules:
|
||||||
|
ensure_dir(res_xml_dir)
|
||||||
|
shutil.copy(join(args.private, args.backup_rules), res_xml_dir)
|
||||||
|
args.backup_rules = split(args.backup_rules)[1][:-4]
|
||||||
|
|
||||||
|
# Copy res_xml files to src/main/res/xml
|
||||||
|
if args.res_xmls:
|
||||||
|
ensure_dir(res_xml_dir)
|
||||||
|
for xmlpath in args.res_xmls:
|
||||||
|
if not os.path.exists(xmlpath):
|
||||||
|
xmlpath = join(args.private, xmlpath)
|
||||||
|
shutil.copy(xmlpath, res_xml_dir)
|
||||||
|
|
||||||
# Render out android manifest:
|
# Render out android manifest:
|
||||||
manifest_path = "src/main/AndroidManifest.xml"
|
manifest_path = "src/main/AndroidManifest.xml"
|
||||||
render_args = {
|
render_args = {
|
||||||
"args": args,
|
"args": args,
|
||||||
"service": service,
|
"service": service,
|
||||||
"service_names": service_names,
|
"service_names": service_names,
|
||||||
"android_api": android_api
|
"android_api": android_api,
|
||||||
|
"debug": "debug" in args.build_mode,
|
||||||
|
"native_services": args.native_services
|
||||||
}
|
}
|
||||||
if get_bootstrap_name() == "sdl2":
|
if get_bootstrap_name() == "sdl2":
|
||||||
render_args["url_scheme"] = url_scheme
|
render_args["url_scheme"] = url_scheme
|
||||||
|
@ -482,9 +560,17 @@ main.py that loads it.''')
|
||||||
aars=aars,
|
aars=aars,
|
||||||
jars=jars,
|
jars=jars,
|
||||||
android_api=android_api,
|
android_api=android_api,
|
||||||
build_tools_version=build_tools_version
|
build_tools_version=build_tools_version,
|
||||||
|
debug_build="debug" in args.build_mode,
|
||||||
|
is_library=(get_bootstrap_name() == 'service_library'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# gradle properties
|
||||||
|
render(
|
||||||
|
'gradle.tmpl.properties',
|
||||||
|
'gradle.properties',
|
||||||
|
args=args)
|
||||||
|
|
||||||
# ant build templates
|
# ant build templates
|
||||||
render(
|
render(
|
||||||
'build.tmpl.xml',
|
'build.tmpl.xml',
|
||||||
|
@ -493,9 +579,18 @@ main.py that loads it.''')
|
||||||
versioned_name=versioned_name)
|
versioned_name=versioned_name)
|
||||||
|
|
||||||
# String resources:
|
# String resources:
|
||||||
|
timestamp = time.time()
|
||||||
|
if 'SOURCE_DATE_EPOCH' in environ:
|
||||||
|
# for reproducible builds
|
||||||
|
timestamp = int(environ['SOURCE_DATE_EPOCH'])
|
||||||
|
private_version = "{} {} {}".format(
|
||||||
|
args.version,
|
||||||
|
args.numeric_version,
|
||||||
|
timestamp
|
||||||
|
)
|
||||||
render_args = {
|
render_args = {
|
||||||
"args": args,
|
"args": args,
|
||||||
"private_version": str(time.time())
|
"private_version": hashlib.sha1(private_version.encode()).hexdigest()
|
||||||
}
|
}
|
||||||
if get_bootstrap_name() == "sdl2":
|
if get_bootstrap_name() == "sdl2":
|
||||||
render_args["url_scheme"] = url_scheme
|
render_args["url_scheme"] = url_scheme
|
||||||
|
@ -527,27 +622,31 @@ main.py that loads it.''')
|
||||||
for patch_name in os.listdir(join('src', 'patches')):
|
for patch_name in os.listdir(join('src', 'patches')):
|
||||||
patch_path = join('src', 'patches', patch_name)
|
patch_path = join('src', 'patches', patch_name)
|
||||||
print("Applying patch: " + str(patch_path))
|
print("Applying patch: " + str(patch_path))
|
||||||
|
|
||||||
|
# -N: insist this is FORWARD patch, don't reverse apply
|
||||||
|
# -p1: strip first path component
|
||||||
|
# -t: batch mode, don't ask questions
|
||||||
|
patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_output([
|
# Use a dry run to establish whether the patch is already applied.
|
||||||
# -N: insist this is FORWARd patch, don't reverse apply
|
# If we don't check this, the patch may be partially applied (which is bad!)
|
||||||
# -p1: strip first path component
|
subprocess.check_output(patch_command + ["--dry-run"])
|
||||||
# -t: batch mode, don't ask questions
|
|
||||||
"patch", "-N", "-p1", "-t", "-i", patch_path
|
|
||||||
])
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if e.returncode == 1:
|
if e.returncode == 1:
|
||||||
# Return code 1 means it didn't apply, this will
|
# Return code 1 means not all hunks could be applied, this usually
|
||||||
# usually mean it is already applied.
|
# means the patch is already applied.
|
||||||
print("Warning: failed to apply patch (" +
|
print("Warning: failed to apply patch (exit code 1), "
|
||||||
"exit code 1), " +
|
"assuming it is already applied: ",
|
||||||
"assuming it is already applied: " +
|
str(patch_path))
|
||||||
str(patch_path)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
else:
|
||||||
|
# The dry run worked, so do the real thing
|
||||||
|
subprocess.check_output(patch_command)
|
||||||
|
|
||||||
|
|
||||||
def parse_args(args=None):
|
def parse_args_and_make_package(args=None):
|
||||||
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
|
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
|
||||||
|
|
||||||
# Get the default minsdk, equal to the NDK API that this dist is built against
|
# Get the default minsdk, equal to the NDK API that this dist is built against
|
||||||
|
@ -602,16 +701,36 @@ tools directory of the Android SDK.
|
||||||
help='Custom key=value to add in application metadata')
|
help='Custom key=value to add in application metadata')
|
||||||
ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
|
ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
|
||||||
help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
|
help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
|
||||||
|
ap.add_argument('--asset', dest='assets',
|
||||||
|
action="append", default=[],
|
||||||
|
metavar="/path/to/source:dest",
|
||||||
|
help='Put this in the assets folder at assets/dest')
|
||||||
|
ap.add_argument('--resource', dest='resources',
|
||||||
|
action="append", default=[],
|
||||||
|
metavar="/path/to/source:kind/asset",
|
||||||
|
help='Put this in the res folder at res/kind')
|
||||||
ap.add_argument('--icon', dest='icon',
|
ap.add_argument('--icon', dest='icon',
|
||||||
help=('A png file to use as the icon for '
|
help=('A png file to use as the icon for '
|
||||||
'the application.'))
|
'the application.'))
|
||||||
|
ap.add_argument('--icon-fg', dest='icon_fg',
|
||||||
|
help=('A png file to use as the foreground of the adaptive icon '
|
||||||
|
'for the application.'))
|
||||||
|
ap.add_argument('--icon-bg', dest='icon_bg',
|
||||||
|
help=('A png file to use as the background of the adaptive icon '
|
||||||
|
'for the application.'))
|
||||||
ap.add_argument('--service', dest='services', action='append', default=[],
|
ap.add_argument('--service', dest='services', action='append', default=[],
|
||||||
help='Declare a new service entrypoint: '
|
help='Declare a new service entrypoint: '
|
||||||
'NAME:PATH_TO_PY[:foreground]')
|
'NAME:PATH_TO_PY[:foreground]')
|
||||||
|
ap.add_argument('--native-service', dest='native_services', action='append', default=[],
|
||||||
|
help='Declare a new native service: '
|
||||||
|
'package.name.service')
|
||||||
if get_bootstrap_name() != "service_only":
|
if get_bootstrap_name() != "service_only":
|
||||||
ap.add_argument('--presplash', dest='presplash',
|
ap.add_argument('--presplash', dest='presplash',
|
||||||
help=('A jpeg file to use as a screen while the '
|
help=('A jpeg file to use as a screen while the '
|
||||||
'application is loading.'))
|
'application is loading.'))
|
||||||
|
ap.add_argument('--presplash-lottie', dest='presplash_lottie',
|
||||||
|
help=('A lottie (json) file to use as an animation while the '
|
||||||
|
'application is loading.'))
|
||||||
ap.add_argument('--presplash-color',
|
ap.add_argument('--presplash-color',
|
||||||
dest='presplash_color',
|
dest='presplash_color',
|
||||||
default='#000000',
|
default='#000000',
|
||||||
|
@ -636,6 +755,28 @@ tools directory of the Android SDK.
|
||||||
'https://developer.android.com/guide/'
|
'https://developer.android.com/guide/'
|
||||||
'topics/manifest/'
|
'topics/manifest/'
|
||||||
'activity-element.html'))
|
'activity-element.html'))
|
||||||
|
|
||||||
|
ap.add_argument('--enable-androidx', dest='enable_androidx',
|
||||||
|
action='store_true',
|
||||||
|
help=('Enable the AndroidX support library, '
|
||||||
|
'requires api = 28 or greater'))
|
||||||
|
ap.add_argument('--android-entrypoint', dest='android_entrypoint',
|
||||||
|
default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
|
||||||
|
help='Defines which java class will be used for startup, usually a subclass of PythonActivity')
|
||||||
|
ap.add_argument('--android-apptheme', dest='android_apptheme',
|
||||||
|
default='@android:style/Theme.NoTitleBar',
|
||||||
|
help='Defines which app theme should be selected for the main activity')
|
||||||
|
ap.add_argument('--add-compile-option', dest='compile_options', default=[],
|
||||||
|
action='append', help='add compile options to gradle.build')
|
||||||
|
ap.add_argument('--add-gradle-repository', dest='gradle_repositories',
|
||||||
|
default=[],
|
||||||
|
action='append',
|
||||||
|
help='Ddd a repository for gradle')
|
||||||
|
ap.add_argument('--add-packaging-option', dest='packaging_options',
|
||||||
|
default=[],
|
||||||
|
action='append',
|
||||||
|
help='Dndroid packaging options')
|
||||||
|
|
||||||
ap.add_argument('--wakelock', dest='wakelock', action='store_true',
|
ap.add_argument('--wakelock', dest='wakelock', action='store_true',
|
||||||
help=('Indicate if the application needs the device '
|
help=('Indicate if the application needs the device '
|
||||||
'to stay on'))
|
'to stay on'))
|
||||||
|
@ -647,6 +788,13 @@ tools directory of the Android SDK.
|
||||||
default=join(curdir, 'whitelist.txt'),
|
default=join(curdir, 'whitelist.txt'),
|
||||||
help=('Use a whitelist file to prevent blacklisting of '
|
help=('Use a whitelist file to prevent blacklisting of '
|
||||||
'file in the final APK'))
|
'file in the final APK'))
|
||||||
|
ap.add_argument('--release', dest='build_mode', action='store_const',
|
||||||
|
const='release', default='debug',
|
||||||
|
help='Build your app as a non-debug release build. '
|
||||||
|
'(Disables gdb debugging among other things)')
|
||||||
|
ap.add_argument('--with-debug-symbols', dest='with_debug_symbols',
|
||||||
|
action='store_const', const=True, default=False,
|
||||||
|
help='Will keep debug symbols from `.so` files.')
|
||||||
ap.add_argument('--add-jar', dest='add_jar', action='append',
|
ap.add_argument('--add-jar', dest='add_jar', action='append',
|
||||||
help=('Add a Java .jar to the libs, so you can access its '
|
help=('Add a Java .jar to the libs, so you can access its '
|
||||||
'classes with pyjnius. You can specify this '
|
'classes with pyjnius. You can specify this '
|
||||||
|
@ -674,6 +822,8 @@ tools directory of the Android SDK.
|
||||||
'filename containing xml. The filename should be '
|
'filename containing xml. The filename should be '
|
||||||
'located relative to the python-for-android '
|
'located relative to the python-for-android '
|
||||||
'directory'))
|
'directory'))
|
||||||
|
ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[],
|
||||||
|
help='Add files to res/xml directory (for example device-filters)', nargs='+')
|
||||||
ap.add_argument('--with-billing', dest='billing_pubkey',
|
ap.add_argument('--with-billing', dest='billing_pubkey',
|
||||||
help='If set, the billing service will be added (not implemented)')
|
help='If set, the billing service will be added (not implemented)')
|
||||||
ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
|
ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
|
||||||
|
@ -685,8 +835,6 @@ tools directory of the Android SDK.
|
||||||
ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
|
ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Use the system python during compileall if possible.')
|
help='Use the system python during compileall if possible.')
|
||||||
ap.add_argument('--no-compile-pyo', dest='no_compile_pyo', action='store_true',
|
|
||||||
help='Do not optimise .py files to .pyo.')
|
|
||||||
ap.add_argument('--sign', action='store_true',
|
ap.add_argument('--sign', action='store_true',
|
||||||
help=('Try to sign the APK with your credentials. You must set '
|
help=('Try to sign the APK with your credentials. You must set '
|
||||||
'the appropriate environment variables.'))
|
'the appropriate environment variables.'))
|
||||||
|
@ -698,10 +846,33 @@ tools directory of the Android SDK.
|
||||||
help='Set the launch mode of the main activity in the manifest.')
|
help='Set the launch mode of the main activity in the manifest.')
|
||||||
ap.add_argument('--allow-backup', dest='allow_backup', default='true',
|
ap.add_argument('--allow-backup', dest='allow_backup', default='true',
|
||||||
help="if set to 'false', then android won't backup the application.")
|
help="if set to 'false', then android won't backup the application.")
|
||||||
|
ap.add_argument('--backup-rules', dest='backup_rules', default='',
|
||||||
|
help=('Backup rules for Android Auto Backup. Argument is a '
|
||||||
|
'filename containing xml. The filename should be '
|
||||||
|
'located relative to the private directory containing your source code '
|
||||||
|
'files (containing your main.py entrypoint). '
|
||||||
|
'See https://developer.android.com/guide/topics/data/'
|
||||||
|
'autobackup#IncludingFiles for more information'))
|
||||||
|
ap.add_argument('--no-byte-compile-python', dest='byte_compile_python',
|
||||||
|
action='store_false', default=True,
|
||||||
|
help='Skip byte compile for .py files.')
|
||||||
ap.add_argument('--no-optimize-python', dest='optimize_python',
|
ap.add_argument('--no-optimize-python', dest='optimize_python',
|
||||||
action='store_false', default=True,
|
action='store_false', default=True,
|
||||||
help=('Whether to compile to optimised .pyo files, using -OO '
|
help=('Whether to compile to optimised .pyc files, using -OO '
|
||||||
'(strips docstrings and asserts)'))
|
'(strips docstrings and asserts)'))
|
||||||
|
ap.add_argument('--extra-manifest-xml', default='',
|
||||||
|
help=('Extra xml to write directly inside the <manifest> element of'
|
||||||
|
'AndroidManifest.xml'))
|
||||||
|
ap.add_argument('--extra-manifest-application-arguments', default='',
|
||||||
|
help='Extra arguments to be added to the <manifest><application> tag of'
|
||||||
|
'AndroidManifest.xml')
|
||||||
|
ap.add_argument('--manifest-placeholders', dest='manifest_placeholders',
|
||||||
|
default='[:]', help=('Inject build variables into the manifest '
|
||||||
|
'via the manifestPlaceholders property'))
|
||||||
|
ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS,
|
||||||
|
help='Use that parameter if you need to implement your own PythonServive Java class')
|
||||||
|
ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
|
||||||
|
help='The full java class name of the main activity')
|
||||||
|
|
||||||
# Put together arguments, and add those from .p4a config file:
|
# Put together arguments, and add those from .p4a config file:
|
||||||
if args is None:
|
if args is None:
|
||||||
|
@ -721,7 +892,6 @@ tools directory of the Android SDK.
|
||||||
_read_configuration()
|
_read_configuration()
|
||||||
|
|
||||||
args = ap.parse_args(args)
|
args = ap.parse_args(args)
|
||||||
args.ignore_path = []
|
|
||||||
|
|
||||||
if args.name and args.name[0] == '"' and args.name[-1] == '"':
|
if args.name and args.name[0] == '"' and args.name[-1] == '"':
|
||||||
args.name = args.name[1:-1]
|
args.name = args.name[1:-1]
|
||||||
|
@ -751,21 +921,19 @@ tools directory of the Android SDK.
|
||||||
if args.permissions and isinstance(args.permissions[0], list):
|
if args.permissions and isinstance(args.permissions[0], list):
|
||||||
args.permissions = [p for perm in args.permissions for p in perm]
|
args.permissions = [p for perm in args.permissions for p in perm]
|
||||||
|
|
||||||
|
if args.res_xmls and isinstance(args.res_xmls[0], list):
|
||||||
|
args.res_xmls = [x for res in args.res_xmls for x in res]
|
||||||
|
|
||||||
if args.try_system_python_compile:
|
if args.try_system_python_compile:
|
||||||
# Hardcoding python2.7 is okay for now, as python3 skips the
|
# Hardcoding python2.7 is okay for now, as python3 skips the
|
||||||
# compilation anyway
|
# compilation anyway
|
||||||
if not exists('crystax_python'):
|
python_executable = 'python2.7'
|
||||||
python_executable = 'python2.7'
|
try:
|
||||||
try:
|
subprocess.call([python_executable, '--version'])
|
||||||
subprocess.call([python_executable, '--version'])
|
except (OSError, subprocess.CalledProcessError):
|
||||||
except (OSError, subprocess.CalledProcessError):
|
pass
|
||||||
pass
|
else:
|
||||||
else:
|
PYTHON = python_executable
|
||||||
PYTHON = python_executable
|
|
||||||
|
|
||||||
if args.no_compile_pyo:
|
|
||||||
PYTHON = None
|
|
||||||
BLACKLIST_PATTERNS.remove('*.py')
|
|
||||||
|
|
||||||
if args.blacklist:
|
if args.blacklist:
|
||||||
with open(args.blacklist) as fd:
|
with open(args.blacklist) as fd:
|
||||||
|
@ -791,4 +959,4 @@ tools directory of the Android SDK.
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parse_args()
|
parse_args_and_make_package()
|
||||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip
|
||||||
|
|
|
@ -21,7 +21,3 @@ LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog $(EXTRA_LDLIBS)
|
||||||
LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS)
|
LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS)
|
||||||
|
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
|
|
||||||
ifdef CRYSTAX_PYTHON_VERSION
|
|
||||||
$(call import-module,python/$(CRYSTAX_PYTHON_VERSION))
|
|
||||||
endif
|
|
||||||
|
|
|
@ -15,15 +15,11 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
#include "bootstrap_name.h"
|
#include "bootstrap_name.h"
|
||||||
|
|
||||||
#ifndef BOOTSTRAP_USES_NO_SDL_HEADERS
|
#ifndef BOOTSTRAP_USES_NO_SDL_HEADERS
|
||||||
#include "SDL.h"
|
#include "SDL.h"
|
||||||
#ifndef BOOTSTRAP_NAME_PYGAME
|
|
||||||
#include "SDL_opengles2.h"
|
#include "SDL_opengles2.h"
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
#ifdef BOOTSTRAP_NAME_PYGAME
|
|
||||||
#include "jniwrapperstuff.h"
|
|
||||||
#endif
|
|
||||||
#include "android/log.h"
|
#include "android/log.h"
|
||||||
|
|
||||||
#define ENTRYPOINT_MAXLEN 128
|
#define ENTRYPOINT_MAXLEN 128
|
||||||
|
@ -169,26 +165,14 @@ int main(int argc, char *argv[]) {
|
||||||
// Set up the python path
|
// Set up the python path
|
||||||
char paths[256];
|
char paths[256];
|
||||||
|
|
||||||
char crystax_python_dir[256];
|
|
||||||
snprintf(crystax_python_dir, 256,
|
|
||||||
"%s/crystax_python", getenv("ANDROID_UNPACK"));
|
|
||||||
char python_bundle_dir[256];
|
char python_bundle_dir[256];
|
||||||
snprintf(python_bundle_dir, 256,
|
snprintf(python_bundle_dir, 256,
|
||||||
"%s/_python_bundle", getenv("ANDROID_UNPACK"));
|
"%s/_python_bundle", getenv("ANDROID_UNPACK"));
|
||||||
if (dir_exists(crystax_python_dir) || dir_exists(python_bundle_dir)) {
|
if (dir_exists(python_bundle_dir)) {
|
||||||
if (dir_exists(crystax_python_dir)) {
|
LOGP("_python_bundle dir exists");
|
||||||
LOGP("crystax_python exists");
|
snprintf(paths, 256,
|
||||||
snprintf(paths, 256,
|
"%s/stdlib.zip:%s/modules",
|
||||||
"%s/stdlib.zip:%s/modules",
|
python_bundle_dir, python_bundle_dir);
|
||||||
crystax_python_dir, crystax_python_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dir_exists(python_bundle_dir)) {
|
|
||||||
LOGP("_python_bundle dir exists");
|
|
||||||
snprintf(paths, 256,
|
|
||||||
"%s/stdlib.zip:%s/modules",
|
|
||||||
python_bundle_dir, python_bundle_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGP("calculated paths to be...");
|
LOGP("calculated paths to be...");
|
||||||
LOGP(paths);
|
LOGP(paths);
|
||||||
|
@ -200,24 +184,11 @@ int main(int argc, char *argv[]) {
|
||||||
|
|
||||||
LOGP("set wchar paths...");
|
LOGP("set wchar paths...");
|
||||||
} else {
|
} else {
|
||||||
// We do not expect to see crystax_python any more, so no point
|
LOGP("_python_bundle does not exist...this not looks good, all python"
|
||||||
// reminding the user about it. If it does exist, we'll have
|
" recipes should have this folder, should we expect a crash soon?");
|
||||||
// logged it earlier.
|
|
||||||
LOGP("_python_bundle does not exist");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Py_Initialize();
|
Py_Initialize();
|
||||||
|
|
||||||
#if PY_MAJOR_VERSION < 3
|
|
||||||
// Can't Py_SetPath in python2 but we can set PySys_SetPath, which must
|
|
||||||
// be applied after Py_Initialize rather than before like Py_SetPath
|
|
||||||
#if PY_MICRO_VERSION >= 15
|
|
||||||
// Only for python native-build
|
|
||||||
PySys_SetPath(paths);
|
|
||||||
#endif
|
|
||||||
PySys_SetArgv(argc, argv);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
LOGP("Initialized python");
|
LOGP("Initialized python");
|
||||||
|
|
||||||
/* ensure threads will work.
|
/* ensure threads will work.
|
||||||
|
@ -236,34 +207,8 @@ int main(int argc, char *argv[]) {
|
||||||
* replace sys.path with our path
|
* replace sys.path with our path
|
||||||
*/
|
*/
|
||||||
PyRun_SimpleString("import sys, posix\n");
|
PyRun_SimpleString("import sys, posix\n");
|
||||||
if (dir_exists("lib")) {
|
|
||||||
/* If we built our own python, set up the paths correctly.
|
|
||||||
* This is only the case if we are using the python2legacy recipe
|
|
||||||
*/
|
|
||||||
LOGP("Setting up python from ANDROID_APP_PATH");
|
|
||||||
PyRun_SimpleString("private = posix.environ['ANDROID_APP_PATH']\n"
|
|
||||||
"argument = posix.environ['ANDROID_ARGUMENT']\n"
|
|
||||||
"sys.path[:] = [ \n"
|
|
||||||
" private + '/lib/python27.zip', \n"
|
|
||||||
" private + '/lib/python2.7/', \n"
|
|
||||||
" private + '/lib/python2.7/lib-dynload/', \n"
|
|
||||||
" private + '/lib/python2.7/site-packages/', \n"
|
|
||||||
" argument ]\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
char add_site_packages_dir[256];
|
char add_site_packages_dir[256];
|
||||||
if (dir_exists(crystax_python_dir)) {
|
|
||||||
snprintf(add_site_packages_dir, 256,
|
|
||||||
"sys.path.append('%s/site-packages')",
|
|
||||||
crystax_python_dir);
|
|
||||||
|
|
||||||
PyRun_SimpleString("import sys\n"
|
|
||||||
"sys.argv = ['notaninterpreterreally']\n"
|
|
||||||
"from os.path import realpath, join, dirname");
|
|
||||||
PyRun_SimpleString(add_site_packages_dir);
|
|
||||||
/* "sys.path.append(join(dirname(realpath(__file__)), 'site-packages'))") */
|
|
||||||
PyRun_SimpleString("sys.path = ['.'] + sys.path");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dir_exists(python_bundle_dir)) {
|
if (dir_exists(python_bundle_dir)) {
|
||||||
snprintf(add_site_packages_dir, 256,
|
snprintf(add_site_packages_dir, 256,
|
||||||
|
@ -281,13 +226,13 @@ int main(int argc, char *argv[]) {
|
||||||
PyRun_SimpleString(
|
PyRun_SimpleString(
|
||||||
"class LogFile(object):\n"
|
"class LogFile(object):\n"
|
||||||
" def __init__(self):\n"
|
" def __init__(self):\n"
|
||||||
" self.buffer = ''\n"
|
" self.__buffer = ''\n"
|
||||||
" def write(self, s):\n"
|
" def write(self, s):\n"
|
||||||
" s = self.buffer + s\n"
|
" s = self.__buffer + s\n"
|
||||||
" lines = s.split(\"\\n\")\n"
|
" lines = s.split('\\n')\n"
|
||||||
" for l in lines[:-1]:\n"
|
" for l in lines[:-1]:\n"
|
||||||
" androidembed.log(l)\n"
|
" androidembed.log(l.replace('\\x00', ''))\n"
|
||||||
" self.buffer = lines[-1]\n"
|
" self.__buffer = lines[-1]\n"
|
||||||
" def flush(self):\n"
|
" def flush(self):\n"
|
||||||
" return\n"
|
" return\n"
|
||||||
"sys.stdout = sys.stderr = LogFile()\n"
|
"sys.stdout = sys.stderr = LogFile()\n"
|
||||||
|
@ -306,14 +251,10 @@ int main(int argc, char *argv[]) {
|
||||||
*/
|
*/
|
||||||
LOGP("Run user program, change dir and execute entrypoint");
|
LOGP("Run user program, change dir and execute entrypoint");
|
||||||
|
|
||||||
/* Get the entrypoint, search the .pyo then .py
|
/* Get the entrypoint, search the .pyc then .py
|
||||||
*/
|
*/
|
||||||
char *dot = strrchr(env_entrypoint, '.');
|
char *dot = strrchr(env_entrypoint, '.');
|
||||||
#if PY_MAJOR_VERSION > 2
|
|
||||||
char *ext = ".pyc";
|
char *ext = ".pyc";
|
||||||
#else
|
|
||||||
char *ext = ".pyo";
|
|
||||||
#endif
|
|
||||||
if (dot <= 0) {
|
if (dot <= 0) {
|
||||||
LOGP("Invalid entrypoint, abort.");
|
LOGP("Invalid entrypoint, abort.");
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -329,21 +270,17 @@ int main(int argc, char *argv[]) {
|
||||||
entrypoint[strlen(env_entrypoint) - 1] = '\0';
|
entrypoint[strlen(env_entrypoint) - 1] = '\0';
|
||||||
LOGP(entrypoint);
|
LOGP(entrypoint);
|
||||||
if (!file_exists(entrypoint)) {
|
if (!file_exists(entrypoint)) {
|
||||||
LOGP("Entrypoint not found (.pyc/.pyo, fallback on .py), abort");
|
LOGP("Entrypoint not found (.pyc, fallback on .py), abort");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
strcpy(entrypoint, env_entrypoint);
|
strcpy(entrypoint, env_entrypoint);
|
||||||
}
|
}
|
||||||
} else if (!strcmp(dot, ".py")) {
|
} else if (!strcmp(dot, ".py")) {
|
||||||
/* if .py is passed, check the pyo version first */
|
/* if .py is passed, check the pyc version first */
|
||||||
strcpy(entrypoint, env_entrypoint);
|
strcpy(entrypoint, env_entrypoint);
|
||||||
entrypoint[strlen(env_entrypoint) + 1] = '\0';
|
entrypoint[strlen(env_entrypoint) + 1] = '\0';
|
||||||
#if PY_MAJOR_VERSION > 2
|
|
||||||
entrypoint[strlen(env_entrypoint)] = 'c';
|
entrypoint[strlen(env_entrypoint)] = 'c';
|
||||||
#else
|
|
||||||
entrypoint[strlen(env_entrypoint)] = 'o';
|
|
||||||
#endif
|
|
||||||
if (!file_exists(entrypoint)) {
|
if (!file_exists(entrypoint)) {
|
||||||
/* fallback on pure python version */
|
/* fallback on pure python version */
|
||||||
if (!file_exists(env_entrypoint)) {
|
if (!file_exists(env_entrypoint)) {
|
||||||
|
@ -353,7 +290,7 @@ int main(int argc, char *argv[]) {
|
||||||
strcpy(entrypoint, env_entrypoint);
|
strcpy(entrypoint, env_entrypoint);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOGP("Entrypoint have an invalid extension (must be .py or .pyc/.pyo), abort.");
|
LOGP("Entrypoint have an invalid extension (must be .py or .pyc), abort.");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
// LOGP("Entrypoint is:");
|
// LOGP("Entrypoint is:");
|
||||||
|
@ -374,8 +311,7 @@ int main(int argc, char *argv[]) {
|
||||||
ret = 1;
|
ret = 1;
|
||||||
PyErr_Print(); /* This exits with the right code if SystemExit. */
|
PyErr_Print(); /* This exits with the right code if SystemExit. */
|
||||||
PyObject *f = PySys_GetObject("stdout");
|
PyObject *f = PySys_GetObject("stdout");
|
||||||
if (PyFile_WriteString(
|
if (PyFile_WriteString("\n", f))
|
||||||
"\n", f)) /* python2 used Py_FlushLine, but this no longer exists */
|
|
||||||
PyErr_Clear();
|
PyErr_Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,10 @@ import android.app.PendingIntent;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
import org.kivy.android.PythonUtil;
|
//imports for channel definition
|
||||||
|
import android.app.NotificationManager;
|
||||||
import org.renpy.android.Hardware;
|
import android.app.NotificationChannel;
|
||||||
|
import android.graphics.Color;
|
||||||
|
|
||||||
public class PythonService extends Service implements Runnable {
|
public class PythonService extends Service implements Runnable {
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@ public class PythonService extends Service implements Runnable {
|
||||||
private String serviceEntrypoint;
|
private String serviceEntrypoint;
|
||||||
// Argument to pass to Python code,
|
// Argument to pass to Python code,
|
||||||
private String pythonServiceArgument;
|
private String pythonServiceArgument;
|
||||||
|
|
||||||
|
|
||||||
public static PythonService mService = null;
|
public static PythonService mService = null;
|
||||||
private Intent startIntent = null;
|
private Intent startIntent = null;
|
||||||
|
|
||||||
|
@ -42,10 +44,6 @@ public class PythonService extends Service implements Runnable {
|
||||||
autoRestartService = restart;
|
autoRestartService = restart;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean canDisplayNotification() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int startType() {
|
public int startType() {
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
@ -64,10 +62,15 @@ public class PythonService extends Service implements Runnable {
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
if (pythonThread != null) {
|
if (pythonThread != null) {
|
||||||
Log.v("python service", "service exists, do not start again");
|
Log.v("python service", "service exists, do not start again");
|
||||||
return START_NOT_STICKY;
|
return startType();
|
||||||
|
}
|
||||||
|
//intent is null if OS restarts a STICKY service
|
||||||
|
if (intent == null) {
|
||||||
|
Context context = getApplicationContext();
|
||||||
|
intent = getThisDefaultIntent(context, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
startIntent = intent;
|
startIntent = intent;
|
||||||
Bundle extras = intent.getExtras();
|
Bundle extras = intent.getExtras();
|
||||||
androidPrivate = extras.getString("androidPrivate");
|
androidPrivate = extras.getString("androidPrivate");
|
||||||
androidArgument = extras.getString("androidArgument");
|
androidArgument = extras.getString("androidArgument");
|
||||||
|
@ -75,28 +78,38 @@ public class PythonService extends Service implements Runnable {
|
||||||
pythonName = extras.getString("pythonName");
|
pythonName = extras.getString("pythonName");
|
||||||
pythonHome = extras.getString("pythonHome");
|
pythonHome = extras.getString("pythonHome");
|
||||||
pythonPath = extras.getString("pythonPath");
|
pythonPath = extras.getString("pythonPath");
|
||||||
|
boolean serviceStartAsForeground = (
|
||||||
|
extras.getString("serviceStartAsForeground").equals("true")
|
||||||
|
);
|
||||||
pythonServiceArgument = extras.getString("pythonServiceArgument");
|
pythonServiceArgument = extras.getString("pythonServiceArgument");
|
||||||
|
|
||||||
pythonThread = new Thread(this);
|
pythonThread = new Thread(this);
|
||||||
pythonThread.start();
|
pythonThread.start();
|
||||||
|
|
||||||
if (canDisplayNotification()) {
|
if (serviceStartAsForeground) {
|
||||||
doStartForeground(extras);
|
doStartForeground(extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
return startType();
|
return startType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected int getServiceId() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected void doStartForeground(Bundle extras) {
|
protected void doStartForeground(Bundle extras) {
|
||||||
String serviceTitle = extras.getString("serviceTitle");
|
String serviceTitle = extras.getString("serviceTitle");
|
||||||
String serviceDescription = extras.getString("serviceDescription");
|
String serviceDescription = extras.getString("serviceDescription");
|
||||||
|
|
||||||
Notification notification;
|
Notification notification;
|
||||||
Context context = getApplicationContext();
|
Context context = getApplicationContext();
|
||||||
Intent contextIntent = new Intent(context, PythonActivity.class);
|
Intent contextIntent = new Intent(context, PythonActivity.class);
|
||||||
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
|
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
notification = new Notification(
|
notification = new Notification(
|
||||||
context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis());
|
context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis());
|
||||||
try {
|
try {
|
||||||
|
@ -109,14 +122,26 @@ public class PythonService extends Service implements Runnable {
|
||||||
IllegalArgumentException | InvocationTargetException e) {
|
IllegalArgumentException | InvocationTargetException e) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Notification.Builder builder = new Notification.Builder(context);
|
// for android 8+ we need to create our own channel
|
||||||
|
// https://stackoverflow.com/questions/47531742/startforeground-fail-after-upgrade-to-android-8-1
|
||||||
|
String NOTIFICATION_CHANNEL_ID = "org.kivy.p4a"; //TODO: make this configurable
|
||||||
|
String channelName = "Background Service"; //TODO: make this configurable
|
||||||
|
NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName,
|
||||||
|
NotificationManager.IMPORTANCE_NONE);
|
||||||
|
|
||||||
|
chan.setLightColor(Color.BLUE);
|
||||||
|
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
||||||
|
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
manager.createNotificationChannel(chan);
|
||||||
|
|
||||||
|
Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||||
builder.setContentTitle(serviceTitle);
|
builder.setContentTitle(serviceTitle);
|
||||||
builder.setContentText(serviceDescription);
|
builder.setContentText(serviceDescription);
|
||||||
builder.setContentIntent(pIntent);
|
builder.setContentIntent(pIntent);
|
||||||
builder.setSmallIcon(context.getApplicationInfo().icon);
|
builder.setSmallIcon(context.getApplicationInfo().icon);
|
||||||
notification = builder.build();
|
notification = builder.build();
|
||||||
}
|
}
|
||||||
startForeground(1, notification);
|
startForeground(getServiceId(), notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -137,7 +162,10 @@ public class PythonService extends Service implements Runnable {
|
||||||
@Override
|
@Override
|
||||||
public void onTaskRemoved(Intent rootIntent) {
|
public void onTaskRemoved(Intent rootIntent) {
|
||||||
super.onTaskRemoved(rootIntent);
|
super.onTaskRemoved(rootIntent);
|
||||||
stopSelf();
|
//sticky servcie runtime/restart is managed by the OS. leave it running when app is closed
|
||||||
|
if (startType() != START_STICKY) {
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
package org.kivy.android;
|
package org.kivy.android;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.io.FilenameFilter;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.renpy.android.AssetExtract;
|
||||||
|
|
||||||
public class PythonUtil {
|
public class PythonUtil {
|
||||||
private static final String TAG = "pythonutil";
|
private static final String TAG = "pythonutil";
|
||||||
|
@ -32,21 +40,25 @@ public class PythonUtil {
|
||||||
|
|
||||||
protected static ArrayList<String> getLibraries(File libsDir) {
|
protected static ArrayList<String> getLibraries(File libsDir) {
|
||||||
ArrayList<String> libsList = new ArrayList<String>();
|
ArrayList<String> libsList = new ArrayList<String>();
|
||||||
addLibraryIfExists(libsList, "crystax", libsDir);
|
|
||||||
addLibraryIfExists(libsList, "sqlite3", libsDir);
|
addLibraryIfExists(libsList, "sqlite3", libsDir);
|
||||||
addLibraryIfExists(libsList, "ffi", libsDir);
|
addLibraryIfExists(libsList, "ffi", libsDir);
|
||||||
|
addLibraryIfExists(libsList, "png16", libsDir);
|
||||||
addLibraryIfExists(libsList, "ssl.*", libsDir);
|
addLibraryIfExists(libsList, "ssl.*", libsDir);
|
||||||
addLibraryIfExists(libsList, "crypto.*", libsDir);
|
addLibraryIfExists(libsList, "crypto.*", libsDir);
|
||||||
libsList.add("python2.7");
|
addLibraryIfExists(libsList, "SDL2", libsDir);
|
||||||
|
addLibraryIfExists(libsList, "SDL2_image", libsDir);
|
||||||
|
addLibraryIfExists(libsList, "SDL2_mixer", libsDir);
|
||||||
|
addLibraryIfExists(libsList, "SDL2_ttf", libsDir);
|
||||||
libsList.add("python3.5m");
|
libsList.add("python3.5m");
|
||||||
libsList.add("python3.6m");
|
libsList.add("python3.6m");
|
||||||
libsList.add("python3.7m");
|
libsList.add("python3.7m");
|
||||||
|
libsList.add("python3.8");
|
||||||
|
libsList.add("python3.9");
|
||||||
libsList.add("main");
|
libsList.add("main");
|
||||||
return libsList;
|
return libsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadLibraries(File filesDir, File libsDir) {
|
public static void loadLibraries(File filesDir, File libsDir) {
|
||||||
String filesDirPath = filesDir.getAbsolutePath();
|
|
||||||
boolean foundPython = false;
|
boolean foundPython = false;
|
||||||
|
|
||||||
for (String lib : getLibraries(libsDir)) {
|
for (String lib : getLibraries(libsDir)) {
|
||||||
|
@ -61,8 +73,8 @@ public class PythonUtil {
|
||||||
// load, and it has failed, give a more
|
// load, and it has failed, give a more
|
||||||
// general error
|
// general error
|
||||||
Log.v(TAG, "Library loading error: " + e.getMessage());
|
Log.v(TAG, "Library loading error: " + e.getMessage());
|
||||||
if (lib.startsWith("python3.7") && !foundPython) {
|
if (lib.startsWith("python3.9") && !foundPython) {
|
||||||
throw new java.lang.RuntimeException("Could not load any libpythonXXX.so");
|
throw new RuntimeException("Could not load any libpythonXXX.so");
|
||||||
} else if (lib.startsWith("python")) {
|
} else if (lib.startsWith("python")) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,5 +85,174 @@ public class PythonUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.v(TAG, "Loaded everything!");
|
Log.v(TAG, "Loaded everything!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getAppRoot(Context ctx) {
|
||||||
|
String appRoot = ctx.getFilesDir().getAbsolutePath() + "/app";
|
||||||
|
return appRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getResourceString(Context ctx, String name) {
|
||||||
|
// Taken from org.renpy.android.ResourceManager
|
||||||
|
Resources res = ctx.getResources();
|
||||||
|
int id = res.getIdentifier(name, "string", ctx.getPackageName());
|
||||||
|
return res.getString(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an error using a toast. (Only makes sense from non-UI threads.)
|
||||||
|
*/
|
||||||
|
protected static void toastError(final Activity activity, final String msg) {
|
||||||
|
activity.runOnUiThread(new Runnable () {
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait to show the error.
|
||||||
|
synchronized (activity) {
|
||||||
|
try {
|
||||||
|
activity.wait(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void recursiveDelete(File f) {
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
for (File r : f.listFiles()) {
|
||||||
|
recursiveDelete(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unpackAsset(
|
||||||
|
Context ctx,
|
||||||
|
final String resource,
|
||||||
|
File target,
|
||||||
|
boolean cleanup_on_version_update) {
|
||||||
|
|
||||||
|
Log.v(TAG, "Unpacking " + resource + " " + target.getName());
|
||||||
|
|
||||||
|
// The version of data in memory and on disk.
|
||||||
|
String dataVersion = getResourceString(ctx, resource + "_version");
|
||||||
|
String diskVersion = null;
|
||||||
|
|
||||||
|
Log.v(TAG, "Data version is " + dataVersion);
|
||||||
|
|
||||||
|
// If no version, no unpacking is necessary.
|
||||||
|
if (dataVersion == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the current disk version, if any.
|
||||||
|
String filesDir = target.getAbsolutePath();
|
||||||
|
String diskVersionFn = filesDir + "/" + resource + ".version";
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte buf[] = new byte[64];
|
||||||
|
InputStream is = new FileInputStream(diskVersionFn);
|
||||||
|
int len = is.read(buf);
|
||||||
|
diskVersion = new String(buf, 0, len);
|
||||||
|
is.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
diskVersion = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the disk data is out of date, extract it and write the version file.
|
||||||
|
if (! dataVersion.equals(diskVersion)) {
|
||||||
|
Log.v(TAG, "Extracting " + resource + " assets.");
|
||||||
|
|
||||||
|
if (cleanup_on_version_update) {
|
||||||
|
recursiveDelete(target);
|
||||||
|
}
|
||||||
|
target.mkdirs();
|
||||||
|
|
||||||
|
AssetExtract ae = new AssetExtract(ctx);
|
||||||
|
if (!ae.extractTar(resource + ".tar", target.getAbsolutePath(), "private")) {
|
||||||
|
String msg = "Could not extract " + resource + " data.";
|
||||||
|
if (ctx instanceof Activity) {
|
||||||
|
toastError((Activity)ctx, msg);
|
||||||
|
} else {
|
||||||
|
Log.v(TAG, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write .nomedia.
|
||||||
|
new File(target, ".nomedia").createNewFile();
|
||||||
|
|
||||||
|
// Write version file.
|
||||||
|
FileOutputStream os = new FileOutputStream(diskVersionFn);
|
||||||
|
os.write(dataVersion.getBytes());
|
||||||
|
os.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unpackPyBundle(
|
||||||
|
Context ctx,
|
||||||
|
final String resource,
|
||||||
|
File target,
|
||||||
|
boolean cleanup_on_version_update) {
|
||||||
|
|
||||||
|
Log.v(TAG, "Unpacking " + resource + " " + target.getName());
|
||||||
|
|
||||||
|
// The version of data in memory and on disk.
|
||||||
|
String dataVersion = getResourceString(ctx, "private_version");
|
||||||
|
String diskVersion = null;
|
||||||
|
|
||||||
|
Log.v(TAG, "Data version is " + dataVersion);
|
||||||
|
|
||||||
|
// If no version, no unpacking is necessary.
|
||||||
|
if (dataVersion == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the current disk version, if any.
|
||||||
|
String filesDir = target.getAbsolutePath();
|
||||||
|
String diskVersionFn = filesDir + "/" + "libpybundle" + ".version";
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte buf[] = new byte[64];
|
||||||
|
InputStream is = new FileInputStream(diskVersionFn);
|
||||||
|
int len = is.read(buf);
|
||||||
|
diskVersion = new String(buf, 0, len);
|
||||||
|
is.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
diskVersion = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! dataVersion.equals(diskVersion)) {
|
||||||
|
// If the disk data is out of date, extract it and write the version file.
|
||||||
|
Log.v(TAG, "Extracting " + resource + " assets.");
|
||||||
|
|
||||||
|
if (cleanup_on_version_update) {
|
||||||
|
recursiveDelete(target);
|
||||||
|
}
|
||||||
|
target.mkdirs();
|
||||||
|
|
||||||
|
AssetExtract ae = new AssetExtract(ctx);
|
||||||
|
if (!ae.extractTar(resource + ".so", target.getAbsolutePath(), "pybundle")) {
|
||||||
|
String msg = "Could not extract " + resource + " data.";
|
||||||
|
if (ctx instanceof Activity) {
|
||||||
|
toastError((Activity)ctx, msg);
|
||||||
|
} else {
|
||||||
|
Log.v(TAG, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write version file.
|
||||||
|
FileOutputStream os = new FileOutputStream(diskVersionFn);
|
||||||
|
os.write(dataVersion.getBytes());
|
||||||
|
os.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,36 +2,34 @@
|
||||||
// spaces amount
|
// spaces amount
|
||||||
package org.renpy.android;
|
package org.renpy.android;
|
||||||
|
|
||||||
import java.io.*;
|
import android.content.Context;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.FileInputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
|
import org.kamranzafar.jtar.TarEntry;
|
||||||
import org.kamranzafar.jtar.*;
|
import org.kamranzafar.jtar.TarInputStream;
|
||||||
|
|
||||||
public class AssetExtract {
|
public class AssetExtract {
|
||||||
|
|
||||||
private AssetManager mAssetManager = null;
|
private AssetManager mAssetManager = null;
|
||||||
private Activity mActivity = null;
|
|
||||||
|
|
||||||
public AssetExtract(Activity act) {
|
public AssetExtract(Context context) {
|
||||||
mActivity = act;
|
mAssetManager = context.getAssets();
|
||||||
mAssetManager = act.getAssets();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean extractTar(String asset, String target) {
|
public boolean extractTar(String asset, String target, String method) {
|
||||||
|
|
||||||
byte buf[] = new byte[1024 * 1024];
|
byte buf[] = new byte[1024 * 1024];
|
||||||
|
|
||||||
|
@ -39,7 +37,12 @@ public class AssetExtract {
|
||||||
TarInputStream tis = null;
|
TarInputStream tis = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
|
if(method == "private"){
|
||||||
|
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
|
||||||
|
} else if (method == "pybundle") {
|
||||||
|
assetStream = new FileInputStream(asset);
|
||||||
|
}
|
||||||
|
|
||||||
tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192));
|
tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e("python", "opening up extract tar", e);
|
Log.e("python", "opening up extract tar", e);
|
||||||
|
@ -51,7 +54,7 @@ public class AssetExtract {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
entry = tis.getNextEntry();
|
entry = tis.getNextEntry();
|
||||||
} catch ( java.io.IOException e ) {
|
} catch ( IOException e ) {
|
||||||
Log.e("python", "extracting tar", e);
|
Log.e("python", "extracting tar", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -76,8 +79,7 @@ public class AssetExtract {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
out = new BufferedOutputStream(new FileOutputStream(path), 8192);
|
out = new BufferedOutputStream(new FileOutputStream(path), 8192);
|
||||||
} catch ( FileNotFoundException e ) {
|
} catch ( FileNotFoundException | SecurityException e ) {}
|
||||||
} catch ( SecurityException e ) { };
|
|
||||||
|
|
||||||
if ( out == null ) {
|
if ( out == null ) {
|
||||||
Log.e("python", "could not open " + path);
|
Log.e("python", "could not open " + path);
|
||||||
|
@ -97,7 +99,7 @@ public class AssetExtract {
|
||||||
|
|
||||||
out.flush();
|
out.flush();
|
||||||
out.close();
|
out.close();
|
||||||
} catch ( java.io.IOException e ) {
|
} catch ( IOException e ) {
|
||||||
Log.e("python", "extracting zip", e);
|
Log.e("python", "extracting zip", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* This class takes care of managing resources for us. In our code, we
|
* This class takes care of managing resources for us. In our code, we
|
||||||
* can't use R, since the name of the package containing R will
|
* can't use R, since the name of the package containing R will
|
||||||
* change. (This same code is used in both org.renpy.android and
|
* change. So this is the next best thing.
|
||||||
* org.renpy.pygame.) So this is the next best thing.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.renpy.android;
|
package org.renpy.android;
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
package {{ args.package }};
|
package {{ args.package }};
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.app.Notification;
|
import {{ args.service_class_name }};
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import org.kivy.android.PythonService;
|
|
||||||
import org.kivy.android.PythonActivity;
|
|
||||||
|
|
||||||
|
|
||||||
public class Service{{ name|capitalize }} extends PythonService {
|
public class Service{{ name|capitalize }} extends {{ base_service_class }} {
|
||||||
{% if sticky %}
|
{% if sticky %}
|
||||||
@Override
|
@Override
|
||||||
public int startType() {
|
public int startType() {
|
||||||
|
@ -20,54 +13,35 @@ public class Service{{ name|capitalize }} extends PythonService {
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not foreground %}
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canDisplayNotification() {
|
protected int getServiceId() {
|
||||||
return false;
|
return {{ service_id }};
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doStartForeground(Bundle extras) {
|
|
||||||
Notification notification;
|
|
||||||
Context context = getApplicationContext();
|
|
||||||
Intent contextIntent = new Intent(context, PythonActivity.class);
|
|
||||||
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
|
||||||
notification = new Notification(
|
|
||||||
context.getApplicationInfo().icon, "{{ args.name }}", System.currentTimeMillis());
|
|
||||||
try {
|
|
||||||
// prevent using NotificationCompat, this saves 100kb on apk
|
|
||||||
Method func = notification.getClass().getMethod(
|
|
||||||
"setLatestEventInfo", Context.class, CharSequence.class,
|
|
||||||
CharSequence.class, PendingIntent.class);
|
|
||||||
func.invoke(notification, context, "{{ args.name }}", "{{ name| capitalize }}", pIntent);
|
|
||||||
} catch (NoSuchMethodException | IllegalAccessException |
|
|
||||||
IllegalArgumentException | InvocationTargetException e) {
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Notification.Builder builder = new Notification.Builder(context);
|
|
||||||
builder.setContentTitle("{{ args.name }}");
|
|
||||||
builder.setContentText("{{ name| capitalize }}");
|
|
||||||
builder.setContentIntent(pIntent);
|
|
||||||
builder.setSmallIcon(context.getApplicationInfo().icon);
|
|
||||||
notification = builder.build();
|
|
||||||
}
|
|
||||||
startForeground({{ service_id }}, notification);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public void start(Context ctx, String pythonServiceArgument) {
|
static public void start(Context ctx, String pythonServiceArgument) {
|
||||||
|
Intent intent = getDefaultIntent(ctx, pythonServiceArgument);
|
||||||
|
ctx.startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public Intent getDefaultIntent(Context ctx, String pythonServiceArgument) {
|
||||||
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
|
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
|
||||||
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
|
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
|
||||||
intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
|
intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
|
||||||
intent.putExtra("androidArgument", argument);
|
intent.putExtra("androidArgument", argument);
|
||||||
|
intent.putExtra("serviceTitle", "{{ args.name }}");
|
||||||
|
intent.putExtra("serviceDescription", "{{ name|capitalize }}");
|
||||||
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
|
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
|
||||||
intent.putExtra("pythonName", "{{ name }}");
|
intent.putExtra("pythonName", "{{ name }}");
|
||||||
|
intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}");
|
||||||
intent.putExtra("pythonHome", argument);
|
intent.putExtra("pythonHome", argument);
|
||||||
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
|
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
|
||||||
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
|
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
|
||||||
ctx.startService(intent);
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) {
|
||||||
|
return Service{{ name|capitalize }}.getDefaultIntent(ctx, pythonServiceArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
static public void stop(Context ctx) {
|
static public void stop(Context ctx) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ buildscript {
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.1.4'
|
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,23 +13,45 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
flatDir {
|
{%- for repo in args.gradle_repositories %}
|
||||||
dirs 'libs'
|
{{repo}}
|
||||||
}
|
{%- endfor %}
|
||||||
|
flatDir {
|
||||||
|
dirs 'libs'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% if is_library %}
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
{% else %}
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion {{ android_api }}
|
compileSdkVersion {{ android_api }}
|
||||||
buildToolsVersion '{{ build_tools_version }}'
|
buildToolsVersion '{{ build_tools_version }}'
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion {{ args.min_sdk_version }}
|
minSdkVersion {{ args.min_sdk_version }}
|
||||||
targetSdkVersion {{ android_api }}
|
targetSdkVersion {{ android_api }}
|
||||||
versionCode {{ args.numeric_version }}
|
versionCode {{ args.numeric_version }}
|
||||||
versionName '{{ args.version }}'
|
versionName '{{ args.version }}'
|
||||||
|
manifestPlaceholders = {{ args.manifest_placeholders}}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
{% if debug_build -%}
|
||||||
|
doNotStrip '**/*.so'
|
||||||
|
{% else %}
|
||||||
|
exclude 'lib/**/gdbserver'
|
||||||
|
exclude 'lib/**/gdb.setup'
|
||||||
|
{%- endif %}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{% if args.sign -%}
|
{% if args.sign -%}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
@ -40,41 +62,73 @@ android {
|
||||||
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
|
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
buildTypes {
|
{% if args.packaging_options -%}
|
||||||
debug {
|
packagingOptions {
|
||||||
}
|
{%- for option in args.packaging_options %}
|
||||||
release {
|
{{option}}
|
||||||
{% if args.sign -%}
|
{%- endfor %}
|
||||||
signingConfig signingConfigs.release
|
}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
}
|
|
||||||
}
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
{% if args.sign -%}
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
{%- endif %}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
{% if args.enable_androidx %}
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
{% else %}
|
||||||
sourceCompatibility JavaVersion.VERSION_1_7
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
targetCompatibility JavaVersion.VERSION_1_7
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
{% endif %}
|
||||||
|
{%- for option in args.compile_options %}
|
||||||
|
{{option}}
|
||||||
|
{%- endfor %}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
jniLibs.srcDir 'libs'
|
jniLibs.srcDir 'libs'
|
||||||
|
java {
|
||||||
|
|
||||||
|
{%- for adir, pattern in args.extra_source_dirs -%}
|
||||||
|
srcDir '{{adir}}'
|
||||||
|
{%- endfor -%}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aaptOptions {
|
||||||
|
noCompress "tflite"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
{%- for aar in aars %}
|
{%- for aar in aars %}
|
||||||
compile(name: '{{ aar }}', ext: 'aar')
|
implementation(name: '{{ aar }}', ext: 'aar')
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- for jar in jars %}
|
{%- for jar in jars %}
|
||||||
compile files('src/main/libs/{{ jar }}')
|
implementation files('src/main/libs/{{ jar }}')
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- if args.depends -%}
|
{%- if args.depends -%}
|
||||||
{%- for depend in args.depends %}
|
{%- for depend in args.depends %}
|
||||||
compile '{{ depend }}'
|
implementation '{{ depend }}'
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
{% if args.presplash_lottie %}
|
||||||
|
implementation 'com.airbnb.android:lottie:3.4.0'
|
||||||
|
{%- endif %}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{% if args.enable_androidx %}
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
{% endif %}
|
|
@ -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>
|
||||||
|
|
16
p4a/pythonforandroid/bootstraps/empty/__init__.py
Normal 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()
|
1
p4a/pythonforandroid/bootstraps/empty/build/.gitkeep
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
52
p4a/pythonforandroid/bootstraps/sdl2/__init__.py
Normal 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()
|
84
p4a/pythonforandroid/bootstraps/sdl2/build/blacklist.txt
Normal 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
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
#define BOOTSTRAP_NAME_SDL2
|
||||||
|
|
||||||
|
const char bootstrap_name[] = "SDL2"; // capitalized for historic reasons
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
@ -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 */;
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
||||||
|
from pythonforandroid.bootstraps.service_only import ServiceOnlyBootstrap
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceLibraryBootstrap(ServiceOnlyBootstrap):
|
||||||
|
|
||||||
|
name = 'service_library'
|
||||||
|
|
||||||
|
|
||||||
|
bootstrap = ServiceLibraryBootstrap()
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
#define BOOTSTRAP_NAME_LIBRARY
|
||||||
|
#define BOOTSTRAP_USES_NO_SDL_HEADERS
|
||||||
|
|
||||||
|
const char bootstrap_name[] = "service_library";
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
52
p4a/pythonforandroid/bootstraps/service_only/__init__.py
Normal 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()
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
49
p4a/pythonforandroid/bootstraps/webview/__init__.py
Normal 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()
|
91
p4a/pythonforandroid/bootstraps/webview/build/blacklist.txt
Normal 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
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
include $(call all-subdir-makefiles)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
#define BOOTSTRAP_NAME_WEBVIEW
|
||||||
|
#define BOOTSTRAP_USES_NO_SDL_HEADERS
|
||||||
|
|
||||||
|
const char bootstrap_name[] = "webview";
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
};
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 16 KiB |