Compare commits

..

No commits in common. "upgrade_p4a" and "master" have entirely different histories.

3445 changed files with 3815 additions and 456218 deletions

View file

@ -2,7 +2,7 @@ name: Publish Assets
on:
push:
branches: [master, upgrade_p4a]
branches: [master]
jobs:
build_arm64_aar:
@ -16,14 +16,20 @@ jobs:
cp -r /root/.buildozer ~/.buildozer/
- name: setup
run: |
apt update
apt install libssl-dev zip unzip openjdk-11-jdk -y
export B_VERSION=$(cat $GITHUB_WORKSPACE/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
echo "NEXUS_SIGNING_KEYRING_FILE=$GITHUB_WORKSPACE/signing2.pgp" >> $GITHUB_ENV
echo "BUILD_VERSION=${B_VERSION}" >> $GITHUB_ENV
export PATH=/usr/bin:$PATH
wget -q 'https://dl.google.com/android/repository/android-ndk-r25b-linux.zip' -P ~/.buildozer/android/
unzip ~/.buildozer/android/android-ndk-r25b-linux.zip -d ~/.buildozer/android/
wget -q 'https://eu.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
cp -f $GITHUB_WORKSPACE/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
cp -f $GITHUB_WORKSPACE/scripts/mangled-glibc-syscalls__arm64.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm64/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
cp -f $GITHUB_WORKSPACE/scripts/build-binary.mk ~/.buildozer/android/crystax-ndk-10.3.2/build/core/build-binary.mk
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/sources/sqlite
cp -Rf $GITHUB_WORKSPACE/scripts/crystax-sources/sqlite ~/.buildozer/android/crystax-ndk-10.3.2/sources/sqlite
rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
mv buildozer.spec.arm64.ci buildozer.spec
chmod u+x ./build-release.sh
- name: build release
@ -71,7 +77,6 @@ jobs:
echo "NEXUS_SIGNING_KEYRING_FILE=$GITHUB_WORKSPACE/signing2.pgp" >> $GITHUB_ENV
export PATH=/usr/bin:$PATH
wget -q 'https://eu.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
unzip ~/
tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9

1
.gitignore vendored
View file

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

View file

@ -1,257 +0,0 @@
from pythonforandroid.recipe import CythonRecipe, IncludedFilesBehaviour
from pythonforandroid.util import current_directory
from pythonforandroid import logger
from os.path import join
class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe):
# name = 'android'
version = None
url = None
src_filename = 'src'
depends = [('sdl2', 'genericndkbuild'), 'pyjnius']
config_env = {}
def get_recipe_env(self, arch):
env = super().get_recipe_env(arch)
env.update(self.config_env)
return env
def prebuild_arch(self, arch):
super().prebuild_arch(arch)
ctx_bootstrap = self.ctx.bootstrap.name
# define macros for Cython, C, Python
tpxi = 'DEF {} = {}\n'
th = '#define {} {}\n'
tpy = '{} = {}\n'
# make sure bootstrap name is in unicode
if isinstance(ctx_bootstrap, bytes):
ctx_bootstrap = ctx_bootstrap.decode('utf-8')
bootstrap = bootstrap_name = ctx_bootstrap
is_lbry = bootstrap_name in ('lbry',)
is_sdl2 = (bootstrap_name == "sdl2")
if bootstrap_name in ["sdl2", "webview", "service_only", "service_library", "lbry"]:
java_ns = u'org.kivy.android'
jni_ns = u'org/kivy/android'
else:
logger.error((
'unsupported bootstrap for android recipe: {}'
''.format(bootstrap_name)
))
exit(1)
config = {
'BOOTSTRAP': bootstrap,
'IS_SDL2': int(is_sdl2),
'PY2': 0,
'JAVA_NAMESPACE': java_ns,
'JNI_NAMESPACE': jni_ns,
'ACTIVITY_CLASS_NAME': self.ctx.activity_class_name,
'ACTIVITY_CLASS_NAMESPACE': self.ctx.activity_class_name.replace('.', '/'),
'SERVICE_CLASS_NAME': self.ctx.service_class_name,
}
# create config files for Cython, C and Python
with (
current_directory(self.get_build_dir(arch.arch))), (
open(join('android', 'config.pxi'), 'w')) as fpxi, (
open(join('android', 'config.h'), 'w')) as fh, (
open(join('android', 'config.py'), 'w')) as fpy:
for key, value in config.items():
fpxi.write(tpxi.format(key, repr(value)))
fpy.write(tpy.format(key, repr(value)))
fh.write(th.format(
key,
value if isinstance(value, int) else '"{}"'.format(value)
))
self.config_env[key] = str(value)
if is_sdl2:
fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n')
fh.write(
'#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n'
)
else:
fh.write('JNIEnv *WebView_AndroidGetJNIEnv(void);\n')
fh.write(
'#define SDL_ANDROID_GetJNIEnv WebView_AndroidGetJNIEnv\n'
)
recipe = AndroidRecipe()
'''
from pythonforandroid.recipe import CythonRecipe, IncludedFilesBehaviour
from pythonforandroid.util import current_directory
from pythonforandroid.patching import will_build
from pythonforandroid import logger
from os.path import join
class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe):
# name = 'android'
version = None
url = None
src_filename = 'src'
depends = [('pygame', 'sdl2', 'genericndkbuild'), ('python2', 'python3crystax')]
config_env = {}
def get_recipe_env(self, arch):
env = super(AndroidRecipe, self).get_recipe_env(arch)
env.update(self.config_env)
return env
def prebuild_arch(self, arch):
super(AndroidRecipe, self).prebuild_arch(arch)
tpxi = 'DEF {} = {}\n'
th = '#define {} {}\n'
tpy = '{} = {}\n'
bootstrap = bootstrap_name = self.ctx.bootstrap.name
is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3', 'sdl2_gradle')
is_pygame = bootstrap_name in ('pygame',)
is_webview = bootstrap_name in ('webview',)
is_lbry = bootstrap_name in ('lbry',)
if is_sdl2 or is_webview or is_lbry:
if is_sdl2:
bootstrap = 'sdl2'
java_ns = 'org.kivy.android'
jni_ns = 'org/kivy/android'
elif is_pygame:
java_ns = 'org.renpy.android'
jni_ns = 'org/renpy/android'
else:
logger.error('unsupported bootstrap for android recipe: {}'.format(bootstrap_name))
exit(1)
config = {
'BOOTSTRAP': bootstrap,
'IS_SDL2': int(is_sdl2),
'IS_PYGAME': int(is_pygame),
'PY2': int(will_build('python2')(self)),
'JAVA_NAMESPACE': java_ns,
'JNI_NAMESPACE': jni_ns,
}
with current_directory(self.get_build_dir(arch.arch)):
with open(join('android', 'config.pxi'), 'w') as fpxi:
with open(join('android', 'config.h'), 'w') as fh:
with open(join('android', 'config.py'), 'w') as fpy:
for key, value in config.items():
fpxi.write(tpxi.format(key, repr(value)))
fpy.write(tpy.format(key, repr(value)))
fh.write(th.format(key, value if isinstance(value, int)
else '"{}"'.format(value)))
self.config_env[key] = str(value)
if is_sdl2:
fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n')
fh.write('#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n')
elif is_pygame:
fh.write('JNIEnv *SDL_ANDROID_GetJNIEnv(void);\n')
recipe = AndroidRecipe()
'''
'''
from pythonforandroid.recipe import CythonRecipe, Recipe, IncludedFilesBehaviour
from pythonforandroid.util import current_directory
from pythonforandroid.patching import will_build
from pythonforandroid import logger
from os.path import join
class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe):
# name = 'android'
version = None
url = None
src_filename = 'src'
depends = [('pygame', 'sdl2', 'genericndkbuild'), ('python2', 'python3crystax')]
call_hostpython_via_targetpython = False
config_env = {}
def get_recipe_env(self, arch):
env = super(AndroidRecipe, self).get_recipe_env(arch)
env.update(self.config_env)
target_python = Recipe.get_recipe('python2', self.ctx).get_build_dir(arch.arch)
env['PYTHON_ROOT'] = join(target_python, 'python-install')
env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7'
env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + ' -lpython2.7'
return env
def prebuild_arch(self, arch):
super(AndroidRecipe, self).prebuild_arch(arch)
tpxi = 'DEF {} = {}\n'
th = '#define {} {}\n'
tpy = '{} = {}\n'
bootstrap = bootstrap_name = self.ctx.bootstrap.name
is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3')
is_pygame = bootstrap_name in ('pygame',)
is_webview = bootstrap_name in ('webview')
is_lbry = bootstrap_name in ('lbry')
if is_sdl2 or is_webview or is_lbry:
if is_sdl2:
bootstrap = 'sdl2'
java_ns = 'org.kivy.android'
jni_ns = 'org/kivy/android'
elif is_pygame:
java_ns = 'org.renpy.android'
jni_ns = 'org/renpy/android'
else:
logger.error('unsupported bootstrap for android recipe: {}'.format(bootstrap_name))
exit(1)
config = {
'BOOTSTRAP': bootstrap,
'IS_SDL2': int(is_sdl2),
'IS_PYGAME': int(is_pygame),
'PY2': int(will_build('python2')(self)),
'JAVA_NAMESPACE': java_ns,
'JNI_NAMESPACE': jni_ns,
}
with current_directory(self.get_build_dir(arch.arch)):
with open(join('android', 'config.pxi'), 'w') as fpxi:
with open(join('android', 'config.h'), 'w') as fh:
with open(join('android', 'config.py'), 'w') as fpy:
for key, value in config.items():
fpxi.write(tpxi.format(key, repr(value)))
fpy.write(tpy.format(key, repr(value)))
fh.write(th.format(key, value if isinstance(value, int)
else '"{}"'.format(value)))
self.config_env[key] = str(value)
if is_sdl2:
fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n')
fh.write('#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n')
elif is_pygame:
fh.write('JNIEnv *SDL_ANDROID_GetJNIEnv(void);\n')
recipe = AndroidRecipe()
'''

View file

@ -1,8 +0,0 @@
'''
Android module
==============
'''
# legacy import
from android._android import *

View file

@ -1,385 +0,0 @@
# Android-specific python services.
include "config.pxi"
IF BOOTSTRAP == 'pygame':
cdef extern int SDL_ANDROID_CheckPause()
cdef extern void SDL_ANDROID_WaitForResume() nogil
cdef extern void SDL_ANDROID_MapKey(int scancode, int keysym)
def check_pause():
return SDL_ANDROID_CheckPause()
def wait_for_resume():
android_accelerometer_enable(False)
SDL_ANDROID_WaitForResume()
android_accelerometer_enable(accelerometer_enabled)
def map_key(scancode, keysym):
SDL_ANDROID_MapKey(scancode, keysym)
# Android keycodes.
KEYCODE_UNKNOWN = 0
KEYCODE_SOFT_LEFT = 1
KEYCODE_SOFT_RIGHT = 2
KEYCODE_HOME = 3
KEYCODE_BACK = 4
KEYCODE_CALL = 5
KEYCODE_ENDCALL = 6
KEYCODE_0 = 7
KEYCODE_1 = 8
KEYCODE_2 = 9
KEYCODE_3 = 10
KEYCODE_4 = 11
KEYCODE_5 = 12
KEYCODE_6 = 13
KEYCODE_7 = 14
KEYCODE_8 = 15
KEYCODE_9 = 16
KEYCODE_STAR = 17
KEYCODE_POUND = 18
KEYCODE_DPAD_UP = 19
KEYCODE_DPAD_DOWN = 20
KEYCODE_DPAD_LEFT = 21
KEYCODE_DPAD_RIGHT = 22
KEYCODE_DPAD_CENTER = 23
KEYCODE_VOLUME_UP = 24
KEYCODE_VOLUME_DOWN = 25
KEYCODE_POWER = 26
KEYCODE_CAMERA = 27
KEYCODE_CLEAR = 28
KEYCODE_A = 29
KEYCODE_B = 30
KEYCODE_C = 31
KEYCODE_D = 32
KEYCODE_E = 33
KEYCODE_F = 34
KEYCODE_G = 35
KEYCODE_H = 36
KEYCODE_I = 37
KEYCODE_J = 38
KEYCODE_K = 39
KEYCODE_L = 40
KEYCODE_M = 41
KEYCODE_N = 42
KEYCODE_O = 43
KEYCODE_P = 44
KEYCODE_Q = 45
KEYCODE_R = 46
KEYCODE_S = 47
KEYCODE_T = 48
KEYCODE_U = 49
KEYCODE_V = 50
KEYCODE_W = 51
KEYCODE_X = 52
KEYCODE_Y = 53
KEYCODE_Z = 54
KEYCODE_COMMA = 55
KEYCODE_PERIOD = 56
KEYCODE_ALT_LEFT = 57
KEYCODE_ALT_RIGHT = 58
KEYCODE_SHIFT_LEFT = 59
KEYCODE_SHIFT_RIGHT = 60
KEYCODE_TAB = 61
KEYCODE_SPACE = 62
KEYCODE_SYM = 63
KEYCODE_EXPLORER = 64
KEYCODE_ENVELOPE = 65
KEYCODE_ENTER = 66
KEYCODE_DEL = 67
KEYCODE_GRAVE = 68
KEYCODE_MINUS = 69
KEYCODE_EQUALS = 70
KEYCODE_LEFT_BRACKET = 71
KEYCODE_RIGHT_BRACKET = 72
KEYCODE_BACKSLASH = 73
KEYCODE_SEMICOLON = 74
KEYCODE_APOSTROPHE = 75
KEYCODE_SLASH = 76
KEYCODE_AT = 77
KEYCODE_NUM = 78
KEYCODE_HEADSETHOOK = 79
KEYCODE_FOCUS = 80
KEYCODE_PLUS = 81
KEYCODE_MENU = 82
KEYCODE_NOTIFICATION = 83
KEYCODE_SEARCH = 84
KEYCODE_MEDIA_PLAY_PAUSE= 85
KEYCODE_MEDIA_STOP = 86
KEYCODE_MEDIA_NEXT = 87
KEYCODE_MEDIA_PREVIOUS = 88
KEYCODE_MEDIA_REWIND = 89
KEYCODE_MEDIA_FAST_FORWARD = 90
KEYCODE_MUTE = 91
# Vibration support.
cdef extern void android_vibrate(double)
def vibrate(s):
android_vibrate(s)
# Accelerometer support.
cdef extern void android_accelerometer_enable(int)
cdef extern void android_accelerometer_reading(float *)
accelerometer_enabled = False
def accelerometer_enable(p):
global accelerometer_enabled
android_accelerometer_enable(p)
accelerometer_enabled = p
def accelerometer_reading():
cdef float rv[3]
android_accelerometer_reading(rv)
return (rv[0], rv[1], rv[2])
# Wifi reading support
cdef extern void android_wifi_scanner_enable()
cdef extern char * android_wifi_scan()
def wifi_scanner_enable():
android_wifi_scanner_enable()
def wifi_scan():
cdef char * reading
reading = android_wifi_scan()
reading_list = []
for line in filter(lambda l: l, reading.split('\n')):
[ssid, mac, level] = line.split('\t')
reading_list.append((ssid.strip(), mac.upper().strip(), int(level)))
return reading_list
# DisplayMetrics information.
cdef extern int android_get_dpi()
def get_dpi():
return android_get_dpi()
# Soft keyboard.
cdef extern void android_show_keyboard(int)
cdef extern void android_hide_keyboard()
from jnius import autoclass, PythonJavaClass, java_method, cast
# API versions
api_version = autoclass('android.os.Build$VERSION').SDK_INT
version_codes = autoclass('android.os.Build$VERSION_CODES')
python_act = autoclass(JAVA_NAMESPACE + '.PythonActivity')
Rect = autoclass('android.graphics.Rect')
mActivity = python_act.mActivity
if mActivity:
# PyGame backend already has the listener so adding
# one here leads to a crash/too much cpu usage.
# SDL2 now does noe need the listener so there is
# no point adding a processor intensive layout listenere here.
height = 0
def get_keyboard_height():
rctx = Rect()
mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rctx)
# NOTE top should always be zero
rctx.top = 0
height = mActivity.getWindowManager().getDefaultDisplay().getHeight() - (rctx.bottom - rctx.top)
return height
else:
def get_keyboard_height():
return 0
# Flags for input_type, for requesting a particular type of keyboard
#android FLAGS
TYPE_CLASS_DATETIME = 4
TYPE_CLASS_NUMBER = 2
TYPE_NUMBER_VARIATION_NORMAL = 0
TYPE_NUMBER_VARIATION_PASSWORD = 16
TYPE_CLASS_TEXT = 1
TYPE_TEXT_FLAG_AUTO_COMPLETE = 65536
TYPE_TEXT_FLAG_AUTO_CORRECT = 32768
TYPE_TEXT_FLAG_NO_SUGGESTIONS = 524288
TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32
TYPE_TEXT_VARIATION_NORMAL = 0
TYPE_TEXT_VARIATION_PASSWORD = 128
TYPE_TEXT_VARIATION_POSTAL_ADDRESS = 112
TYPE_TEXT_VARIATION_URI = 16
TYPE_CLASS_PHONE = 3
IF BOOTSTRAP == 'sdl2':
def remove_presplash():
# Remove android presplash in SDL2 bootstrap.
mActivity.removeLoadingScreen()
def show_keyboard(target, input_type):
if input_type == 'text':
_input_type = TYPE_CLASS_TEXT
elif input_type == 'number':
_input_type = TYPE_CLASS_NUMBER
elif input_type == 'url':
_input_type = \
TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_URI
elif input_type == 'mail':
_input_type = \
TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_EMAIL_ADDRESS
elif input_type == 'datetime':
_input_type = TYPE_CLASS_DATETIME
elif input_type == 'tel':
_input_type = TYPE_CLASS_PHONE
elif input_type == 'address':
_input_type = TYPE_TEXT_VARIATION_POSTAL_ADDRESS
if hasattr(target, 'password') and target.password:
if _input_type == TYPE_CLASS_TEXT:
_input_type |= TYPE_TEXT_VARIATION_PASSWORD
elif _input_type == TYPE_CLASS_NUMBER:
_input_type |= TYPE_NUMBER_VARIATION_PASSWORD
if hasattr(target, 'keyboard_suggestions') and not target.keyboard_suggestions:
if _input_type == TYPE_CLASS_TEXT:
_input_type = TYPE_CLASS_TEXT | \
TYPE_TEXT_FLAG_NO_SUGGESTIONS
android_show_keyboard(_input_type)
def hide_keyboard():
android_hide_keyboard()
# Build info.
cdef extern char* BUILD_MANUFACTURER
cdef extern char* BUILD_MODEL
cdef extern char* BUILD_PRODUCT
cdef extern char* BUILD_VERSION_RELEASE
cdef extern void android_get_buildinfo()
class BuildInfo:
MANUFACTURER = None
MODEL = None
PRODUCT = None
VERSION_RELEASE = None
def get_buildinfo():
android_get_buildinfo()
binfo = BuildInfo()
binfo.MANUFACTURER = BUILD_MANUFACTURER
binfo.MODEL = BUILD_MODEL
binfo.PRODUCT = BUILD_PRODUCT
binfo.VERSION_RELEASE = BUILD_VERSION_RELEASE
return binfo
IF IS_PYGAME:
# Activate input - required to receive input events.
cdef extern void android_activate_input()
def init():
android_activate_input()
# Action send
cdef extern void android_action_send(char*, char*, char*, char*, char*)
def action_send(mimetype, filename=None, subject=None, text=None,
chooser_title=None):
cdef char *j_mimetype = <bytes>mimetype
cdef char *j_filename = NULL
cdef char *j_subject = NULL
cdef char *j_text = NULL
cdef char *j_chooser_title = NULL
if filename is not None:
j_filename = <bytes>filename
if subject is not None:
j_subject = <bytes>subject
if text is not None:
j_text = <bytes>text
if chooser_title is not None:
j_chooser_title = <bytes>chooser_title
android_action_send(j_mimetype, j_filename, j_subject, j_text,
j_chooser_title)
cdef extern int android_checkstop()
cdef extern void android_ackstop()
def check_stop():
return android_checkstop()
def ack_stop():
android_ackstop()
# -------------------------------------------------------------------
# URL Opening.
def open_url(url):
Intent = autoclass('android.content.Intent')
Uri = autoclass('android.net.Uri')
browserIntent = Intent()
browserIntent.setAction(Intent.ACTION_VIEW)
browserIntent.setData(Uri.parse(url))
currentActivity = cast('android.app.Activity', mActivity)
currentActivity.startActivity(browserIntent)
return True
# Web browser support.
class AndroidBrowser(object):
def open(self, url, new=0, autoraise=True):
return open_url(url)
def open_new(self, url):
return open_url(url)
def open_new_tab(self, url):
return open_url(url)
import webbrowser
webbrowser.register('android', AndroidBrowser, None, -1)
cdef extern void android_start_service(char *, char *, char *)
def start_service(title=None, description=None, arg=None):
cdef char *j_title = NULL
cdef char *j_description = NULL
if title is not None:
j_title = <bytes>title
if description is not None:
j_description = <bytes>description
if arg is not None:
j_arg = <bytes>arg
android_start_service(j_title, j_description, j_arg)
cdef extern void android_stop_service()
def stop_service():
android_stop_service()
class AndroidService(object):
'''Android service class.
Run ``service/main.py`` from application directory as a service.
:Parameters:
`title`: str, default to 'Python service'
Notification title.
`description`: str, default to 'Kivy Python service started'
Notification text.
'''
def __init__(self, title='Python service',
description='Kivy Python service started'):
self.title = title
self.description = description
def start(self, arg=''):
'''Start the service.
:Parameters:
`arg`: str, default to ''
Argument to pass to a service,
through environment variable ``PYTHON_SERVICE_ARGUMENT``.
'''
start_service(self.title, self.description, arg)
def stop(self):
'''Stop the service.
'''
stop_service()

View file

@ -1,81 +0,0 @@
# -------------------------------------------------------------------
# Billing
cdef extern void android_billing_service_start()
cdef extern void android_billing_service_stop()
cdef extern void android_billing_buy(char *sku)
cdef extern char *android_billing_get_purchased_items()
cdef extern char *android_billing_get_pending_message()
class BillingService(object):
BILLING_ACTION_SUPPORTED = 'billingsupported'
BILLING_ACTION_ITEMSCHANGED = 'itemschanged'
BILLING_TYPE_INAPP = 'inapp'
BILLING_TYPE_SUBSCRIPTION = 'subs'
def __init__(self, callback):
super(BillingService, self).__init__()
self.callback = callback
self.purchased_items = None
android_billing_service_start()
def _stop(self):
android_billing_service_stop()
def buy(self, sku):
cdef char *j_sku = <bytes>sku
android_billing_buy(j_sku)
def get_purchased_items(self):
cdef char *items = NULL
cdef bytes pitem
items = android_billing_get_purchased_items()
if items == NULL:
return []
pitems = items
ret = {}
for item in pitems.split('\n'):
if not item:
continue
sku, qt = item.split(',')
ret[sku] = {'qt': int(qt)}
return ret
def check(self, *largs):
cdef char *message
cdef bytes pymessage
while True:
message = android_billing_get_pending_message()
if message == NULL:
break
pymessage = <bytes>message
self._handle_message(pymessage)
if self.purchased_items is None:
self._check_new_items()
def _handle_message(self, message):
action, data = message.split('|', 1)
#print "HANDLE MESSAGE-----", (action, data)
if action == 'billingSupported':
tp, value = data.split('|')
value = True if value == '1' else False
self.callback(BillingService.BILLING_ACTION_SUPPORTED, tp, value)
elif action == 'requestPurchaseResponse':
self._check_new_items()
elif action == 'purchaseStateChange':
self._check_new_items()
elif action == 'restoreTransaction':
self._check_new_items()
def _check_new_items(self):
items = self.get_purchased_items()
if self.purchased_items != items:
self.purchased_items = items
self.callback(BillingService.BILLING_ACTION_ITEMSCHANGED, self.purchased_items)

View file

@ -1,120 +0,0 @@
#include <jni.h>
#include <stdio.h>
#include <android/log.h>
#include <string.h>
#include <stdlib.h>
#include "config.h"
#define aassert(x) { if (!x) { __android_log_print(ANDROID_LOG_ERROR, "android_jni", "Assertion failed. %s:%d", __FILE__, __LINE__); abort(); }}
#define PUSH_FRAME { (*env)->PushLocalFrame(env, 16); }
#define POP_FRAME { (*env)->PopLocalFrame(env, NULL); }
void android_billing_service_start() {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "billingServiceStart", "()V");
aassert(mid);
}
PUSH_FRAME;
(*env)->CallStaticVoidMethod(env, cls, mid);
POP_FRAME;
}
void android_billing_service_stop() {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "billingServiceStop", "()V");
aassert(mid);
}
PUSH_FRAME;
(*env)->CallStaticVoidMethod(env, cls, mid);
POP_FRAME;
}
void android_billing_buy(char *sku) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "billingBuy", "(Ljava/lang/String;)V");
aassert(mid);
}
PUSH_FRAME;
(*env)->CallStaticVoidMethod(
env, cls, mid,
(*env)->NewStringUTF(env, sku)
);
POP_FRAME;
}
char *android_billing_get_purchased_items() {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
jobject jreading;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "billingGetPurchasedItems", "()Ljava/lang/String;");
aassert(mid);
}
PUSH_FRAME;
jreading = (*env)->CallStaticObjectMethod(env, cls, mid);
const char * reading = (*env)->GetStringUTFChars(env, jreading, 0);
POP_FRAME;
return reading;
}
char *android_billing_get_pending_message() {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
jobject jreading;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "billingGetPendingMessage", "()Ljava/lang/String;");
aassert(mid);
}
PUSH_FRAME;
jreading = (*env)->CallStaticObjectMethod(env, cls, mid);
const char * reading = (*env)->GetStringUTFChars(env, jreading, 0);
POP_FRAME;
return reading;
}

View file

@ -1,358 +0,0 @@
#include <jni.h>
#include <stdio.h>
#include <android/log.h>
#include <string.h>
#include <stdlib.h>
#include "config.h"
#define aassert(x) { if (!x) { __android_log_print(ANDROID_LOG_ERROR, "android_jni", "Assertion failed. %s:%d", __FILE__, __LINE__); abort(); }}
#define PUSH_FRAME { (*env)->PushLocalFrame(env, 16); }
#define POP_FRAME { (*env)->PopLocalFrame(env, NULL); }
void android_vibrate(double seconds) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Hardware");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "vibrate", "(D)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
(jdouble) seconds);
}
void android_accelerometer_enable(int enable) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Hardware");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "accelerometerEnable", "(Z)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
(jboolean) enable);
}
void android_wifi_scanner_enable(void){
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Hardware");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "enableWifiScanner", "()V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(env, cls, mid);
}
char * android_wifi_scan() {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
jobject jreading;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Hardware");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "scanWifi", "()Ljava/lang/String;");
aassert(mid);
}
PUSH_FRAME;
jreading = (*env)->CallStaticObjectMethod(env, cls, mid);
const char * reading = (*env)->GetStringUTFChars(env, jreading, 0);
POP_FRAME;
return reading;
}
void android_accelerometer_reading(float *values) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
jobject jvalues;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Hardware");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "accelerometerReading", "()[F");
aassert(mid);
}
PUSH_FRAME;
jvalues = (*env)->CallStaticObjectMethod(env, cls, mid);
(*env)->GetFloatArrayRegion(env, jvalues, 0, 3, values);
POP_FRAME;
}
int android_get_dpi(void) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Hardware");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "getDPI", "()I");
aassert(mid);
}
return (*env)->CallStaticIntMethod(env, cls, mid);
}
void android_show_keyboard(int input_type) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Hardware");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "showKeyboard", "(I)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(env, cls, mid, (jint) input_type);
}
void android_hide_keyboard(void) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Hardware");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "hideKeyboard", "()V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(env, cls, mid);
}
char* BUILD_MANUFACTURER = NULL;
char* BUILD_MODEL = NULL;
char* BUILD_PRODUCT = NULL;
char* BUILD_VERSION_RELEASE = NULL;
void android_get_buildinfo() {
static JNIEnv *env = NULL;
if (env == NULL) {
jclass *cls = NULL;
jfieldID fid;
jstring sval;
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "android/os/Build");
fid = (*env)->GetStaticFieldID(env, cls, "MANUFACTURER", "Ljava/lang/String;");
sval = (jstring) (*env)->GetStaticObjectField(env, cls, fid);
BUILD_MANUFACTURER = (*env)->GetStringUTFChars(env, sval, 0);
fid = (*env)->GetStaticFieldID(env, cls, "MODEL", "Ljava/lang/String;");
sval = (jstring) (*env)->GetStaticObjectField(env, cls, fid);
BUILD_MODEL = (*env)->GetStringUTFChars(env, sval, 0);
fid = (*env)->GetStaticFieldID(env, cls, "PRODUCT", "Ljava/lang/String;");
sval = (jstring) (*env)->GetStaticObjectField(env, cls, fid);
BUILD_PRODUCT = (*env)->GetStringUTFChars(env, sval, 0);
cls = (*env)->FindClass(env, "android/os/Build$VERSION");
fid = (*env)->GetStaticFieldID(env, cls, "RELEASE", "Ljava/lang/String;");
sval = (jstring) (*env)->GetStaticObjectField(env, cls, fid);
BUILD_VERSION_RELEASE = (*env)->GetStringUTFChars(env, sval, 0);
}
}
#if IS_PYGAME
void android_activate_input(void) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/SDLSurfaceView");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "activateInput", "()V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(env, cls, mid);
}
int android_checkstop(void) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/SDLSurfaceView");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "checkStop", "()I");
aassert(mid);
}
return (*env)->CallStaticIntMethod(env, cls, mid);
}
void android_ackstop(void) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/SDLSurfaceView");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "ackStop", "()I");
aassert(mid);
}
(*env)->CallStaticIntMethod(env, cls, mid);
}
void android_action_send(char *mimeType, char *filename, char *subject, char *text, char *chooser_title) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/Action");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "send",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
aassert(mid);
}
jstring j_mimeType = (*env)->NewStringUTF(env, mimeType);
jstring j_filename = NULL;
jstring j_subject = NULL;
jstring j_text = NULL;
jstring j_chooser_title = NULL;
if ( filename != NULL )
j_filename = (*env)->NewStringUTF(env, filename);
if ( subject != NULL )
j_subject = (*env)->NewStringUTF(env, subject);
if ( text != NULL )
j_text = (*env)->NewStringUTF(env, text);
if ( chooser_title != NULL )
j_chooser_title = (*env)->NewStringUTF(env, text);
(*env)->CallStaticVoidMethod(
env, cls, mid,
j_mimeType, j_filename, j_subject, j_text,
j_chooser_title);
}
void android_open_url(char *url) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/SDLSurfaceView");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "openUrl", "(Ljava/lang/String;)V");
aassert(mid);
}
PUSH_FRAME;
(*env)->CallStaticVoidMethod(
env, cls, mid,
(*env)->NewStringUTF(env, url)
);
POP_FRAME;
}
#endif // IS_PYGAME
void android_start_service(char *title, char *description, char *arg) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "start_service",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
aassert(mid);
}
jstring j_title = NULL;
jstring j_description = NULL;
jstring j_arg = NULL;
if ( title != 0 )
j_title = (*env)->NewStringUTF(env, title);
if ( description != 0 )
j_description = (*env)->NewStringUTF(env, description);
if ( arg != 0 )
j_arg = (*env)->NewStringUTF(env, arg);
(*env)->CallStaticVoidMethod(env, cls, mid, j_title, j_description, j_arg);
}
void android_stop_service() {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "stop_service", "()V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(env, cls, mid);
}

View file

@ -1,125 +0,0 @@
cdef extern void android_sound_queue(int, char *, char *, long long, long long)
cdef extern void android_sound_play(int, char *, char *, long long, long long)
cdef extern void android_sound_stop(int)
cdef extern void android_sound_seek(int, float)
cdef extern void android_sound_dequeue(int)
cdef extern void android_sound_playing_name(int, char *, int)
cdef extern void android_sound_pause(int)
cdef extern void android_sound_unpause(int)
cdef extern void android_sound_set_volume(int, float)
cdef extern void android_sound_set_secondary_volume(int, float)
cdef extern void android_sound_set_pan(int, float)
cdef extern int android_sound_queue_depth(int)
cdef extern int android_sound_get_pos(int)
cdef extern int android_sound_get_length(int)
channels = set()
volumes = { }
def queue(channel, file, name, fadein=0, tight=False):
channels.add(channel)
real_fn = file.name
base = getattr(file, "base", -1)
length = getattr(file, "length", -1)
android_sound_queue(channel, name, real_fn, base, length)
def play(channel, file, name, paused=False, fadein=0, tight=False):
channels.add(channel)
real_fn = file.name
base = getattr(file, "base", -1)
length = getattr(file, "length", -1)
android_sound_play(channel, name, real_fn, base, length)
def seek(channel, position):
android_sound_seek(channel, position)
def stop(channel):
android_sound_stop(channel)
def dequeue(channel, even_tight=False):
android_sound_dequeue(channel)
def queue_depth(channel):
return android_sound_queue_depth(channel)
def playing_name(channel):
cdef char buf[1024]
android_sound_playing_name(channel, buf, 1024)
rv = buf
if not len(rv):
return None
return rv
def pause(channel):
android_sound_pause(channel)
return
def unpause(channel):
android_sound_unpause(channel)
return
def unpause_all():
for i in channels:
unpause(i)
def pause_all():
for i in channels:
pause(i)
def fadeout(channel, ms):
stop(channel)
def busy(channel):
return playing_name(channel) != None
def get_pos(channel):
return android_sound_get_pos(channel)
def get_length(channel):
return android_sound_get_length(channel)
def set_volume(channel, volume):
android_sound_set_volume(channel, volume)
volumes[channel] = volume
def set_secondary_volume(channel, volume):
android_sound_set_secondary_volume(channel, volume)
def set_pan(channel, pan):
android_sound_set_pan(channel, pan)
def set_end_event(channel, event):
return
def get_volume(channel):
return volumes.get(channel, 1.0)
def init(freq, stereo, samples, status=False):
return
def quit():
for i in channels:
stop(i)
def periodic():
return
def alloc_event(surf):
return
def refresh_event():
return
def check_version(version):
return

View file

@ -1,308 +0,0 @@
#include <jni.h>
#include <stdio.h>
#include <android/log.h>
#include <string.h>
JNIEnv *SDL_ANDROID_GetJNIEnv();
#define aassert(x) { if (!x) { __android_log_print(ANDROID_LOG_ERROR, "android_sound_jni", "Assertion failed. %s:%d", __FILE__, __LINE__); abort(); }}
#define PUSH_FRAME { (*env)->PushLocalFrame(env, 16); }
#define POP_FRAME { (*env)->PopLocalFrame(env, NULL); }
void android_sound_queue(int channel, char *filename, char *real_fn, long long base, long long length) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "queue", "(ILjava/lang/String;Ljava/lang/String;JJ)V");
aassert(mid);
}
PUSH_FRAME;
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel,
(*env)->NewStringUTF(env, filename),
(*env)->NewStringUTF(env, real_fn),
(jlong) base,
(jlong) length);
POP_FRAME;
}
void android_sound_play(int channel, char *filename, char *real_fn, long long base, long long length) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "play", "(ILjava/lang/String;Ljava/lang/String;JJ)V");
aassert(mid);
}
PUSH_FRAME;
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel,
(*env)->NewStringUTF(env, filename),
(*env)->NewStringUTF(env, real_fn),
(jlong) base,
(jlong) length);
POP_FRAME;
}
void android_sound_seek(int channel, float position){
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "seek", "(IF)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel,
(jfloat) position);
}
void android_sound_stop(int channel) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "stop", "(I)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel);
}
void android_sound_dequeue(int channel) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "dequeue", "(I)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel);
}
int android_sound_queue_depth(int channel) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "queue_depth", "(I)I");
aassert(mid);
}
(*env)->CallStaticIntMethod(
env, cls, mid,
channel);
}
void android_sound_playing_name(int channel, char *buf, int buflen) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
jobject s = NULL;
char *jbuf;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "playing_name", "(I)Ljava/lang/String;");
aassert(mid);
}
PUSH_FRAME;
s = (*env)->CallStaticObjectMethod(
env, cls, mid,
channel);
jbuf = (*env)->GetStringUTFChars(env, s, NULL);
strncpy(buf, jbuf, buflen);
(*env)->ReleaseStringUTFChars(env, s, jbuf);
POP_FRAME;
}
void android_sound_set_volume(int channel, float value) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "set_volume", "(IF)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel,
(jfloat) value);
}
void android_sound_set_secondary_volume(int channel, float value) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "set_secondary_volume", "(IF)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel,
(jfloat) value);
}
void android_sound_set_pan(int channel, float value) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "set_pan", "(IF)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel,
(jfloat) value);
}
void android_sound_pause(int channel) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "pause", "(I)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel);
}
void android_sound_unpause(int channel) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "unpause", "(I)V");
aassert(mid);
}
(*env)->CallStaticVoidMethod(
env, cls, mid,
channel);
}
int android_sound_get_pos(int channel) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "get_pos", "(I)I");
aassert(mid);
}
return (*env)->CallStaticIntMethod(
env, cls, mid,
channel);
}
int android_sound_get_length(int channel) {
static JNIEnv *env = NULL;
static jclass *cls = NULL;
static jmethodID mid = NULL;
if (env == NULL) {
env = SDL_ANDROID_GetJNIEnv();
aassert(env);
cls = (*env)->FindClass(env, "org/renpy/android/RenPySound");
aassert(cls);
mid = (*env)->GetStaticMethodID(env, cls, "get_length", "(I)I");
aassert(mid);
}
return (*env)->CallStaticIntMethod(
env, cls, mid,
channel);
}

View file

@ -1,61 +0,0 @@
from jnius import PythonJavaClass, java_method, autoclass, cast
from android.config import JAVA_NAMESPACE, JNI_NAMESPACE
_activity = autoclass(JAVA_NAMESPACE + '.PythonActivity').mActivity
_callbacks = {
'on_new_intent': [],
'on_activity_result': [] }
class NewIntentListener(PythonJavaClass):
__javainterfaces__ = [JNI_NAMESPACE + '/PythonActivity$NewIntentListener']
__javacontext__ = 'app'
def __init__(self, callback, **kwargs):
super(NewIntentListener, self).__init__(**kwargs)
self.callback = callback
@java_method('(Landroid/content/Intent;)V')
def onNewIntent(self, intent):
self.callback(intent)
class ActivityResultListener(PythonJavaClass):
__javainterfaces__ = [JNI_NAMESPACE + '/PythonActivity$ActivityResultListener']
__javacontext__ = 'app'
def __init__(self, callback):
super(ActivityResultListener, self).__init__()
self.callback = callback
@java_method('(IILandroid/content/Intent;)V')
def onActivityResult(self, requestCode, resultCode, intent):
self.callback(requestCode, resultCode, intent)
def bind(**kwargs):
for event, callback in kwargs.items():
if event not in _callbacks:
raise Exception('Unknown {!r} event'.format(event))
elif event == 'on_new_intent':
listener = NewIntentListener(callback)
_activity.registerNewIntentListener(listener)
_callbacks[event].append(listener)
elif event == 'on_activity_result':
listener = ActivityResultListener(callback)
_activity.registerActivityResultListener(listener)
_callbacks[event].append(listener)
def unbind(**kwargs):
for event, callback in kwargs.items():
if event not in _callbacks:
raise Exception('Unknown {!r} event'.format(event))
else:
for listener in _callbacks[event][:]:
if listener.callback == callback:
_callbacks[event].remove(listener)
if event == 'on_new_intent':
_activity.unregisterNewIntentListener(listener)
elif event == 'on_activity_result':
_activity.unregisterActivityResultListener(listener)

View file

@ -1,7 +0,0 @@
'''
Android Billing API
===================
'''
from android._android_billing import *

View file

@ -1,79 +0,0 @@
# -------------------------------------------------------------------
# Broadcast receiver bridge
from jnius import autoclass, PythonJavaClass, java_method
from android.config import JAVA_NAMESPACE, JNI_NAMESPACE
class BroadcastReceiver(object):
class Callback(PythonJavaClass):
__javainterfaces__ = [JNI_NAMESPACE + '/GenericBroadcastReceiverCallback']
__javacontext__ = 'app'
def __init__(self, callback, *args, **kwargs):
self.callback = callback
PythonJavaClass.__init__(self, *args, **kwargs)
@java_method('(Landroid/content/Context;Landroid/content/Intent;)V')
def onReceive(self, context, intent):
self.callback(context, intent)
def __init__(self, callback, actions=None, categories=None):
super(BroadcastReceiver, self).__init__()
self.callback = callback
if not actions and not categories:
raise Exception('You need to define at least actions or categories')
def _expand_partial_name(partial_name):
if '.' in partial_name:
return partial_name # Its actually a full dotted name
else:
name = 'ACTION_{}'.format(partial_name.upper())
if not hasattr(Intent, name):
raise Exception('The intent {} doesnt exist'.format(name))
return getattr(Intent, name)
# resolve actions/categories first
Intent = autoclass('android.content.Intent')
resolved_actions = [_expand_partial_name(x) for x in actions or []]
resolved_categories = [_expand_partial_name(x) for x in categories or []]
# resolve android API
GenericBroadcastReceiver = autoclass(JAVA_NAMESPACE + '.GenericBroadcastReceiver')
IntentFilter = autoclass('android.content.IntentFilter')
HandlerThread = autoclass('android.os.HandlerThread')
# create a thread for handling events from the receiver
self.handlerthread = HandlerThread('handlerthread')
# create a listener
self.listener = BroadcastReceiver.Callback(self.callback)
self.receiver = GenericBroadcastReceiver(self.listener)
self.receiver_filter = IntentFilter()
for x in resolved_actions:
self.receiver_filter.addAction(x)
for x in resolved_categories:
self.receiver_filter.addCategory(x)
def start(self):
Handler = autoclass('android.os.Handler')
self.handlerthread.start()
self.handler = Handler(self.handlerthread.getLooper())
self.context.registerReceiver(self.receiver, self.receiver_filter, None,
self.handler)
def stop(self):
self.context.unregisterReceiver(self.receiver)
self.handlerthread.quit()
@property
def context(self):
from os import environ
if 'PYTHON_SERVICE_ARGUMENT' in environ:
PythonService = autoclass(JAVA_NAMESPACE + '.PythonService')
return PythonService.mService
PythonActivity = autoclass(JAVA_NAMESPACE + '.PythonActivity')
return PythonActivity.mActivity

View file

@ -1,309 +0,0 @@
# This module is, as much a possible, a clone of the pygame
# mixer api.
import android._android_sound as sound
import time
import threading
import os
condition = threading.Condition()
def periodic():
for i in range(0, num_channels):
if i in channels:
channels[i].periodic()
num_channels = 8
reserved_channels = 0
def init(frequency=22050, size=-16, channels=2, buffer=4096):
return None
def pre_init(frequency=22050, size=-16, channels=2, buffersize=4096):
return None
def quit():
stop()
return None
def stop():
for i in range(0, num_channels):
sound.stop(i)
def pause():
for i in range(0, num_channels):
sound.pause(i)
def unpause():
for i in range(0, num_channels):
sound.unpause(i)
def get_busy():
for i in range(0, num_channels):
if sound.busy(i):
return True
return False
def fadeout(time):
# Fadeout doesn't work - it just immediately stops playback.
stop()
# A map from channel number to Channel object.
channels = { }
def set_num_channels(count):
global num_channels
num_channels = count
def get_num_channels(count):
return num_channels
def set_reserved(count):
global reserved_channels
reserved_channels = count
def find_channel(force=False):
busy = [ ]
for i in range(reserved_channels, num_channels):
c = Channel(i)
if not c.get_busy():
return c
busy.append(c)
if not force:
return None
return min(busy, key=lambda x : x.play_time)
class ChannelImpl(object):
def __init__(self, id):
self.id = id
self.loop = None
self.queued = None
self.play_time = time.time()
def periodic(self):
qd = sound.queue_depth(self.id)
if qd < 2:
self.queued = None
if self.loop is not None and sound.queue_depth(self.id) < 2:
self.queue(self.loop, loops=1)
def play(self, s, loops=0, maxtime=0, fade_ms=0):
if loops:
self.loop = s
sound.play(self.id, s.file, s.serial)
self.play_time = time.time()
with condition:
condition.notify()
def seek(self, position):
sound.seek(self.id, position)
def stop(self):
self.loop = None
sound.stop(self.id)
def pause(self):
sound.pause(self.id)
def unpause(self):
sound.pause(self.id)
def fadeout(self, time):
# No fadeout
self.stop()
def set_volume(self, left, right=None):
sound.set_volume(self.id, left)
def get_volume(self):
return sound.get_volume(self.id)
def get_busy(self):
return sound.busy(self.id)
def get_sound(self):
is_busy = sound.busy(self.id)
if not is_busy:
return
serial = sound.playing_name(self.id)
if not serial:
return
return sounds.get(serial, None)
def queue(self, s):
self.loop = None
self.queued = s
sound.queue(self.id, s.what, s.serial)
with condition:
condition.notify()
def get_queue(self):
return self.queued
def get_pos(self):
return sound.get_pos(self.id)/1000.
def get_length(self):
return sound.get_length(self.id)/1000.
def Channel(n):
"""
Gets the channel with the given number.
"""
rv = channels.get(n, None)
if rv is None:
rv = ChannelImpl(n)
channels[n] = rv
return rv
sound_serial = 0
sounds = { }
class Sound(object):
def __init__(self, what):
# Doesn't support buffers.
global sound_serial
self._channel = None
self._volume = 1.
self.serial = str(sound_serial)
sound_serial += 1
if isinstance(what, file):
self.file = what
else:
self.file = file(os.path.abspath(what), "rb")
sounds[self.serial] = self
def play(self, loops=0, maxtime=0, fade_ms=0):
# avoid new play if the sound is already playing
# -> same behavior as standard pygame.
if self._channel is not None:
if self._channel.get_sound() is self:
return
self._channel = channel = find_channel(True)
channel.set_volume(self._volume)
channel.play(self, loops=loops)
return channel
def stop(self):
for i in range(0, num_channels):
if Channel(i).get_sound() is self:
Channel(i).stop()
def fadeout(self, time):
self.stop()
def set_volume(self, left, right=None):
self._volume = left
if self._channel:
if self._channel.get_sound() is self:
self._channel.set_volume(self._volume)
def get_volume(self):
return self._volume
def get_num_channels(self):
rv = 0
for i in range(0, num_channels):
if Channel(i).get_sound() is self:
rv += 1
return rv
def get_length(self):
return 1.0
music_channel = Channel(256)
music_sound = None
class music(object):
@staticmethod
def load(filename):
music_channel.stop()
global music_sound
music_sound = Sound(filename)
@staticmethod
def play(loops=0, start=0.0):
# No start.
music_channel.play(music_sound, loops=loops)
@staticmethod
def rewind():
music_channel.play(music_sound)
@staticmethod
def seek(position):
music_channel.seek(position)
@staticmethod
def stop():
music_channel.stop()
@staticmethod
def pause():
music_channel.pause()
@staticmethod
def unpause():
music_channel.unpause()
@staticmethod
def fadeout(time):
music_channel.fadeout(time)
@staticmethod
def set_volume(value):
music_channel.set_volume(value)
@staticmethod
def get_volume():
return music_channel.get_volume()
@staticmethod
def get_busy():
return music_channel.get_busy()
@staticmethod
def get_pos():
return music_channel.get_pos()
@staticmethod
def queue(filename):
return music_channel.queue(Sound(filename))

View file

@ -1,48 +0,0 @@
'''
Runnable
========
'''
from jnius import PythonJavaClass, java_method, autoclass
from android.config import JAVA_NAMESPACE
# reference to the activity
_PythonActivity = autoclass(JAVA_NAMESPACE + '.PythonActivity')
class Runnable(PythonJavaClass):
'''Wrapper around Java Runnable class. This class can be used to schedule a
call of a Python function into the PythonActivity thread.
'''
__javainterfaces__ = ['java/lang/Runnable']
__runnables__ = []
def __init__(self, func):
super(Runnable, self).__init__()
self.func = func
def __call__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
Runnable.__runnables__.append(self)
_PythonActivity.mActivity.runOnUiThread(self)
@java_method('()V')
def run(self):
try:
self.func(*self.args, **self.kwargs)
except:
import traceback
traceback.print_exc()
Runnable.__runnables__.remove(self)
def run_on_ui_thread(f):
'''Decorator to create automatically a :class:`Runnable` object with the
function. The function will be delayed and call into the Activity thread.
'''
def f2(*args, **kwargs):
Runnable(f)(*args, **kwargs)
return f2

View file

@ -1,34 +0,0 @@
from distutils.core import setup, Extension
import os
library_dirs = ['libs/' + os.environ['ARCH']]
lib_dict = {
'pygame': ['sdl'],
'sdl2': ['SDL2', 'SDL2_image', 'SDL2_mixer', 'SDL2_ttf']
}
sdl_libs = lib_dict[os.environ['BOOTSTRAP']] if os.environ['BOOTSTRAP'] == 'sdl2' else []
renpy_sound = Extension('android._android_sound',
['android/_android_sound.c', 'android/_android_sound_jni.c', ],
libraries=sdl_libs + ['log'],
library_dirs=library_dirs)
modules = [Extension('android._android',
['android/_android.c', 'android/_android_jni.c'],
libraries=sdl_libs + ['log'],
library_dirs=library_dirs),
Extension('android._android_billing',
['android/_android_billing.c', 'android/_android_billing_jni.c'],
libraries=['log'],
library_dirs=library_dirs)]
if int(os.environ['IS_PYGAME']):
modules.append(renpy_sound)
setup(name='android',
version='1.0',
packages=['android'],
package_dir={'android': 'android'},
ext_modules=modules
)

View file

@ -1,74 +0,0 @@
import os
from pythonforandroid.recipe import PythonRecipe, CompiledComponentsPythonRecipe
class CoincurveRecipe(CompiledComponentsPythonRecipe):
# version = '15.0.0'
# url = 'https://github.com/ofek/coincurve/archive/{version}.tar.gz'
# call_hostpython_via_targetpython = False
# depends = ['setuptools',
# 'libffi', 'cffi', 'libsecp256k1']
# patches = [
# "cross_compile.patch", "drop_setup_requires.patch",
# "find_lib.patch", "no-download.patch", "setup.py.patch"]
#
# def get_recipe_env(self, arch=None, with_flags_in_cc=True):
# env = super().get_recipe_env(arch, with_flags_in_cc)
# # sets linker to use the correct gcc (cross compiler)
# env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions'
# libsecp256k1 = self.get_recipe('libsecp256k1', self.ctx)
# libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch)
# env['LDFLAGS'] += ' -L{}'.format(os.path.join(libsecp256k1_dir, '.libs'))
# env['CFLAGS'] += ' -I' + os.path.join(libsecp256k1_dir, 'include')
# # only keeps major.minor (discards patch)
# python_version = self.ctx.python_recipe.version[0:3]
# # required additional library and path for Crystax
# if self.ctx.ndk == 'crystax':
# ndk_dir_python = os.path.join(self.ctx.ndk_dir, 'sources/python/', python_version)
# env['LDFLAGS'] += ' -L{}'.format(os.path.join(ndk_dir_python, 'libs', arch.arch))
# env['LDFLAGS'] += ' -lpython{}'.format(python_version)
# # until `pythonforandroid/archs.py` gets merged upstream:
# # https://github.com/kivy/python-for-android/pull/1250/files#diff-569e13021e33ced8b54385f55b49cbe6
# env['CFLAGS'] += ' -I{}/include/python/'.format(ndk_dir_python)
# else:
# env['PYTHON_ROOT'] = self.ctx.get_python_install_dir(arch.arch)
# env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python{}'.format(python_version)
# env['LDFLAGS'] += " -lpython{}".format(python_version)
# env['LDFLAGS'] += " -lsecp256k1"
# return env
version = 'v15.0.1'
url = 'https://github.com/ofek/coincurve/archive/refs/tags/v15.0.1.tar.gz'
call_hostpython_via_targetpython = False
depends = [('python2', 'python3'), 'setuptools',
'libffi', 'cffi', 'libsecp256k1']
patches = [
"cross_compile.patch", "drop_setup_requires.patch",
"find_lib.patch", "no-download.patch", "setup.py.patch"]
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
env = super().get_recipe_env(arch, with_flags_in_cc)
# sets linker to use the correct gcc (cross compiler)
env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions'
libsecp256k1 = self.get_recipe('libsecp256k1', self.ctx)
libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch)
env['LDFLAGS'] += ' -L{}'.format(os.path.join(libsecp256k1_dir, '.libs'))
env['CFLAGS'] += ' -I' + os.path.join(libsecp256k1_dir, 'include')
# only keeps major.minor (discards patch)
python_version = self.ctx.python_recipe.version[0:3]
# required additional library and path for Crystax
# if self.ctx.ndk == 'crystax':
# ndk_dir_python = os.path.join(self.ctx.ndk_dir, 'sources/python/', python_version)
# env['LDFLAGS'] += ' -L{}'.format(os.path.join(ndk_dir_python, 'libs', arch.arch))
# env['LDFLAGS'] += ' -lpython{}m'.format(python_version)
# # until `pythonforandroid/archs.py` gets merged upstream:
# # https://github.com/kivy/python-for-android/pull/1250/files#diff-569e13021e33ced8b54385f55b49cbe6
# env['CFLAGS'] += ' -I{}/include/python/'.format(ndk_dir_python)
# else:
env['PYTHON_ROOT'] = self.ctx.get_python_install_dir(arch.arch)
env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python{}'.format(python_version)
env['LDFLAGS'] += " -lpython{}".format(python_version)
env['LDFLAGS'] += " -lsecp256k1"
return env
recipe = CoincurveRecipe()

View file

@ -1,12 +0,0 @@
diff --git a/setup.py b/setup.py
index c224fb2..bf925bd 100644
--- a/setup.py
+++ b/setup.py
@@ -182,6 +182,7 @@ class build_clib(_build_clib):
'--disable-dependency-tracking',
'--with-pic',
'--enable-module-recovery',
+ "--host=%s" % os.environ['TOOLCHAIN_PREFIX'],
'--disable-jni',
'--prefix',
os.path.abspath(self.build_clib),

View file

@ -1,12 +0,0 @@
diff --git a/setup.py b/setup.py
index c224fb2..466e789 100644
--- a/setup.py
+++ b/setup.py
@@ -250,7 +250,6 @@ else:
def has_c_libraries(self):
return not has_system_lib()
setup_kwargs = dict(
- setup_requires=['cffi>=1.3.0', 'pytest-runner>=2.6.2'],
ext_package='coincurve',
cffi_modules=['_cffi_build/build.py:ffi'],
cmdclass={

View file

@ -1,13 +0,0 @@
diff --git a/setup_support.py b/setup_support.py
index e7a4f2e..72f0c4d 100644
--- a/setup_support.py
+++ b/setup_support.py
@@ -68,6 +69,8 @@ def build_flags(library, type_, path):
def _find_lib():
+ # we're picking up the recipe one
+ return True
from cffi import FFI
ffi = FFI()
try:

View file

@ -1,13 +0,0 @@
diff --git a/setup.py b/setup.py
index c224fb2..d5f6d1a 100644
--- a/setup.py
+++ b/setup.py
@@ -51,6 +51,8 @@ if [int(i) for i in setuptools_version.split('.', 2)[:2]] < [3, 3]:
def download_library(command):
+ # we will use the custom libsecp256k1 recipe
+ return
if command.dry_run:
return
libdir = absolute('libsecp256k1')

View file

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

View file

@ -24,7 +24,6 @@ RUN wget 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildoz
RUN tar -xvf ~/.buildozer/android/platform/android-sdk_r23-linux.tgz -C ~/.buildozer/android/platform/ && \
mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk && \
unzip ~/.buildozer/android/platform/platform-28_r06.zip -d ~/.buildozer/android/platform/android-sdk/platforms && \
# android 9 (pie) comes with api level 28.
mv ~/.buildozer/android/platform/android-sdk/platforms/android-9 ~/.buildozer/android/platform/android-sdk/platforms/android-28 && \
mkdir -p ~/.buildozer/android/platform/android-sdk/build-tools && \
unzip ~/.buildozer/android/platform/build-tools_r28.0.3-linux.zip -d ~/.buildozer/android/platform/android-sdk/build-tools && \
@ -46,10 +45,3 @@ RUN git clone https://github.com/lbryio/buildozer.git
RUN cd buildozer && python setup.py install && cd ..
CMD ["/bin/bash"]
# result is
# ./root/.buildozer/android/platform/android-sdk/[build-tools,cmdline-tools,licenses,platforms,tools]
# ...build-tools/28.0.3/
# ...cmdline-tools/5.0/[bin,lib,source.properties]
# ...platforms/android-28/[]
# ...tools/[]

View file

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

View file

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

View file

@ -1,46 +1,22 @@
from distutils.spawn import find_executable
from os import environ
from os.path import join
from multiprocessing import cpu_count
from os.path import (exists, join, dirname, split)
from glob import glob
from pythonforandroid.recipe import Recipe
from pythonforandroid.util import BuildInterruptingException, build_platform
class Arch:
class Arch(object):
toolchain_prefix = None
'''The prefix for the toolchain dir in the NDK.'''
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.
@ -52,14 +28,6 @@ class Arch:
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 [
@ -70,235 +38,216 @@ class Arch:
@property
def target(self):
# 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
)
target_data = self.command_prefix.split('-')
return '-'.join(
[target_data[0], 'none', target_data[1], target_data[2]])
@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):
def get_env(self, with_flags_in_cc=True, clang=False):
env = {}
# HOME: User's home directory
#
# Many tools including p4a store outputs in the user's home
# directory. This is found from the HOME environment variable
# and falls back to the system account database. Setting HOME
# can be used to globally divert these tools to use a different
# path. Furthermore, in containerized environments the user may
# not exist in the account database, so if HOME isn't set than
# these tools will fail.
if 'HOME' in environ:
env['HOME'] = environ['HOME']
cflags = [
'-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))
# 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']
env['CFLAGS'] = ' '.join(cflags)
# CPPFLAGS (for macros and includes)
env['CPPFLAGS'] = ' '.join(self.common_cppflags).format(
ctx=self.ctx,
command_prefix=self.command_prefix,
python_includes=join(
self.ctx.get_python_install_dir(self.arch),
'include/python{}'.format(self.ctx.python_recipe.version[0:3]),
),
)
# LDFLAGS: Link the extra global link paths first before anything else
# Link the extra global link paths first before anything else
# (such that overriding system libraries with them is possible)
env['LDFLAGS'] = (
' '
+ " ".join(
[
"-L'"
+ link_path.replace("'", "'\"'\"'")
+ "'" # no shlex.quote in py2
for link_path in self.extra_global_link_paths
]
)
+ ' ' + ' '.join(self.common_ldflags).format(
ctx_libs_dir=self.ctx.get_libs_dir(self.arch)
)
)
env['LDFLAGS'] = ' ' + " ".join([
"-L'" + l.replace("'", "'\"'\"'") + "'" # no shlex.quote in py2
for l in self.extra_global_link_paths
]) + ' '
# LDLIBS: Library flags or names given to compilers when they are
# supposed to invoke the linker.
env['LDLIBS'] = ' '.join(self.common_ldlibs)
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
# 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_')})
# 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 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'])
if cc is None:
print('Searching path are: {!r}'.format(env['PATH']))
print('Searching path are: {!r}'.format(environ['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(self.clang_exe))
'installed. Exiting.'.format(exe))
if with_flags_in_cc:
env['CC'] = '{ccache}{exe} {cflags}'.format(
exe=self.clang_exe,
exe=exe,
ccache=ccache,
cflags=env['CFLAGS'])
env['CXX'] = '{ccache}{execxx} {cxxflags}'.format(
execxx=self.clang_exe_cxx,
execxx=execxx,
ccache=ccache,
cxxflags=env['CXXFLAGS'])
else:
env['CC'] = '{ccache}{exe}'.format(
exe=self.clang_exe,
exe=exe,
ccache=ccache)
env['CXX'] = '{ccache}{execxx}'.format(
execxx=self.clang_exe_cxx,
execxx=execxx,
ccache=ccache)
# 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['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)
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),
'native-build',
'build',
'lib.{}-{}'.format(
build_platform,
self.ctx.python_recipe.major_minor_version_string,
),
'build', 'lib.{}-{}'.format(
build_platform, self.ctx.python_recipe.major_minor_version_string)
)
# 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]
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
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 '{triplet}{ndk_api}'.format(
triplet='-'.join(['armv7a', target_data[1], target_data[2]]),
ndk_api=self.ctx.ndk_api,
)
return '-'.join(
['armv7a', 'none', target_data[1], target_data[2]])
class ArchARMv7_a(ArchARM):
arch = 'armeabi-v7a'
arch_cflags = [
'-march=armv7-a',
'-mfloat-abi=softfp',
'-mfpu=vfp',
'-mthumb',
'-fPIC',
]
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
class Archx86(Arch):
arch = 'x86'
toolchain_prefix = 'x86'
command_prefix = 'i686-linux-android'
arch_cflags = [
'-march=i686',
'-mssse3',
'-mfpmath=sse',
'-m32',
'-fPIC',
]
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
class Archx86_64(Arch):
arch = 'x86_64'
toolchain_prefix = 'x86_64'
command_prefix = 'x86_64-linux-android'
arch_cflags = [
'-march=x86-64',
'-msse4.2',
'-mpopcnt',
'-m64',
'-fPIC',
]
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
class ArchAarch_64(Arch):
arch = 'arm64-v8a'
toolchain_prefix = 'aarch64-linux-android'
command_prefix = 'aarch64-linux-android'
arch_cflags = [
'-march=armv8-a',
'-fPIC'
# '-I' + join(dirname(__file__), 'includes', 'arm64-v8a'),
]
platform_dir = 'arch-arm64'
# 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
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

View file

@ -1,4 +1,6 @@
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
@ -14,16 +16,16 @@ def argv_contains(t):
return False
class Bdist(Command):
class BdistAPK(Command):
description = 'Create an APK with python-for-android'
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(self.package_type)
option_dict = self.distribution.get_option_dict('apk')
# This is a hack, we probably aren't supposed to loop through
# the option_dict so early because distutils does exactly the
@ -32,9 +34,10 @@ class Bdist(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(self.package_type)
setup_options = self.distribution.get_option_dict('apk')
for (option, (source, value)) in setup_options.items():
if source == 'command line':
continue
@ -67,15 +70,16 @@ class Bdist(Command):
sys.argv.append('--version={}'.format(version))
if not argv_contains('--arch'):
arch = 'armeabi-v7a'
arch = 'arm64-v8a'
self.arch = arch
sys.argv.append('--arch={}'.format(arch))
def run(self):
self.prepare_build_dir()
from pythonforandroid.entrypoints import main
sys.argv[1] = self.package_type
from pythonforandroid.toolchain import main
sys.argv[1] = 'apk'
main()
def prepare_build_dir(self):
@ -108,7 +112,7 @@ class Bdist(Command):
makedirs(new_dir)
print('Including {}'.format(filen))
copyfile(filen, join(bdist_dir, filen))
if basename(filen) in ('main.py', 'main.pyc'):
if basename(filen) in ('main.py', 'main.pyo'):
main_py_dirs.append(filen)
# This feels ridiculous, but how else to define the main.py dir?
@ -119,7 +123,7 @@ class Bdist(Command):
exit(1)
if len(main_py_dirs) > 1:
print('WARNING: Multiple main.py dirs found, using the shortest path')
main_py_dirs = sorted(main_py_dirs, key=lambda j: len(split(j)))
main_py_dirs.sort(key=lambda j: len(split(j)))
if not argv_contains('--launcher'):
sys.argv.append('--private={}'.format(
@ -127,39 +131,18 @@ class Bdist(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()

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

@ -1,20 +1,20 @@
import functools
import glob
import importlib
import os
from os.path import (join, dirname, isdir, normpath, splitext, basename)
from os import listdir, walk, sep
import sh
import shlex
import glob
import importlib
import os
import shutil
from pythonforandroid.logger import (shprint, info, logger, debug)
from pythonforandroid.util import (
current_directory, ensure_dir, temp_directory, BuildInterruptingException)
from pythonforandroid.logger import (warning, shprint, info, logger,
debug)
from pythonforandroid.util import (current_directory, ensure_dir,
temp_directory)
from pythonforandroid.recipe import Recipe
def copy_files(src_root, dest_root, override=True, symlink=False):
def copy_files(src_root, dest_root, override=True):
for root, dirnames, filenames in walk(src_root):
for filename in filenames:
subdir = normpath(root.replace(src_root, ""))
@ -29,44 +29,12 @@ def copy_files(src_root, dest_root, override=True, symlink=False):
if override and os.path.exists(dest_file):
os.unlink(dest_file)
if not os.path.exists(dest_file):
if symlink:
os.symlink(src_file, dest_file)
else:
shutil.copy(src_file, dest_file)
shutil.copy(src_file, dest_file)
else:
os.makedirs(dest_file)
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:
class Bootstrap(object):
'''An Android project template, containing recipe stuff for
compilation and templated fields for APK info.
'''
@ -77,11 +45,15 @@ class Bootstrap:
bootstrap_dir = None
build_dir = None
dist_dir = None
dist_name = None
distribution = None
# All bootstraps should include Python in some way:
recipe_depends = ['python3', 'android']
recipe_depends = [
("python2", "python2legacy", "python3", "python3crystax"),
'android',
]
can_be_chosen_automatically = True
'''Determines whether the bootstrap can be chosen as one that
@ -98,9 +70,9 @@ class Bootstrap:
def dist_dir(self):
'''The dist dir at which to place the finished distribution.'''
if self.distribution is None:
raise BuildInterruptingException(
'Internal error: tried to access {}.dist_dir, but {}.distribution '
'is None'.format(self, self))
warning('Tried to access {}.dist_dir, but {}.distribution '
'is None'.format(self, self))
exit(1)
return self.distribution.dist_dir
@property
@ -112,7 +84,7 @@ class Bootstrap:
and optional dependencies are being used,
and returns a list of these.'''
recipes = []
built_recipes = self.ctx.recipe_build_order or []
built_recipes = self.ctx.recipe_build_order
for recipe in self.recipe_depends:
if isinstance(recipe, (tuple, list)):
for alternative in recipe:
@ -132,102 +104,70 @@ class Bootstrap:
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."""
bootstrap_dirs = self.get_bootstrap_dirs()
# now do a cumulative copy of all bootstrap dirs
'''Ensure that a build dir exists for the recipe. This same single
dir will be used for building all different archs.'''
self.build_dir = self.get_build_dir()
for bootstrap_dir in bootstrap_dirs:
copy_files(join(bootstrap_dir, 'build'), self.build_dir, symlink=self.ctx.symlink_bootstrap_files)
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'))
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):
def prepare_dist_dir(self, name):
ensure_dir(self.dist_dir)
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()
def run_distribute(self):
self.distribution.save_info(self.dist_dir)
@classmethod
def all_bootstraps(cls):
def list_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):
result.add(name)
return result
yield name
@classmethod
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.'''
def get_bootstrap_from_recipes(cls, recipes, ctx):
'''Returns a bootstrap whose recipe requirements do not conflict with
the given recipes.'''
info('Trying to find a bootstrap that matches the given recipes.')
bootstraps = [cls.get_bootstrap(name, ctx)
for name in cls.all_bootstraps()]
acceptable_bootstraps = set()
# Find out which bootstraps are acceptable:
for name in cls.list_bootstraps()]
acceptable_bootstraps = []
for bs in bootstraps:
if not bs.can_be_chosen_automatically:
continue
possible_dependency_lists = expand_dependencies(bs.recipe_depends, ctx)
possible_dependency_lists = expand_dependencies(bs.recipe_depends)
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)
@ -235,63 +175,19 @@ class Bootstrap:
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.add(bs)
acceptable_bootstraps.append(bs)
info('Found {} acceptable bootstraps: {}'.format(
len(acceptable_bootstraps),
[bs.name for bs in acceptable_bootstraps]))
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]
if acceptable_bootstraps:
info('Using the first of these: {}'
.format(acceptable_bootstraps[0].name))
return acceptable_bootstraps[0]
return None
@classmethod
@ -322,16 +218,15 @@ class Bootstrap:
tgt_dir = join(dest_dir, arch.arch)
ensure_dir(tgt_dir)
for src_dir in src_dirs:
libs = glob.glob(join(src_dir, wildcard))
if libs:
shprint(sh.cp, '-a', *libs, tgt_dir)
for lib in glob.glob(join(src_dir, wildcard)):
shprint(sh.cp, '-a', lib, 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)
filenames = glob.glob(javaclass_dir)
shprint(sh.cp, '-a', *filenames, dest_dir)
for filename in glob.glob(javaclass_dir):
shprint(sh.cp, '-a', filename, dest_dir)
def distribute_aars(self, arch):
'''Process existing .aar bundles and copy to current dist dir.'''
@ -364,19 +259,24 @@ class Bootstrap:
debug(" to {}".format(so_tgt_dir))
ensure_dir(so_tgt_dir)
so_files = glob.glob(join(so_src_dir, '*.so'))
shprint(sh.cp, '-a', *so_files, so_tgt_dir)
for f in so_files:
shprint(sh.cp, '-a', f, 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])
logger.info(f'Strip Env {env["STRIP"]} strip {strip} env {env}')
if len(tokens) > 1:
strip = strip.bake(tokens[1:])
libs_dir = join(self.dist_dir, f'_python_bundle__{arch.arch}',
libs_dir = join(self.dist_dir, '_python_bundle',
'_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')
@ -401,31 +301,9 @@ class Bootstrap:
shprint(sh.rm, '-rf', d)
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:
def expand_dependencies(recipes):
recipe_lists = [[]]
for recipe in recipes_with_deps:
for recipe in recipes:
if isinstance(recipe, (tuple, list)):
new_recipe_lists = []
for alternative in recipe:
@ -435,6 +313,6 @@ def expand_dependencies(recipes, ctx):
new_recipe_lists.append(new_list)
recipe_lists = new_recipe_lists
else:
for existing_list in recipe_lists:
existing_list.append(recipe)
for old_list in recipe_lists:
old_list.append(recipe)
return recipe_lists

View file

@ -1,13 +1,13 @@
#!/usr/bin/env python3
#!/usr/bin/env python2.7
from __future__ import print_function
from gzip import GzipFile
import hashlib
import json
from os.path import (
dirname, join, isfile, realpath,
relpath, split, exists, basename
)
from os import environ, listdir, makedirs, remove
from os import listdir, makedirs, remove
import os
import shlex
import shutil
@ -16,20 +16,19 @@ 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, error_if_missing=True):
def get_dist_info_for(key):
try:
with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
info = json.load(fileh)
value = info[key]
value = str(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)
@ -40,6 +39,10 @@ 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')
@ -54,6 +57,7 @@ else:
curdir = dirname(__file__)
PYTHON = get_hostpython()
PYTHON_VERSION = get_python_version()
if PYTHON is not None and not exists(PYTHON):
PYTHON = None
@ -68,23 +72,29 @@ 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')))
DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity'
DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService'
def try_unlink(fn):
if exists(fn):
os.unlink(fn)
def ensure_dir(path):
@ -144,33 +154,75 @@ def listfiles(d):
yield fn
def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True):
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):
'''
Make a zip file `fn` from the contents of source_dis.
'''
def clean(tinfo):
"""cleaning function (for reproducible builds)"""
tinfo.uid = tinfo.gid = 0
tinfo.uname = tinfo.gname = ''
tinfo.mtime = 0
return tinfo
# 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)
# get the files and relpath file of all the directory we asked for
files = []
for sd in source_dirs:
sd = realpath(sd)
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
compile_dir(sd, optimize_python=optimize_python)
files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd)
if select(x)]
# create tar.gz of thoses files
gf = GzipFile(tfn, 'wb', mtime=0) # deterministic
tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT)
tf = tarfile.open(tfn, 'w:gz', format=tarfile.USTAR_FORMAT)
dirs = []
for fn, afn in files:
dn = dirname(afn)
@ -186,24 +238,25 @@ def make_tar(tfn, source_dirs, byte_compile_python=False, 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, filter=clean)
tf.add(fn, afn)
tf.close()
gf.close()
def compile_py_file(python_file, optimize_python=True):
def compile_dir(dfn, optimize_python=True):
'''
Compile python_file to *.pyc and return the filename of the *.pyc file.
Compile *.py in directory `dfn` to *.pyo
'''
if PYTHON is None:
return
args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file]
if int(PYTHON_VERSION[0]) >= 3:
args = [PYTHON, '-m', 'compileall', '-b', '-f', dfn]
else:
args = [PYTHON, '-m', 'compileall', '-f', dfn]
if optimize_python:
# -OO = strip docstrings
args.insert(1, '-OO')
@ -215,18 +268,16 @@ def compile_py_file(python_file, 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.pyc:
# If no launcher is specified, require a main.py/main.pyo:
if (get_bootstrap_name() != "sdl" or args.launcher is None) and \
get_bootstrap_name() not in ["webview", "service_library"]:
get_bootstrap_name() != "webview":
# (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.pyc'))):
print('''BUILD FAILURE: No main.py(c) found in your app directory. This
not exists(join(realpath(args.private), 'main.pyo'))):
print('''BUILD FAILURE: No main.py(o) 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.''')
@ -235,168 +286,53 @@ main.py that loads it.''')
assets_dir = "src/main/assets"
# Delete the old assets.
shutil.rmtree(assets_dir, ignore_errors=True)
try_unlink(join(assets_dir, 'public.mp3'))
try_unlink(join(assets_dir, 'private.mp3'))
ensure_dir(assets_dir)
# remove make_python_zip()
# In order to speedup import and initial depack,
# construct a python27.zip
make_python_zip()
# Add extra environment variable file into tar-able directory:
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
if hasattr(args, "window"):
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
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).
use_setup_py = get_dist_info_for("use_setup_py",
error_if_missing=False) is True
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.')
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)
# skip this:
# if exists(join(args.private, "main.pyo")):
# shutil.copyfile(join(args.private, "main.pyo"),
# join(main_py_only_dir, "main.pyo"))
# elif exists(join(args.private, "main.py")):
# shutil.copyfile(join(args.private, "main.py"),
# join(main_py_only_dir, "main.py"))
# tar_dirs.append(main_py_only_dir)
# Check all main.py files we need to copy:
copy_paths = ["main.py", join("service", "main.py")]
for copy_path in copy_paths:
variants = [
copy_path,
copy_path.partition(".")[0] + ".pyc",
]
# Check in all variants with all possible endings:
for variant in variants:
if exists(join(args.private, variant)):
# Make sure surrounding directly exists:
dir_path = os.path.dirname(variant)
if (len(dir_path) > 0 and
not exists(
join(main_py_only_dir, dir_path)
)):
os.mkdir(join(main_py_only_dir, dir_path))
# Copy actual file:
shutil.copyfile(
join(args.private, variant),
join(main_py_only_dir, variant),
)
# Append directory with all main.py's to result apk paths:
tar_dirs.append(main_py_only_dir)
# if get_bootstrap_name() == "webview":
# for asset in listdir('webview_includes'):
# shutil.copy(join('webview_includes', asset), join(assets_dir, asset))
for asset in args.assets:
asset_src, asset_dest = asset.split(":")
if isfile(realpath(asset_src)):
ensure_dir(dirname(join(assets_dir, asset_dest)))
shutil.copy(realpath(asset_src), join(assets_dir, asset_dest))
else:
shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest))
if args.private or args.launcher:
for arch in get_dist_info_for("archs"):
libs_dir = f"libs/{arch}"
make_tar(
join(libs_dir, "libpybundle.so"),
[f"_python_bundle__{arch}"],
byte_compile_python=args.byte_compile_python,
optimize_python=args.optimize_python,
)
make_tar(
join(assets_dir, "private.tar"),
tar_dirs,
byte_compile_python=args.byte_compile_python,
optimize_python=args.optimize_python,
)
finally:
for directory in _temp_dirs_to_clean:
shutil.rmtree(directory)
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)
# 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, 'mipmap/icon.png')
join(res_dir, 'drawable/icon.png')
)
if args.icon_fg and args.icon_bg:
shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png'))
shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png'))
with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd:
fd.write("""<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/icon_background"/>
<foreground android:drawable="@mipmap/icon_foreground"/>
</adaptive-icon>
""")
elif args.icon_fg or args.icon_bg:
print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. "
"Ignoring.")
if get_bootstrap_name() != "service_only":
lottie_splashscreen = join(res_dir, 'raw/splashscreen.json')
if args.presplash_lottie:
shutil.copy(
'templates/lottie.xml',
join(res_dir, 'layout/lottie.xml')
)
ensure_dir(join(res_dir, 'raw'))
shutil.copy(
args.presplash_lottie,
join(res_dir, 'raw/splashscreen.json')
)
else:
if exists(lottie_splashscreen):
remove(lottie_splashscreen)
remove(join(res_dir, 'layout/lottie.xml'))
shutil.copy(
args.presplash or default_presplash,
join(res_dir, 'drawable/presplash.jpg')
)
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 = []
@ -424,17 +360,17 @@ main.py that loads it.''')
version_code = 0
if not args.numeric_version:
"""
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.
"""
# 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')
min_sdk = args.min_sdk_version
for i in args.version.split('.'):
version_code *= 100
version_code += int(i)
args.numeric_version = "{}{}{}".format("10", min_sdk, version_code)
args.numeric_version = "{}{}{}".format(arch_code, min_sdk, version_code)
if args.intent_filters:
with open(args.intent_filters) as fd:
@ -451,9 +387,6 @@ 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 = '**'
@ -469,7 +402,6 @@ 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]
@ -494,7 +426,6 @@ 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
@ -516,37 +447,19 @@ 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 = sorted(build_tools_versions,
key=LooseVersion)
build_tools_versions.sort(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,
"debug": "debug" in args.build_mode,
"native_services": args.native_services
"android_api": android_api
}
if get_bootstrap_name() == "sdl2":
render_args["url_scheme"] = url_scheme
@ -569,17 +482,9 @@ main.py that loads it.''')
aars=aars,
jars=jars,
android_api=android_api,
build_tools_version=build_tools_version,
debug_build="debug" in args.build_mode,
is_library=(get_bootstrap_name() == 'service_library'),
build_tools_version=build_tools_version
)
# gradle properties
render(
'gradle.tmpl.properties',
'gradle.properties',
args=args)
# ant build templates
render(
'build.tmpl.xml',
@ -588,18 +493,9 @@ 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": hashlib.sha1(private_version.encode()).hexdigest()
"private_version": str(time.time())
}
if get_bootstrap_name() == "sdl2":
render_args["url_scheme"] = url_scheme
@ -631,31 +527,27 @@ 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:
# 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"])
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
])
except subprocess.CalledProcessError as e:
if e.returncode == 1:
# 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))
# 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)
)
else:
raise e
else:
# The dry run worked, so do the real thing
subprocess.check_output(patch_command)
def parse_args_and_make_package(args=None):
def parse_args(args=None):
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
# Get the default minsdk, equal to the NDK API that this dist is built against
@ -710,36 +602,16 @@ 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',
@ -764,28 +636,6 @@ 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'))
@ -797,13 +647,6 @@ 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 '
@ -831,8 +674,6 @@ 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',
@ -844,6 +685,8 @@ 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.'))
@ -855,33 +698,10 @@ 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 .pyc files, using -OO '
help=('Whether to compile to optimised .pyo 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:
@ -901,6 +721,7 @@ 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]
@ -930,19 +751,21 @@ 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
python_executable = 'python2.7'
try:
subprocess.call([python_executable, '--version'])
except (OSError, subprocess.CalledProcessError):
pass
else:
PYTHON = python_executable
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')
if args.blacklist:
with open(args.blacklist) as fd:
@ -962,11 +785,10 @@ tools directory of the Android SDK.
'--launcher (SDL2 bootstrap only)' +
'to have something to launch inside the .apk!')
sys.exit(1)
print('ARGS ARGS ARGS', args)
make_package(args)
return args
if __name__ == "__main__":
parse_args_and_make_package()
parse_args()

View file

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

View file

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

View file

@ -15,11 +15,15 @@
#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
@ -165,14 +169,26 @@ 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(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(crystax_python_dir) || dir_exists(python_bundle_dir)) {
if (dir_exists(crystax_python_dir)) {
LOGP("crystax_python exists");
snprintf(paths, 256,
"%s/stdlib.zip:%s/modules",
crystax_python_dir, crystax_python_dir);
}
if (dir_exists(python_bundle_dir)) {
LOGP("_python_bundle dir exists");
snprintf(paths, 256,
"%s/stdlib.zip:%s/modules",
python_bundle_dir, python_bundle_dir);
}
LOGP("calculated paths to be...");
LOGP(paths);
@ -184,11 +200,24 @@ int main(int argc, char *argv[]) {
LOGP("set wchar paths...");
} else {
LOGP("_python_bundle does not exist...this not looks good, all python"
" recipes should have this folder, should we expect a crash soon?");
// 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");
}
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.
@ -207,8 +236,34 @@ 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,
@ -226,13 +281,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.replace('\\x00', ''))\n"
" self.__buffer = lines[-1]\n"
" androidembed.log(l)\n"
" self.buffer = lines[-1]\n"
" def flush(self):\n"
" return\n"
"sys.stdout = sys.stderr = LogFile()\n"
@ -251,10 +306,14 @@ int main(int argc, char *argv[]) {
*/
LOGP("Run user program, change dir and execute entrypoint");
/* Get the entrypoint, search the .pyc then .py
/* Get the entrypoint, search the .pyo 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;
@ -270,17 +329,21 @@ int main(int argc, char *argv[]) {
entrypoint[strlen(env_entrypoint) - 1] = '\0';
LOGP(entrypoint);
if (!file_exists(entrypoint)) {
LOGP("Entrypoint not found (.pyc, fallback on .py), abort");
LOGP("Entrypoint not found (.pyc/.pyo, fallback on .py), abort");
return -1;
}
} else {
strcpy(entrypoint, env_entrypoint);
}
} else if (!strcmp(dot, ".py")) {
/* if .py is passed, check the pyc version first */
/* if .py is passed, check the pyo 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)) {
@ -290,7 +353,7 @@ int main(int argc, char *argv[]) {
strcpy(entrypoint, env_entrypoint);
}
} else {
LOGP("Entrypoint have an invalid extension (must be .py or .pyc), abort.");
LOGP("Entrypoint have an invalid extension (must be .py or .pyc/.pyo), abort.");
return -1;
}
// LOGP("Entrypoint is:");
@ -311,7 +374,8 @@ 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))
if (PyFile_WriteString(
"\n", f)) /* python2 used Py_FlushLine, but this no longer exists */
PyErr_Clear();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,10 +14,10 @@ import android.app.PendingIntent;
import android.os.Process;
import java.io.File;
//imports for channel definition
import android.app.NotificationManager;
import android.app.NotificationChannel;
import android.graphics.Color;
import org.kivy.android.PythonUtil;
import org.renpy.android.Hardware;
public class PythonService extends Service implements Runnable {
@ -33,8 +33,6 @@ 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;
@ -44,6 +42,10 @@ public class PythonService extends Service implements Runnable {
autoRestartService = restart;
}
public boolean canDisplayNotification() {
return true;
}
public int startType() {
return START_NOT_STICKY;
}
@ -62,15 +64,10 @@ 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 startType();
}
//intent is null if OS restarts a STICKY service
if (intent == null) {
Context context = getApplicationContext();
intent = getThisDefaultIntent(context, "");
return START_NOT_STICKY;
}
startIntent = intent;
startIntent = intent;
Bundle extras = intent.getExtras();
androidPrivate = extras.getString("androidPrivate");
androidArgument = extras.getString("androidArgument");
@ -78,38 +75,28 @@ 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 (serviceStartAsForeground) {
if (canDisplayNotification()) {
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_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
notification = new Notification(
context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis());
try {
@ -122,26 +109,14 @@ public class PythonService extends Service implements Runnable {
IllegalArgumentException | InvocationTargetException e) {
}
} else {
// 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);
Notification.Builder builder = new Notification.Builder(context);
builder.setContentTitle(serviceTitle);
builder.setContentText(serviceDescription);
builder.setContentIntent(pIntent);
builder.setSmallIcon(context.getApplicationInfo().icon);
notification = builder.build();
}
startForeground(getServiceId(), notification);
startForeground(1, notification);
}
@Override
@ -162,10 +137,7 @@ public class PythonService extends Service implements Runnable {
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
//sticky servcie runtime/restart is managed by the OS. leave it running when app is closed
if (startType() != START_STICKY) {
stopSelf();
}
stopSelf();
}
@Override

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
/**
* 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. So this is the next best thing.
* change. (This same code is used in both org.renpy.android and
* org.renpy.pygame.) So this is the next best thing.
*/
package org.renpy.android;

View file

@ -1,11 +1,18 @@
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 {{ args.service_class_name }};
import android.app.Notification;
import android.app.PendingIntent;
import android.os.Bundle;
import org.kivy.android.PythonService;
import org.kivy.android.PythonActivity;
public class Service{{ name|capitalize }} extends {{ base_service_class }} {
public class Service{{ name|capitalize }} extends PythonService {
{% if sticky %}
@Override
public int startType() {
@ -13,35 +20,54 @@ public class Service{{ name|capitalize }} extends {{ base_service_class }} {
}
{% endif %}
{% if not foreground %}
@Override
protected int getServiceId() {
return {{ service_id }};
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);
}
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);
return intent;
}
@Override
protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) {
return Service{{ name|capitalize }}.getDefaultIntent(ctx, pythonServiceArgument);
ctx.startService(intent);
}
static public void stop(Context ctx) {

View file

@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath 'com.android.tools.build:gradle:3.1.4'
}
}
@ -13,45 +13,23 @@ allprojects {
repositories {
google()
jcenter()
{%- for repo in args.gradle_repositories %}
{{repo}}
{%- endfor %}
flatDir {
dirs 'libs'
}
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 }}'
manifestPlaceholders = {{ args.manifest_placeholders}}
}
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
{% if debug_build -%}
doNotStrip '**/*.so'
{% else %}
exclude 'lib/**/gdbserver'
exclude 'lib/**/gdb.setup'
{%- endif %}
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
defaultConfig {
minSdkVersion {{ args.min_sdk_version }}
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }}
versionName '{{ args.version }}'
}
{% if args.sign -%}
signingConfigs {
@ -62,73 +40,41 @@ android {
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
}
}
{%- 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 %}
}
}
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 %}
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 %}
{%- 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 %}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -435,7 +435,7 @@ main.py that loads it.''')
if exists('build.properties'):
os.remove('build.properties')
def parse_args_and_make_package(args=None):
def parse_args(args=None):
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
default_android_api = 12
import argparse
@ -604,4 +604,4 @@ tools directory of the Android SDK.
if __name__ == "__main__":
parse_args_and_make_package()
parse_args()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,82 +0,0 @@
package {{ args.package }};
import java.io.File;
import android.os.Build;
import android.content.Intent;
import android.content.Context;
import android.content.res.Resources;
import android.util.Log;
import org.kivy.android.PythonService;
import org.kivy.android.PythonUtil;
public class Service{{ name|capitalize }} extends PythonService {
private static final String TAG = "PythonService";
{% if sticky %}
@Override
public int startType() {
return START_STICKY;
}
{% endif %}
@Override
protected int getServiceId() {
return {{ service_id }};
}
public static void prepare(Context ctx) {
String appRoot = PythonUtil.getAppRoot(ctx);
Log.v(TAG, "Ready to unpack");
File app_root_file = new File(appRoot);
PythonUtil.unpackAsset(ctx, "private", app_root_file, true);
PythonUtil.unpackPyBundle(ctx, ctx.getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false);
}
public static void start(Context ctx, String pythonServiceArgument) {
Intent intent = getDefaultIntent(ctx, pythonServiceArgument);
//foreground: {{foreground}}
{% if foreground %}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ctx.startForegroundService(intent);
} else {
ctx.startService(intent);
}
{% else %}
ctx.startService(intent);
{% endif %}
}
static public Intent getDefaultIntent(Context ctx, String pythonServiceArgument) {
String appRoot = PythonUtil.getAppRoot(ctx);
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
intent.putExtra("androidPrivate", appRoot);
intent.putExtra("androidArgument", appRoot);
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
intent.putExtra("serviceTitle", "{{ name|capitalize }}");
intent.putExtra("serviceDescription", "");
intent.putExtra("pythonName", "{{ name }}");
intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}");
intent.putExtra("pythonHome", appRoot);
intent.putExtra("androidUnpack", appRoot);
intent.putExtra("pythonPath", appRoot + ":" + appRoot + "/lib");
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
return intent;
}
@Override
protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) {
return Service{{ name|capitalize }}.getDefaultIntent(ctx, pythonServiceArgument);
}
static public void stop(Context ctx) {
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
ctx.stopService(intent);
}
}

View file

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

View file

@ -1,172 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
}
}
plugins {
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
}
allprojects {
repositories {
jcenter()
maven {
url 'https://maven.google.com'
}
flatDir {
dirs 'libs'
}
}
}
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
group = "io.lbry"
version = "{{ args.version }}"
android {
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
defaultConfig {
minSdkVersion {{ args.min_sdk_version }}
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }} * 10 + 2
versionName '{{ args.version }}'
multiDexEnabled true
ndk {
abiFilters "arm64-v8a"
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
dexOptions {
jumboMode true
}
{% if args.sign -%}
signingConfigs {
release {
storeFile file(System.getenv("P4A_RELEASE_KEYSTORE"))
keyAlias System.getenv("P4A_RELEASE_KEYALIAS")
storePassword System.getenv("P4A_RELEASE_KEYSTORE_PASSWD")
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
}
}
{%- endif %}
buildTypes {
debug {
}
release {
{% if args.sign -%}
signingConfig signingConfigs.release
{%- endif %}
}
}
sourceSets {
main {
jniLibs.srcDir 'libs'
}
}
}
ext {
compileSdkVersion = {{ android_api }}
buildToolsVersion = '{{ build_tools_version }}'
minSdkVersion = {{ args.min_sdk_version }}
targetSdkVersion = {{ android_api }}
}
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
}
}
}
}
//nexusPublishing {
// repositories {
// sonatype {
// stagingProfileId = sonatypeStagingProfileId
// username = ossrhUsername
// password = ossrhPassword
// nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
// snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
// }
// }
//}
afterEvaluate {
println("Components: " + components*.name)
publishing {
publications {
release(MavenPublication) {
groupId 'io.lbry'
artifactId 'lbrysdk64'
version '{{ args.version }}'
from components.release
pom {
name = 'LBRY SDK for Android'
description = 'The LBRY SDK packaged as an Android AAR'
url = 'https://github.com/lbryio/lbry-android-sdk'
licenses {
license {
name = 'MIT License'
url = 'https://raw.githubusercontent.com/lbryio/lbry-android-sdk/master/LICENSE'
}
}
developers {
developer {
id = 'akinwale'
name = 'Akinwale Ariwodola'
email = 'akinwale@lbry.com'
}
}
scm {
url = 'https://github.com/lbryio/lbry-android-sdk'
connection = 'scm:git:github.com/lbryio/lbry-android-sdk.git'
developerConnection = 'scm:git:ssh://github.com/lbryio/lbry-android-sdk.git'
}
}
}
}
}
}
signing {
sign publishing.publications
}
dependencies {
{%- for aar in aars %}
compile(name: '{{ aar }}', ext: 'aar')
{%- endfor -%}
{%- if args.depends -%}
{%- for depend in args.depends %}
implementation '{{ depend }}'
{%- endfor %}
{%- endif %}
}

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View file

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

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