python

Пишем список дел на Python 3 для Android через QPython3 и SL4A

  • пятница, 3 ноября 2017 г. в 03:12:43
https://habrahabr.ru/post/341580/
  • Разработка под Android
  • Python



Движок 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):
    """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]

Определяем функцию, отвечающую за создание новой задачи. Принцип аналогичен. В аргументе default мы передаем ей текст, который по умолчанию появляется в строке ввода (пустой при ""). В более сложных программах можно передавать различные подписи и кнопки.


def dialog_confirm(message):
    """Confirm yes or no"""
    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.toast("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.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 local 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.toast("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.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. Вопросы и замечания лучше писать мне в личку.