Пишем список дел на Python 3 для Android через QPython3 и SL4A
Движок QPython (и QPython 3) для Android — вещь по-прежнему плохо изученная, и особенно что касается его встроенной библиотеки Scripting Layer For Android (SL4A), она же androidhelper. Эту библиотеку написали несколько сотрудников Google по принципу 20% свободного времени, снабдили ее спартанской документацией, которую почти невозможно найти, и отправили в свободное плавание. Я искал информацию об SL4A по крупицам, но со временем нашел практически все, что мне нужно.
SL4A позволяет задействовать практически все возможности консольного Python 3 вплоть до библиотек типа matplotlib, при этом используются стандартные диалоги Android: ввод текста, списки, вопросы, радиокнопки, выбор даты и т.д. Программа не будет поражать красотой, но многие задачи решать сможет. Самое главное, что мы получим доступ к различным функциям устройства. Например, можно:
- делать телефонные звонки
- посылать SMS
- менять громкость
- включать Wi-Fi и Bluetooth
- открывать веб-страницы
- открывать сторонние приложения
- делать фото- и видеосъемку камерой
- извлекать контакты из контактной книги
- посылать системные оповещения
- определять GPS-координаты устройства
- определять заряд батареи
- считывать данные SIM-карты
- воспроизводить медиафайлы
- работать с буфером обмена
- генерировать голосовые сообщения
- экспортировать данные на внешние активности (share)
- открывать локальные html-страницы
- и др.
В нашем примере мы напишем простейший список задач. Мы сможем создавать и удалять задачи, а также экспортировать их. Программа будет вибрировать и разговаривать. Мы будем пользоваться тремя видами диалогов: список, текстовый ввод и вопрос «да/нет». На все про все нам хватит менее 100 строк кода. Интерфейс сделаем английским ради универсальности (и GitHub).
Вот весь код и комментарии к наиболее существенным моментам.
from androidhelper import Android
droid = Android()
Создаем объект droid класса Android (), который будет отвечать за взаимодействие с SL4A.
path=droid.environment()[1]["download"][:droid.environment()[1]["download"].index("/Download")] + "/qpython/scripts3/tasks.txt"
Переменная path будет содержать абсолютное имя файла, в котором хранятся задачи. Почему так длинно? Дело в том, что SL4A не может работать с локальным путем, поэтому приходится определять абсолютный, а абсолютный может отличаться на разных Android-устройствах. Мы обойдем эту проблему путем определения местоположения папки Download
с помощью метода droid.environment()
. Затем мы отсекаем Download
и добавляем путь Qpython/Scripts3
(он всегда одинаков) плюс имя файла.
def dialog_list(options):
droid.dialogCreateAlert("\ud83d\udcc3 My Tasks (%d)" % len(options))
droid.dialogSetItems(options)
droid.dialogSetPositiveButtonText("\u2795")
droid.dialogSetNegativeButtonText("Exit")
droid.dialogSetNeutralButtonText("\u2702")
droid.dialogShow()
return droid.dialogGetResponse()[1]
Определяем функцию, отвечающую за вывод списка задач. Это делается с помощью метода droid.dialogCreateAlert()
. Затем ряд вспомогательных методов выводят собственно пункты, создают кнопки и получают результат от пользователя. Для упрощения мы упакуем все эти методы в одну простую функцию, которой будем передавать список задач. В более сложных скриптах можно передавать больше аргументов: заголовок, названия кнопок и т.д.
def dialog_text(default):
droid.dialogCreateInput("\u2795 New Task", "Enter a new task:", default)
droid.dialogSetPositiveButtonText("Submit")
droid.dialogSetNeutralButtonText("Clear")
droid.dialogSetNegativeButtonText("Cancel")
droid.dialogShow()
return droid.dialogGetResponse()[1]
Определяем функцию, отвечающую за создание новой задачи. Принцип аналогичен. В аргументе default
мы передаем ей текст, который по умолчанию появляется в строке ввода (пустой при »). В более сложных программах можно передавать различные подписи и кнопки.
def dialog_confirm(message):
droid.dialogCreateAlert("Confirmation", message)
droid.dialogSetPositiveButtonText("Yes")
droid.dialogSetNegativeButtonText("No")
droid.dialogShow()
return droid.dialogGetResponse().result
Эта функция будет задавать вопрос пользователю, чтобы получить ответ да или нет. Мы передаем ей текст вопроса.
while 1:
try:
with open(path) as file:
tasks=file.readlines()
except:
droid.makeToast("File %s not found or opening error" % path)
tasks=[]
Создаем цикл (чтобы скрипт не вышел после первого же действия) и первым делом читаем файл задач и загружаем его в список tasks
. Если файла нет, создаем пустой список.
response=dialog_list(tasks)
Выводим список задач. Когда пользователь делает какой-то выбор, метод dialog_list()
возвращает это действие в виде значения, которое мы присваиваем переменной response
.
if "item" in response:
del tasks[response["item"]]
droid.vibrate(200)
droid.makeToast("Дело сделано!")
droid.ttsSpeak("Дело сделано!")
Начинаем обрабатывать действие пользователя. Поскольку метод droid.dialogGetResponse()
, который мы используем в функции списка, выдает довольно сложную структуру в виде словаря, его придется препарировать не самым очевидным способом. В данном случае по простому клику на пункт списка он удаляется — мы выполнили дело. Сообщим об этом во всплывающем сообщении и одновременно сделаем (чисто забавы ради) виброзвонок на 200 миллисекунд и сгенерируем голосовую фразу Дело сделано!
.
elif "which" in response:
if "neutral" in response["which"]:
choice=dialog_confirm("Are you sure you want to wipe all tasks?")
if choice!=None and "which" in choice and choice["which"]=="positive":
tasks=[]
По нажатию на среднюю (нейтральную) кнопку с ножницами можно разом удалить все дела. При этом будет выведен подтверждающий вопрос.
elif "positive" in response["which"]:
default=""
while 1:
input=dialog_text(default)
if "canceled" in input:
default=input["value"]
elif "neutral" in input["which"]:
default=""
elif "positive" in input["which"]:
tasks.append(input["value"]+"\n")
droid.ttsSpeak("Новое дело!")
break
else:
break
else:
exit=True
Здесь мы создаем новую задачу. Обратим внимание на переменную cancel
— ее выдает droid.dialogGetResponse()
в случае клика вне диалога (на пустую область экрана). Чтобы корректно обработать такую ситуацию, мы ввели дополнительное условие. По средней кнопке (neutral
) поле ввода будет очищаться. При positive
мы создаем новый пункт списка и выходим из цикла. Если нажать на самую правую кнопку, сработает else
и мы просто выйдем из цикла, ничего не сохранив (хотя формально это будет значение negative
в input["which"]
). Последняя строка означает, что пользователь нажал на Exit
. Тогда мы устанавливаем флаг exit
в True
.
with open(path, "w") as file:
for i in range(len(tasks)): file.write(tasks[i])
После каждой обработки списка сохраняем список задач в файл.
if exit==True:
break
Если пользователь решил выйти, мы выходим из главного цикла while
.
choice=dialog_confirm("Do you want to export tasks?")
if choice!=None and "which" in choice and choice["which"]=="positive":
droid.sendEmail("Email", "My Tasks", ''.join(tasks), attachmentUri=None)
В самом конце мы спрашиваем у пользователя, надо ли экспортировать все задачи куда-нибудь — на почту, в облако, в мессенджер и т.д. При положительном ответе список задач преобразуется в строку и экспортируется.
На этом всё. Программа будет выглядеть, как на скриншоте выше.
Полный листинг
Окончательный полный листинг (с комментариями на английском):
#!/usr/bin/python
# -*- coding: utf-8 -*-
# This is a very simple to-do list for Android. Requires QPython3 (download it from Google Play Market).
from androidhelper import Android
droid = Android()
# Find absolute path on Android
path=droid.environment()[1]["download"][:droid.environment()[1]["download"].index("/Download")] + "/qpython/scripts3/tasks.txt"
def dialog_list(options):
"""Show tasks"""
droid.dialogCreateAlert("\ud83d\udcc3 My Tasks (%d)" % len(options))
droid.dialogSetItems(options)
droid.dialogSetPositiveButtonText("\u2795")
droid.dialogSetNegativeButtonText("Exit")
droid.dialogSetNeutralButtonText("\u2702")
droid.dialogShow()
return droid.dialogGetResponse()[1]
def dialog_text(default):
"""Show text input"""
droid.dialogCreateInput("\u2795 New Task", "Enter a new task:", default)
droid.dialogSetPositiveButtonText("Submit")
droid.dialogSetNeutralButtonText("Clear")
droid.dialogSetNegativeButtonText("Cancel")
droid.dialogShow()
return droid.dialogGetResponse()[1]
def dialog_confirm(message):
"""Confirm yes or no"""
droid.dialogCreateAlert("Confirmation", message)
droid.dialogSetPositiveButtonText("Yes")
droid.dialogSetNegativeButtonText("No")
droid.dialogShow()
return droid.dialogGetResponse().result
# Run main cycle
while 1:
# Open file
try:
with open(path) as file:
tasks=file.readlines()
except:
droid.makeToast("File %s not found or opening error" % path)
tasks=[]
# Show tasks and wait for user response
response=dialog_list(tasks)
# Process response
if "item" in response: # delete individual task
del tasks[response["item"]]
droid.vibrate(200)
droid.makeToast("Дело сделано!")
droid.ttsSpeak("Дело сделано!")
elif "which" in response:
if "neutral" in response["which"]: # delete all tasks
choice=dialog_confirm("Are you sure you want to wipe all tasks?")
if choice!=None and "which" in choice and choice["which"]=="positive":
tasks=[]
elif "positive" in response["which"]: # create new task
default=""
while 1:
input=dialog_text(default)
if "canceled" in input:
default=input["value"]
elif "neutral" in input["which"]: # clear input
default=""
elif "positive" in input["which"]: # create new task
tasks.append(input["value"]+"\n")
droid.ttsSpeak("Новое дело!")
break
else:
break
else:
exit=True
# Save tasks to file
with open(path, "w") as file:
for i in range(len(tasks)): file.write(tasks[i])
# If user chose to exit, break cycle and quit
if exit==True:
break
# Export tasks
choice=dialog_confirm("Do you want to export tasks?")
if choice!=None and "which" in choice and choice["which"]=="positive":
droid.sendEmail("Email", "My Tasks", ''.join(tasks), attachmentUri=None)
Также вы можете найти его на GitHub.
Пара замечаний. SL4A не позволяет использовать никакую графику, однако можно использовать довольно большое количество всевозможных смайлов и эмодзи как Unicode-символы. Это могут быть хоть домики, хоть собачки, хоть кошечки. В нашем примере мы использовали знак плюс (\u2795
), ножницы (\u2702
) и листок бумаги (\ud83d\udcc3
). C каждой новой версией Unicode их становится все больше, но этим не стоит злоупотреблять — новые смайлы не будут отображаться на более старых версиях Android.
Для запуска скриптов QPython нужно заходить в собственно QPython, но существует интересный плагин для приложения Tasker, позволяющий проделывать довольно мощные вещи с QPython-скриптами, например выводя их на рабочий стол в виде иконок или запуская по различным условиям.
Полезные ресурсы по теме
- QPython 3 в Google Play Market
- Сайт QPython
- Документация по androidhelper
- SL4A на GitHub
- Небольшое описание SL4A на Python Central
- Блог человека, иногда пишущего про SL4A с примерами
P.S. Вопросы и замечания лучше писать мне в личку.