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