Продолжение статьи C/C++ из Python (ctypes) на Android. Теперь поговорим о том как использовать Java из Python, ведь для android это родной язык и почти все работающие библиотеки и нужные вещи написаны на ней. Научимся создавать классы, вызывать методы, возвращать результат и дергать callback-и. На Linux аналогично работает.
Java
Создаем два файла Test.java и CallbackWrapper.java.
Test.java:
package some.kivy_test;
// Подключение интерфейса CallbackWrapper из CallbackWrapper.java
import some.kivy_test.CallbackWrapper;
/**
\brief Тестовый класс.
Содержит набор методов, вызываемые из python и в python (main.py).
*/
public class Test {
CallbackWrapper wrapper = null;
/**
Конструктор класса
\param callback_wrapper указатель на callback_wrapper из main.py
*/
public Test(CallbackWrapper callback_wrapper) {
this.wrapper = callback_wrapper;
}
/**
Функция возврата строки
\return Возвращает строку
*/
public String hello() {
return "Hello from Test.java";
}
/**
Функция возврата строки
\param arg1 строка
\return Возвращает строку
*/
public String hello2(String arg1) {
return arg1;
}
/**
Функция вызова callback1 из callback_wrapper (main.py)
*/
public void callback1() {
this.wrapper.callback1();
}
/**
Функция вызова callback2 из callback_wrapper (main.py)
*/
public void callback2() {
this.wrapper.callback2("callback");
}
/**
Функция вызова callback3 из callback_wrapper (main.py)
*/
public void callback3() {
this.wrapper.callback3(11, (byte) 22);
}
}
Класс Test продемонстрирует рабочий функционал:
- создание класса;
- вызов методов класса;
- вызов callback-ов.
CallbackWrapper.java:
package some.kivy_test;
// Обертка для python callback-ов
public interface CallbackWrapper {
public void callback1();
public boolean callback2(String arg1);
public int callback3(int arg1, byte arg2);
}
CallbackWrapper — интерфейс методов Java реализованных в python.
Python
Здесь нам понадобится фреймворк Kivy, buildozer — утилита для создания apk пакетов и jnius — библиотека доступа к классам Java. Установку и настройку проводил по статье: Kivy. Сборка пакетов под Android и никакой магии
Установка kivy, buildozer & jnius
sudo pip3 install kivy
sudo pip3 install buildozer
sudo pip3 install pyjnius
Установив kivy, buildozer и jnius приступим к созданию тестовой программы. Создадим папку под нее:
mkdir android_python
cd android_python
Теперь создадим main.py, это точка запуска нашей программы.
touch main.py
И заполним его:
#!/usr/bin/python3
#-*- coding: utf-8 -*-
import kivy
kivy.require("2.1.0")
from kivy.app import App
from kivy.uix.button import Button
from kivy.utils import platform
import jnius
from jnius import cast
from jnius import autoclass, PythonJavaClass, java_method
# Подключение класса System
System = autoclass('java.lang.System')
# Подключение классов Android
if platform == 'android':
PythonActivity = autoclass('org.kivy.android.PythonActivity')
currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
# Класс графики, который создает кнопку для выхода из приложения.
class ButtonApp(App):
def build(self):
# use a (r, g, b, a) tuple
btn = Button(text ="Push Me !",
font_size ="20sp",
background_color = (1, 1, 1, 1),
color = (1, 1, 1, 1),
size_hint = (.2, .1),
pos_hint = {'x':.4, 'y':.45})
# bind() use to bind the button to function callback
btn.bind(on_press = self.callback)
return btn
# callback function tells when button pressed
def callback(self, event):
if platform == 'android':
currentActivity.finishAndRemoveTask()
System.exit(0)
##
# Класс оповещений событий из Java
##
class CallbackWrapper(PythonJavaClass):
__javacontext__ = 'app'
# Указываем что этот класс является реализацией интерфейса
# CallbackWrapper из пакета some.kivy_test
__javainterfaces__ = ['some/kivy_test/CallbackWrapper']
def __init__(self):
super().__init__()
##
# Реализация callback-ов описанных в CallbackWrapper.java.
# Имена методов должны быть разные.
##
@java_method('()V')
def callback1(self):
print("Python: @java_method('()V')")
@java_method('(Ljava/lang/String;)Z')
def callback2(self, arg1):
print("Python: @java_method('(Ljava/lang/String;)Z'), ", arg1)
return True
@java_method('(IB)I')
def callback3(self, arg1, arg2):
print("Python: @java_method('(IB)I'), ", arg1, arg2)
return 555
##
# Старт.
##
if __name__ == "__main__":
# Печать функций python
print("Python: Hello world!")
# Вызов метода печати из класса System.
System.out.println('I/python: Java: Hello world!')
if platform == 'android':
# Получение указателя на класс Test из Test.java
Test = autoclass('some.kivy_test.Test')
# Создание класса с callback-ми, переменная содержащая объект класса должна быть
# глобальной или быть переменной внутри класса. Иначе объект Python выходит
# за рамки интерпретатора Python, даже если ссылка на него все еще существует в Java.
global callback_wrapper
callback_wrapper = CallbackWrapper()
# Создание класса Test из Test.java, передаем в него указатель на класс callback-ов
test = Test(callback_wrapper)
# Демонстрация вызовов методов из класса Test
print("Python: ", test.hello())
print("Python: ", test.hello2("test.hello2()"))
# Демонстрация вызовов методов CallbackWrapper определенных здесь из класса Test
test.callback1()
test.callback2()
test.callback3()
# Отрисовка графики приложения
ButtonApp().run()
Callback-и из CallbackWrapper создаются через декоратор java_method. Подробное описание сигнатурного формата java_method.
main.py cоздает простую графическую программу с одной кнопкой при нажатии которой произойдет закрытие приложения. Основная задача статьи показать как работать с Java, результат работы увидим в консоли.
Buildozer
Так же нам понадобится файл спецификации buildozer, описывающий правила сборки пакета apk. Создаем его:
touch buildozer.spec
Заполняем:
[app]
# (str) Title of your application
title = kivy_test
# (str) Package name
package.name = kivy_test
# (str) Package domain (needed for android/ios packaging)
package.domain = com.heattheatr
# (str) Source code where the main.py live
source.dir = .
# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,jpeg,ttf,kv,mp3, #,so,2,6,2,1,a
source.include_patterns = img/*, font/*, ui/*, music/*
# (list) Application version
version = 0.0.28
# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3, \
kivy, \
jnius
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy
#requirements.source.libtest = lib/libtest
# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait
# (bool) Indicate if the application should be fullscreen or not
fullscreen = 1
# (list) Permissions
android.permissions = WRITE_EXTERNAL_STORAGE
# (int) Target Android API, should be as high as possible.
android.api = 31
# (int) Minimum API your APK will support.
android.minapi = 21
# (str) Android NDK version to use
android.ndk = 23b
# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
android.skip_update = False
# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
android.accept_sdk_license = True
# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.archs = arm64-v8a
android.gradle_dependencies = 'de.d0pam1n:pjsip-for-android:2.6'
#android.add_jars = %(source.dir)s/java_lib/AJVoIP.jar
#android.add_aars = %(source.dir)s/libs_lib/*.aar
# (list) Android additionnal libraries to copy into libs/armeabi
android.add_src = %(source.dir)s/java_src/*
android.add_libs_arm64_v8a = %(source.dir)s/libs/libs_arm64_v8a/*.*
android.add_libs_armeabi_v7a = %(source.dir)s/libs/libs_armeabi-v7a/*.*
#android.library_references = /home/djvu/workspace/intercom/phone/libs/libs_arm64_v8a/*.*
#android.copy_libs = 1
[buildozer]
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2
# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 0
# (str) Path to build artifact storage, absolute or relative to spec file
build_dir = ./.buildozer
# (str) Path to build output (i.e. .apk, .ipa) storage
bin_dir = ./bin
Данный buildozer.spec собирает приложения под архитектуру arm64-v8a.
В поле android.add_src указывается папка в которой находятся Java исходники.
Теперь соберем apk пакет:
buildozer android debug
Операция очень долгая и растянется на несколько десятков минут. Так же потребуется порядка 1.5 GB свободного места, т.к. buildozer подтянет все необходимые библиотеки для сборки.
После удачного завершения в папке bin соберется пакет kivy_test-0.0.1-arm64-v8a-debug.apk.
Android
На телефоне нужно включить режим отладки по USB и разрешить установку через USB.
Устанавливаем на телефон:
adb install -r ./bin/kivy_test-*.apk
Находим приложение на телефоне:
Подключаемся к консоли телефона и мониторим работу приложения:
adb logcat | grep python
Запускаем и получаем следующее — Java отработала без проблем:
I/python (21423): [INFO ] [Text ] Provider: sdl2
I/python (21423): Python: Hello world!
I/System.out(21423): I/python: Java: Hello world!
I/python (21423): Python: Hello from Test.java
I/python (21423): Python: test.hello2()
I/python (21423): Python: @java_method('()V')
I/python (21423): Python: @java_method('(Ljava/lang/String;)Z'), callback
I/python (21423): Python: @java_method('(IB)I'), 11 22
I/python (21423): [INFO ] [Window ] Provider: sdl2
I/python (21423): [INFO ] [GL ] Using the "OpenGL ES 2" graphics system
На экране телефона видим следующую картинку:
Нажатие на кнопку закрывает приложение.
Linux
Так же main.py можно запустить из под linux, будет подключен класс java.lang.System и вызван метод:
System.out.println('I/python: Java: Hello world!')
Частично материал дублирует прошлую статью, сделано с целью просмотра всего в одном месте.
Спасибо за внимание.