Android SL4A 脚本编程高级教程(二)

原文:Pro Android Scripting with SL4A

协议:CC BY-NC-SA 4.0

六、使用 Python 编写后台脚本

本章将介绍如何创建使用 Android 脚本层(SL4A)的脚本,这些脚本没有用户界面,并在后台运行。

本章的主要主题如下:

  • 编写在后台执行特定任务的脚本
  • 展示 SL4A 的不同功能方面

Python 作为一种开发脚本来快速有效地完成基本功能任务的语言而闻名。这一章将向你展示如何构建脚本来执行特定的操作,而不需要任何干预。所以本章中的脚本将没有用户界面可言。虽然在终端窗口中启动脚本时可能会有一些状态信息,但是用户除了启动脚本之外没有其他事情可做。

后台任务

使用 SL4A 的最新版本(撰写本文时为 r4),您可以在终端或后台启动任何脚本。要在后台启动它,选择看起来像一个小齿轮的图标,如图图 6-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-1。 SL4A 脚本启动选项

当脚本运行时,它会在通知页面上放置一个条目来标识应用,并在必要时为您提供关闭应用的方法。如果您希望在设备启动时启动一个脚本,也有一个专门针对 SL4A 编写的应用。该应用被称为启动时启动,并做了很多事情。图 6-2 显示了主屏幕的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-2。*在启动首选项屏幕上启动

每当您的设备启动时,该工具将启动一个 SL4A 脚本。如果您想要启动多个脚本,您将需要创建一个主脚本,该主脚本将依次启动其他脚本。这就带来了一个明显的问题:如何从 Python 启动另一个 SL4A 脚本?要回答这个问题,我们需要看一下makeIntent函数。以下是文档中关于makeIntent的内容:

makeIntent( String action, String uri[optional], String type[optional]: MIME type/subtype of the URI, JSONObject extras[optional]: a Map of extras to add to the Intent, JSONArray categories[optional]: a List of categories to add to the Intent, String packagename[optional]: name of package. If used, requires classname to be useful, String classname[optional]: name of class. If used, requires packagename to be useful, Integer flags[optional]: Intent flags)

关键是这是一个明确的意图,意味着你不需要一个 URI。为了启动另一个 SL4A 脚本,您必须完全限定packagenamecomponentname。由此产生的调用将如下所示:

intent=droid.makeIntent("com.googlecode.android_scripting.action.LAUNCH_BACKGROUND_SCRIPT",\ None, \ None, \ {"com.googlecode.android_scripting.extra.SCRIPT_PATH" : "/sdcard/sl4a/scripts/hello_world.py"}, \ None, \ "com.googlecode.android_scripting", \ "com.googlecode.android_scripting.activity.ScriptingLayerServiceLauncher").result

我们可以通过如下几行额外的代码使其更容易阅读:

import android droid = android.Android() action = "com.googlecode.android_scripting.action.LAUNCH_BACKGROUND_SCRIPT" clsname = "com.googlecode.android_scripting" pkgname = "com.googlecode.android_scripting.activity.ScriptingLayerServiceLauncher" extras = {"com.googlecode.android_scripting.extra.SCRIPT_PATH": "/sdcard/sl4a/scripts/hello_world.py"} myintent = droid.makeIntent(action, None, None, extras, None, clsname, pkgname).result droid.startActivityIntent(myintent)

触发器

SL4A 提供了实现触发器的方法。我想在这里简单地提到它们,但是要知道,在撰写本文时,它们仍然有些缺陷。基本概念是提供一种机制,根据设备上发生的某些条件或事件来触发某些功能。图 6-3 显示了在查看脚本列表时,如果按下菜单按钮,然后选择触发器,您将看到的菜单。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-3。*触发菜单

任何现有的触发器都将显示在此屏幕中。您可以使用 cancel all 按钮取消所有触发器,或者通过长按想要移除的触发器来调出移除按钮来选择单个触发器(参见图 6-4 )。要添加新的触发器,按下图 6-3 中所示的添加按钮。这将显示/sdcard/sl4a/scripts目录的内容,并允许您选择要运行的脚本。一旦你选择了一个脚本,你会看到一个弹出菜单,如图 6-5 中的所示。您可以在这里选择触发脚本运行的内容。选项列表包括电池、位置、电话、传感器和信号强度。

坏消息是触发器功能不全,所以使用它们要自担风险。从好的方面来看,有一种方法可以使用稍微不同的方法实现一些相同的功能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-4。*移除触发按钮

请注意,如果您启动一个崩溃的应用,您可能会进入一个无限循环,每次 SL4A 启动时,它都会尝试启动您触发的脚本,然后它会再次崩溃。如果您可以进入通知屏幕并调出 SL4A 触发器,您应该能够按下 Cancel All 按钮并删除有问题的脚本。解决这个问题的唯一方法是卸载然后重新安装 SL4A。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-5。*触发激活菜单

基于方向的动作

这里有一个方便的脚本,如果你把手机正面朝下放在平面上,它会把你的手机置于静音模式。代码使用startSensingTimed API 调用来确定方向和移动。如果它确定设备是静止的并且基本上是水平的,它将使用toggleRingerSilentMode呼叫将振铃器设置为静音。代码如下所示:

`import android, time
droid = android.Android()

droid.startSensingTimed(1, 5)
silent = False
while True:
e = droid.eventPoll(1)

facedown = e.result and ‘data’ in e.result[0] and
e.result[0][‘data’][‘zforce’] and e.result[0][‘data’][‘zforce’] < -5
if facedown and not silent:
droid.vibrate(100)
droid.toggleRingerSilentMode(True)
silent = True
elif not facedown and silent:
droid.toggleRingerSilentMode(False)
silent = False

time.sleep(5)`

另一种检测电话被面朝下放置的方法是使用光传感器。下面是一个简短的片段,它将使用文本到语音(TTS)功能来让您知道手机何时面朝下:

`import android, time

droid = android.Android()
droid.startSensing()

while True:
result = droid.sensorsGetLight().result
if result is not None and result <= 10:
droid.ttsSpeak(‘I can’t see!’)
time.sleep(5)`

这可能是一个谈论日志的好地方。编写没有用户界面的程序的最大挑战之一是调试。调试“静默”代码的方法有很多——从插入打印语句到使用 Android SDK 的 DDMS 工具。大多数 Linux 系统应用都会生成某种类型的日志,明确用于监控执行和记录错误信息。Android 平台提供了一个名为 logcat 的日志工具。有一个名为log的 API 函数,它会将您想要的任何字符串消息写到logcat文件中。或者,您可以写入自己的日志文件。

在第九章中,我将详细介绍一个使用日志记录信息的复杂应用。下面是一些日志条目的样子:

{"task":"loadconfig"} <type 'unicode'> ok... {u'task': u'loadconfig'} loadconfig {"sections": {"locale": [{"name": "prefix", "value": "+60", "description": "International prefix. Used to clean up phone numbers before sending.\nThis will only affect numbers that do not yet have an international code.\nExamples (assuming prefix is +60):\n0123456789 will become +60123456789\n60123456789 will become +60123456789\n+49332211225 remains unchanged"}], "merger": [{"name": "informeveryratio", "value": "10", "description": "Use TTS to inform you every total / n messages. Set to 1 if you do not wish to use this feature.\nExample\nIf you are sending 200 messages and set this value to 5, you will be informed by TTS of the status every 200 / 5 = 40 messages."}, {"name": "informevery", "value": "0", "description": "Use TTS to inform you every n messages. Set to 0 if you do not wish to use this feature."}], "application": [{"name": "showonlycsvfiles", "value": "0", "description": "While importing the CSV file, only files with the extension .csv will be shown if this is set to 1."}, {"name": "showonlytextfiles", "value": "1", "description": "While importing template text from a file, only files with the extension .txt will be shown if this is set to 1."}, {"name": "showhiddendirectories", "value": "0", "description": "While browsing, hidden directories (stating with '.') will not be shown if this is set to 1."}]}} Had to wait cause process was only 0.005585 second {"task":"listdir","path":"/sdcard","type":"csv"} <type 'unicode'> ok... {u'path': u'/sdcard', u'task': u'listdir', u'type': u'csv'} listdir Loading directory content {"files": ["._.Trashes", "handcent1.log"], "folders": ["accelerometervalues", "Aldiko", "amazonmp3", "Android", "astrid", "com.coupons.GroceryIQ", "com.foxnews.android", "com.googlecode.bshforandroid", "com.googlecode.pythonforandroid", "data", "DCIM", "Digital Editions", "documents", "download", "Downloads", "droidscript", "dropbox", "eBooks", "Evernote", "gameloft", "gReader", "Grooveshark", "handcent", "HTC Sync", "ItchingThumb", "jsword", "logs", "LOST.DIR", "Mail Attachments", "media", "mspot", "Music", "My Documents", "pulse", "rfsignaldata", "rosie_scroll", "rssreader", "Sample Photo", "skifta", "sl4a", "StudyDroid", "swiftkey", "tmp", "TunnyBrowser", "twc-cache"]} {"task":"listdir","path":"/sdcard/sl4a","type":"csv"} <type 'unicode'> ok... {u'path': u'/sdcard/sl4a', u'task': u'listdir', u'type': u'csv'} listdir Loading directory content {"files": ["battery.py.log", "BeanShell 2.0b4.log", "DockProfile.py.log", "downloader.py.log", "downloaderv2.py.log", "DroidTrack.py.log", "geostatus.py.log", "getIPaddr.py.log", "hello_world.bsh.log", "httpd.py.log", "netip.py.log", "null.log", "Python 2.6.2.log", "Shell.log", "simpleHTTP2.py.log", "smssender.py.log", "speak.py.log", "ssid2key.py.log", "test.py.log", "trackmylocation.py.log", "weather.py.log", "wifi.py.log", "wifi_scanner.py.log"], "folders": ["extras", "scripts"]} Had to wait cause process was only 0.025512 second {"task":"listdir","path":"/sdcard/sl4a/scripts","type":"csv"} <type 'unicode'> ok... {u'path': u'/sdcard/sl4a/scripts', u'task': u'listdir', u'type': u'csv'} listdir

如果你仔细观察,你会注意到许多不同类型的条目。有一些信息条目来标识特定代码段何时被执行,比如loadconfig。其他条目转储 Python 变量的内容,如以下行:

{u'path': u'/sdcard/sl4a/scripts', u'task': u'listdir', u'type': u'csv'}

花括号将该对象标识为包含总共三个键/值对的 Python 字典。对于日志文件中的内容,您有很大的灵活性。下面是来自第九章 SMSSender 应用的代码,用于打开一个日志文件:

`# Prepare a log file

TODO: Would be better thing to use the python logger instead

LOG = “…/SMSSender.py.log”
if os.path.exists(LOG) is False:
f = open(LOG, “w”)
f.close()
LOG = open(LOG, “a”)`

要写入条目,只需使用LOG.write(message)将字符串message写入日志文件。SMSSender 应用使用一个函数将消息写入终端和日志文件。代码如下:

`def log(self, message):
“”" Log and print messages

message – Message to log
“”"
LOG.write(message)
print message`

定义了 log 函数后,您可以使用如下语句:

self.log("Selected filename %s " % filename)

在创建任何类型的服务应用时,日志记录都是工具箱中的重要工具。当你的程序停止工作,你需要查看当时发生了什么时,它会非常方便。也许你写的代码完美无缺,但对我来说并不总是这样。

基于位置的动作

可能有一些你经常去的地方,当你在那里的时候,你肯定希望你的手机静音。教堂可能是其中之一,也可能是疗养院、医院或图书馆。您可以创建一个脚本,非常类似于基于传感器的操作,它将检测您的位置并采取特定的操作。您需要知道的是该位置的 GPS 坐标。

为了让这个脚本工作,我们需要一些辅助函数来计算从当前位置到“特殊”位置的距离。对于这个搜索,你可能想尝试一下[stackoverflow.com](http://stackoverflow.com)站点。这个网站有大量的编码问题问答。下面是在[stackoverflow.com](http://stackoverflow.com)上找到的一段代码,用于使用哈弗辛公式计算两个 GPS 点之间的距离:

`from math import *

def haversine(lon1, lat1, lon2, lat2):
“”"
Calculate the great circle distance between two points
on the earth (specified in decimal degrees)
“”"

convert decimal degrees to radians

lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])

haversine formula

dlon = lon2 - lon1
dlat = lat2 - lat1 a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * atan2(sqrt(a), sqrt(1-a))
km = 6367 * c
return km`

有了这些,我们现在只需要写一个简短的脚本来获取我们的当前位置,然后使用我们的固定位置调用哈弗辛函数。如果我们在一个固定的距离内(例如,小于 1000 英尺),我们将打开手机的静音模式。

`import android, time
droid = android.Android()

lat1 = 33.111111
lon1 = 90.000000

droid.startLocating()

time.sleep(15)
while True:
loc = droid.readLocation().result
if loc = {}:
loc = getLastKnownLocation().result
if loc != {}:
try:
n = loc[‘gps’]
except KeyError:
n = loc[‘network’]
la = n[‘latitude’]
lo = n[‘longitude’]

if haversine(la, lo, lat1, lon1) < 1:
droid.toggleRingerSilentMode(True)
else:
droid.toggleRingerSilentMode(False)`

基于时间的操作

这里有一个简单的脚本,可以让你在一天中的特定时间将手机设置为静音,然后在另一个时间再打开铃声。把它当成你的“我睡觉时不要打扰”剧本。

`“”" Silences the phone between set hours

Meant for use on Android phones with the SL4A application
“”"

Created by Christian Blades (christian.blades@docblades.com) - Mon Mar 08, 2010

import android
import datetime
from time import sleep # MIN_HOUR and MAX_HOUR take an integer value between 0 and 23

12am == 0 and 1pm == 13

MIN_HOUR = 23
MAX_HOUR = 6

if MIN_HOUR > 23 or MIN_HOUR < 0 or MAX_HOUR > 23 or MAX_HOUR < 0:

If the min and max values are out of range, raise an error

raise ValueError(“0 <= (MIN_HOUR|MAX_HOUR) <= 23”)

d_now = datetime.datetime.now

d_min = d_now().replace(hour=MIN_HOUR, minute=0, second=0)
d_max = d_now().replace(hour=MAX_HOUR, minute=0, second=0)

a_day = datetime.timedelta(days=1)

droid = android.Android()

def td_to_seconds(td):
“”" Convert a timedelta to seconds “”"
return td.seconds + (td.days * 24 * 60 * 60)

def advance_times():
“”" Advance for the following day “”"
d_min = d_min + a_day
d_max = d_max + a_day
return

def wait_for(dt):
“”" Wait until dt “”"
sleep(td_to_seconds(dt - d_now()))

def main_loop():
“”"
Infinite loop that silences and unsilences the phone on schedule

1. Wait for silent time
2. Silence the phone
3. Wait for awake time
4. Turn on the ringer
5. Advance the min and max to the following day
6. Repeat

NOTE: Must start during a loud period
“”"
while True:
wait_for(d_min)
droid.makeToast(“Goodnight”)
droid.setRingerSilent(True)
wait_for(d_max) droid.makeToast(“Good morning”)
droid.setRingerSilent(False)
advance_times()

t_now = d_now()

if MAX_HOUR < MIN_HOUR:

Do a little extra processing if we’re going from

a larger hour to a smaller (ie: 2300 to 0600)

if t_now.hour <= d_min.hour and t_now.hour < d_max.hour:

If it’s, say, 0200 currently and we’re going from 2300 to 0600

Make the 2300 minimum for the previous night

d_min = d_min - a_day
elif t_now.hour >= d_min.hour and t_now.hour > d_max.hour:

In this case, it’s 0900 and we’re going from 2300 to 0600

Make the maximum for the next morning

d_max = d_max + a_day

print "Now: " + t_now.ctime()
print "Min: " + d_min.ctime()
print "Max: " + d_max.ctime()

if t_now >= d_min and t_now < d_max:

Is it silent time now?

If so, do the silent stuff, then enter the loop

droid.makeToast(“Goodnight”)
droid.setRingerSilent(True)
wait_for(d_max)
droid.setRingerSilent(False)
advance_times()

main_loop()`

基于运行时间的触发器

创建在一段时间后或特定时间触发的脚本非常简单。下面是一个代码片段,它每十秒钟打印一条消息:

`import android, time

droid = android.Android()

make Toast every ten seconds.

while True:
droid.makeToast(‘New Toast’)
time.sleep(10)`

以这个想法为起点,你可以构建各种各样的脚本。如果您想要构建几个脚本来设置一个固定的计时器在一个小时后响起,或者一个钟声在整点时响起,该怎么办?在走得太远之前,您需要做几件事情。首先,你需要一个声音来提醒你。在谷歌上快速搜索警报声会出现各种各样的结果。我在网站上找到了一个不错的收藏。其中许多都是.wav格式的。幸运的是,你的 Android 设备可以毫无问题地播放.wav文件。

我们将使用mediaPlay API 函数来实际播放声音。如果您愿意,可以在模拟器上测试这一点。首先,您需要创建一个目录来保存您的声音文件,然后使用adb push命令将声音文件推送到设备,如下所示:

adb shell mkdir /sdcard/sounds adb push alarm.wav /sdcard/sounds/

从那以后,这个脚本非常简单,因为它只是使用 Python 标准库time.sleep例程休眠一个小时,然后播放声音。剧本是这样的:

`import android
from time import sleep

droid = android.Android()

This script will simply sleep for an hour and then play an alarm

droid.makeToast(‘Alarm set for 1 hour from now’)
time.sleep(3600)
droid.mediaPlay(‘file:///sdcard/sounds/alarm.wav’)`

消逝时间主题的一个微小变化是以固定的时间间隔执行一个动作,例如在每小时的顶部和底部发送包含当前位置信息的 SMS。这在不需要昂贵服务的情况下追踪某人的行踪是很有用的。发送短信需要一行代码,如下所示:

droid.smsSend('8005551234','Test from Android')

要添加获取当前位置的代码,首先必须调用startLocating函数开始收集位置信息。接下来,您调用readLocation来实际读取您当前的位置,最后调用stopLocating来关闭定位功能。我们将增加 15 秒的延迟,以便在 GPS 打开时给它一点时间来调整。如果我们没有 GPS 信号,我们将使用基于网络信息的当前位置。代码如下所示:

`droid = android.Android()
droid.startLocating()
time.sleep(15)
loc = droid.readLocation()
droid.stopLocating()

if ‘gps’ in loc.result:
lat = str(loc.result[‘gps’][‘latitude’])
lon = str(loc.result[‘gps’][‘longitude’])
else:
lat = str(loc.result[‘network’][‘latitude’])
lon = str(loc.result[‘network’][‘longitude’]) now = str(datetime.datetime.now())
outString = 'I am here: ’ + now + ’ ’ + lat + ’ ’ + lon

droid.smsSend(‘8005551234’, outstring)`

FTP 文件同步工具

让两台或多台机器之间的文件或目录保持同步是你一旦开始使用就离不开的任务之一。使用任何数量的商业程序都有许多方法来完成这项任务。使用 SL4A 同步文件的一种方法是使用 FTP 服务器。在 Linux、Mac OS X 和 Windows 上安装和配置 FTP 服务器非常简单。我将在这里为您概述这些步骤。

在 Mac OS X 上,您需要通过点按屏幕右上角的苹果图标并选择“偏好设置”来打开“系统偏好设置”工具。您应该会看到一个类似于图 6-6 中的窗口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-6。 Mac OS X 系统偏好设置屏幕

FTP 服务是共享偏好设置的一部分,因此通过点按图标打开该文件夹。你会看到另一个窗口,如图图 6-7 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-7。 Mac OS X 文件共享偏好设置

接下来,在服务列表中找到文件共享条目,并确保选中开启复选框(参见图 6-7 )。最后,点击用户列表上方的选项按钮,弹出文件共享选项窗口,如图图 6-8 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-8。 Mac OS X 文件共享偏好设置

点击使用 FTP 共享文件和文件夹将实际启动 FTP 服务器。为了远程访问 FTP 服务器,您需要在 Mac 电脑上有一个用户帐户。在 Linux 上,我使用一个名为 vsftpd 的程序。这是一个免费的 FTP 服务器,安装简单,与最新版本的 Ubuntu 配合使用效果很好。要安装它,你使用一个单一的apt-get命令,如图图 6-9 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-9。*在 Ubuntu 10.11 中从终端窗口安装 vsftpd

下载完成后,该程序将自动启动。您不必对配置做任何更改,因为像匿名连接这样的东西在默认情况下是禁用的。如果您想检查配置文件,它位于/etc目录中,命名为vsftpd.conf。图 6-10 显示了在命令提示符下使用 Windows FTP 客户端连接到 Linux 机器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-10。*从 Windows 命令提示符连接到 vsftp】

在 Windows 上,从 Windows 功能屏幕启用 FTP 服务器。进入该屏幕最简单的方法是按下键盘上的 windows 图标键,然后在搜索框中键入 Windows 功能。在“控制面板”下,您应该看到的第一个条目是“打开或关闭 Windows 功能”行。点击这一行将打开 Windows 功能面板,如图 6-11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-11。*从 Windows 功能控制面板工具启用 Windows FTP 服务器

在 Windows 功能屏幕打开的情况下,您需要检查两件事情以使您的 FTP 服务器运行:FTP 服务必须启用,并且您需要 IIS 管理控制台来管理 FTP 服务。安装完成后,您应该能够启动 IIS 管理控制台并配置您的 FTP 服务。

为此,我们将使用同样的技术,按下键盘上的 Windows 图标键,并在搜索框中键入 Internet。这将显示几个选项,包括 Internet Explorer 和 Internet 信息服务(IIS)管理器(参见图 6-12 )。接下来,您希望启动 IIS 管理器并检查 FTP 服务的当前设置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-12。*快速启动菜单中的互联网信息服务(IIS)管理器

Windows 7 的所有默认设置都与 Linux 上的vsftpd相似,匿名登录被禁用。您可以从 IIS 管理器控制台调整许多其他配置设置,如图 6-13 中的所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-13。*互联网信息服务(IIS)管理器屏幕

一旦您启用了服务器软件,您将需要实际创建一个供 FTP 服务使用的站点。这可以通过右键单击左侧窗格中的“站点”文件夹,或者选择“站点”文件夹并单击操作窗格中的“添加 Ftp 站点”行来完成。您将看到几个对话框来指导您设置一个新的 FTP 站点。第一个对话框提示输入文件的名称和物理位置(见图 6-14 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-14。 FTP 站点信息对话框

当你点击下一步时,你会看到一个类似于图 6-15 的对话框。您可以在这里将 FTP 服务器分配到特定的 IP 地址(在本例中是机器的 IP 地址)并设置 SSL 设置。我们不需要 SSL 加密,因为它只能在本地网络上运行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-15。 FTP 站点绑定和 SSL 设置

再次单击“下一步”会将您带到最后一个对话框,您必须在其中配置身份验证规则。因为您将需要登录,所以向任何经过身份验证的用户授予完全访问权限,如图图 6-16 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-16。 FTP 站点认证设置

在 Windows 7 中,你需要做的最后一件事是更改防火墙设置以允许 FTP 连接。这可以在具有管理员权限的命令窗口中完成,如图图 6-17 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-17。*修改 Windows 7 防火墙设置的命令

在 Windows 上配置 FTP 服务器显然比在 Linux 或 Mac OS X 上要繁琐一些。您可以使用其他第三方 FTP 服务器程序,但我想向您展示如何让它与基本操作系统一起工作。如果你做的一切都正确,你应该在 IIS 管理器屏幕上看到你的 FTP 站点,状态为 Started,如图 6-18 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-18。 IIS 管理器显示同步 FTP 站点已启动

现在我们已经解决了服务器部分的问题,我们可以继续使用 SL4A 构建一个小的客户端工具。好消息是 Python 标准库提供了一个用于构建客户端代码的ftplib模块,因此您不必去寻找任何东西。使用ftplib模块非常简单,主要包括识别目标系统(HOST)和登录所需的用户凭证。代码的主要部分通过比较两个目录中的文件列表来保持两个目录的同步。正如所写的,同步是从设备到远程服务器的一种方式,但是您可以修改它,而不需要很多额外的编码。

剧本是这样的:

`import ftplib
import time
import os

import android
droid = android.Android()

HOST = ‘192.168.1.81’
USER = ‘user’
PASS = ‘pass’
REMOTE = ‘phone-sync’
LOCAL = ‘/sdcard/sl4a/scripts/ftp-sync’ if not os.path.exists(LOCAL):
os.makedirs(LOCAL)

while True:
srv = ftplib.FTP(HOST)
srv.login(USER, PASS)
srv.cwd(REMOTE)

os.chdir(LOCAL)

remote = srv.nlst()
local = os.listdir(os.curdir)
for file in remote:
if file not in local:
srv.storlines('RETR ’ + file,
open(file, ‘w’).write)

srv.close()
time.sleep(1)`

与 Flickr 同步照片

Flickr 是分享照片的绝佳服务。在许多带有摄像头的 Android 设备上,Gallery 应用提供了一个分享个人照片的选项。如果你可以运行一个脚本,将你所有的照片同步到 Flickr,那不是很好吗?这就是 SL4A 出现的原因。

寻找代码来完成这项艰巨的工作是另一个简单的谷歌搜索。虽然有很多选择,但我选定了一个名为uploader.py的。它已经存在了一段时间,并被一些博客帖子引用。如果您选择使用这段代码,您还需要一个名为xmltramp.py的文件。这段代码提供了许多由uploader.py使用的 XML 函数。在您尝试在 Android 设备上使用之前,在您的桌面上测试一下代码并不是一个坏主意。这是一个很好的主意,主要是为了通过 Flickr 授权你的应用。

第一次运行代码时,你会看到一个 Yahoo 登录界面,如图 6-19 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-19。*雅虎 Flickr 登录界面

接下来,你会看到一个页面,要求你授权uploader.py程序与你的 Flickr 账户通信。该屏幕看起来类似于图 6-20 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-20。 Flickr 授权屏幕

点击“下一步”后,你至少还会看到一个屏幕,然后你会看到类似于图 6-21 的东西,让你知道你的应用已经被授权连接 Flickr。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-21。*成功授权画面

上传图片的代码非常简单。下面是uploadImage函数的样子:

def uploadImage( self, image ): if ( not self.uploaded.has_key( image ) ): print "Uploading ", image, "...", try: photo = ('photo', image, open(image,'rb').read()) d = { api.token : str(self.token), api.perms : str(self.perms), "tags" : str( FLICKR["tags"] ), "is_public" : str( FLICKR["is_public"] ), "is_friend" : str( FLICKR["is_friend"] ), "is_family" : str( FLICKR["is_family"] ) } sig = self.signCall( d ) d[ api.sig ] = sig d[ api.key ] = FLICKR[ api.key ] url = self.build_request(api.upload, d, (photo,)) xml = urllib2.urlopen( url ).read() res = xmltramp.parse(xml) if ( self.isGood( res ) ): print "successful." self.logUpload( res.photoid, image ) else : print "problem.." self.reportError( res ) except: print str(sys.exc_info())

与谷歌文档同步

“谷歌文档”是一个很好的方式,可以让你在任何有互联网接入和网络浏览器的地方创建电子表格或文字处理文档。涉及 Google Docs 和 Python 的后台任务的一个想法是自动呼叫日志同步工具。这个工具每天运行一次,用你当天的活动更新谷歌文档中的一个电子表格。我们将在这里使用一些新技术来访问 Google Docs 上的一个帐户,并通过首先下载当月的电子表格,然后追加当天的条目来进行电子表格追加。最后,新的电子表格将被上传回 Google Docs。

首先,我们将使用第五章中的脚本来获得今天通话的副本。下面是这个片段的样子:

myconst = droid.getConstants("android.provider.CallLog$Calls").result calls=droid.queryContent(myconst["CONTENT_URI"],["name","number","duration"]).result for call in calls:

该代码片段将在您的 Google 文档电子表格中插入一个新行:

`import time
import gdata.spreadsheet.service
email = ‘youraccount@gmail.com’
password = ‘yourpassword’
weight = ‘180’
spreadsheet_key = ‘pRoiw3us3wh1FyEip46wYtW’

All spreadsheets have worksheets. I think worksheet #1 by default always

has a value of ‘od6’

worksheet_id = ‘od6’
spr_client = gdata.spreadsheet.service.SpreadsheetsService()
spr_client.email = email
spr_client.password = password
spr_client.source = ‘Example Spreadsheet Writing Application’
spr_client.ProgrammaticLogin()

Prepare the dictionary to write

dict = {}
dict[‘date’] = time.strftime(‘%m/%d/%Y’)
dict[‘time’] = time.strftime(‘%H:%M:%S’)
dict[‘weight’] = weight
print dict
entry = spr_client.InsertRow(dict, spreadsheet_key, worksheet_id)
if isinstance(entry, gdata.spreadsheet.SpreadsheetsList):
print “Insert row succeeded.”
else:
print “Insert row failed.”

millis = int(msgs.result[0][‘date’])/1000
strtime = datetime.datetime.fromtimestamp(millis)
strtime`

图 6-22 显示了我们的文档在谷歌文档中的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 6-22。*包含通话记录数据的谷歌文档电子表格

启动启动器

现在,我已经给了你很多关于小服务脚本的想法,让我们用一个启动器应用来结束这一章,它将结合一些想法,如日志记录和启动后台脚本,将它们结合在一起。如果您知道意图或活动名称,也可以使用这个脚本启动其他非 SL4A 应用。

这是最终的脚本:

`import android

STARTUP_SCRIPTS = (
‘facedown.py’,
‘logGPS.py’,
‘silentnight.py’
)

droid = android.Android()

LOG = “…/logtest.py.log”
if os.path.exists(LOG) is False:
f = open(LOG, “w”)
f.close()
LOG = open(LOG, “a”) for script in STARTUP_SCRIPTS:
extras = {“com.googlecode.android_scripting.extra.SCRIPT_PATH”:
“/sdcard/sl4a/scripts/%s” % script}
myintent = droid.makeIntent(
“com.googlecode.android_scripting.action.LAUNCH_BACKGROUND_SCRIPT”,
None, None, extras, None,
“com.googlecode.android_scripting”,
“com.googlecode.android_scripting.activity.ScriptingLayerServiceLauncher”).result
droid.startActivityIntent(myintent)
LOG.write(“Starting %s\n” % script)`

我们将添加到脚本启动器的最后一个东西是一个额外的脚本,它将打开一个文本文件并从需要报警的事件列表中读取。它非常简单,将是我们的启动启动器将要加载的脚本之一。

代码如下:

`import time

import android

droid = android.Android()

SCHEDULE = ‘/sdcard/sl4a/scripts/schedule.txt’

Parse the schedule into a dict.

alerts = dict()
for line in open(SCHEDULE, ‘r’).readlines():
line = line.strip()
if not line: continue
t, msg = line.split(’ ', 1)

alerts[t] = msg

Check the time periodically and handle alarms.

while True:
t = time.strftime(‘%H:%M’)
if t in alerts:
droid.vibrate()
droid.makeToast(alerts[t])
del alerts[t]

time.sleep(5)`

schedule.txt文本文件将包含任意数量的带有时间和消息字符串的行。这是一个可能的例子:

17:00 Time to head home! 21:00 Put the trash out 22:00 Set the alarm

请注意,所有时间都必须使用 24 小时制。现在,我们有办法在启动时启动任意数量的不同脚本,将您的 Android 设备变成一个强大的通知工具。

总结

本章将通过一些例子向您展示如何使用 SL4A 和 Python 来自动化在后台运行的任务。

这是本章的要点列表:

  • 在启动时启动脚本:使用新的 on boot 应用,您可以设置任何 SL4A 脚本在每次设备启动时启动。只有在彻底测试了你的脚本之后,才使用这个函数。
  • 基于传感器采取行动:任何正在运行的脚本都可以访问 Android 设备的全部感知能力,您可以基于任何传感器输入采取行动。
  • 基于时间的动作:您可以使用标准的 Python 定时器函数来创建基于时间的脚本。只要你不设置无限计时器,这真的是一个很容易的事情。请记住,如果您确实创建了一个“无限循环”应用,您可以从通知屏幕中终止任何 SL4A 脚本。

七、Python 脚本工具

本章将介绍如何使用 Python 来完成 SL4A 的不同工具任务。在典型的个人电脑上,这些属于命令行工具类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 撰写本章时使用的 SL4A 版本是基于 Python 2.6.2 的。本章中的所有例子都是用 Python 2.6.4 在 Windows 7 64 位机器和基于 Android 2.2 的模拟器上测试的。

是时候开始了。以下是本章将要研究的内容:

  • Python 库以及如何使用它们
  • 基于电子邮件的应用
  • 基于位置的应用
  • 用于传输文件的 Web 服务器

Python 库

有大量的库可供 Python 语言完成从操作 MP3 ID3 标签到在 JPEG 图像中读写 EXIF 数据的所有任务。在 SL4A 项目中使用它们的技巧是将它们安装在目标设备上。如果这个库完全是用 Python 编写的,那么您应该能够毫无问题地使用它。如果这个库实际上是一个二进制模块的包装器,事情会变得有点困难,就像任何基于开源 Lame 项目的 MP3 工具一样。虽然有一种方法可以让二进制模块重新编译并针对 ARM 架构,但这并不是一项简单的任务。

使用现有库的其他挑战来自它们通常的分发方式。可能还需要额外的依赖项。大多数情况下,您会找到一个从终端窗口用如下命令运行的setup.py文件:

python setup.py install

该命令通常会将库安装到 Python 站点包目录中。这种方法在 Android 设备上的唯一问题是站点包目录在非根设备上是只读的。在一个单独的.py文件中,有相当数量的库是独立的。如果是这种情况,那么你所要做的就是将文件复制到设备和正确的目录中。

这可能是一个谈论当你在你的设备上安装 Python 时下载的.zip文件中的内容的好时机。如果您在安装 Python 解释器时注意了一下,您会看到三个文件经过。如果你错过了,你仍然可以看到 adb 命令的文件,如图 7-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-1。*设备上 Python 目录的内容

python_r7.zip文件包含执行解释器所需的基本 Python 文件。您将在python_scripts_r8.zip文件中找到十个示例程序,您可以在学习中使用它们。test.py文件是一个很好的起点,因为它包含了不同对话调用的测试套件。最后,python_extras_r8.zip文件包含了许多帮助函数和库,项目维护人员认为这对 Python 开发人员会有帮助。

您可以使用以下命令将python_extras_r8.zip文件的副本下载到您的开发工作站:

adb pull /sdcard/com.googlecode.pythonforandroid/python_extras_r8.zip

该文件包含您期望在典型 Python 安装的 site-packages 目录中找到的内容。如果你打开 zip 文件,你会看到一个类似于图 7-2 的文件和目录列表。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-2。*Python extras 的内容。zip 文件

如果您的开发机器使用的是 Windows,您会在C:\Python26\Lib\site-packages中找到对应的目录。当在 Android 设备上使用 Python 时,有一种方法可以向PYTHONPATH变量添加本地路径。这需要两行代码,因此:

import sys sys.path.append('/sdcard/sl4a/mylib')

在本例中,目录/sdcard/sl4a/mylib包含您希望 Python 在您的设备上可用的文件。使用 Python 库最简单的方法是以鸡蛋的形式出现。Python 支持库的 zip 压缩文件格式,使用.egg作为文件扩展名。它在概念上类似于 Java 中的.jar文件。使用 Python .egg文件所要做的就是将它复制到设备上适当的目录中。这可以通过如下的adb push命令来实现:

adb push library.egg /sdcard/com.googlecode.pythonforandroid/extras/python

基于电子邮件的应用

发送电子邮件是我们大多数人认为理所当然的事情。在移动电子邮件的世界里,我们可能要感谢黑莓设备,因为它把它带给了你,无论你身在何处。Android 设备默认有电子邮件,并与谷歌的 Gmail 紧密集成。这使得编写发送电子邮件的工具脚本的想法非常有吸引力。

SL4A Android facade 提供了一个sendEmail API 调用。这个函数有三个参数:to_address(以逗号分隔的接收者列表)、titlemessage。从那里,它将信息传递给默认的电子邮件应用。然后,您必须使用该应用实际发送消息。如果您碰巧在设备上注册了多个应用来处理电子邮件,系统还会提示您选择使用哪一个。虽然这种方法确实有效,但它并没有真正完成手头的任务。我的意思是,你可以使用内置的电子邮件程序,但这将是乏味的,我真正想要的是一种自动发送电子邮件的方式。这就是 Python 来拯救我们的地方。

我们将为这个任务使用的库是smtplib。它是 Python 标准库的一部分,所以你不需要做任何特别的事情就可以使用它。我们还将利用 Gmail 的 SMTP 服务来发送邮件。此外,我们将使用email库,它包含许多帮助函数,允许我们以正确的形式构造消息。最后,我们将使用mimetypes库来帮助我们对消息进行编码。email库提供了一个叫做MIMEMultipart的东西,它让我们定义一封电子邮件的不同部分。以下是用 Python 创建消息的方法:

# Create an SMTP formatted message msg = MIMEMultipart() msg['Subject'] = 'Our Subject' msg['To'] = 'receiver@host.net' msg['From'] = 'sender@gmail.com' msg.attach(MIMEText(body, 'plain'))

msg 结构中使用的大部分数据都是 string 类型的,所以创建消息的主体很简单。因为 Google 需要认证才能通过 SMTP 服务器发送邮件,所以您需要有一个 Gmail 帐户才能使用这个脚本。

下面是从命令行与 Google SMTP 服务器通信的样子。要启动 Python,您需要在 Linux 或 Mac OS X 上打开一个终端窗口,或者在 Windows 上打开一个命令提示符。在那里,您应该能够输入 Python:

`>>> smtpObj = smtplib.SMTP(smtp_server,smtp_port)

smtpObj.starttls()
(220, ‘2.0.0 Ready to start TLS’)
smtpObj.ehlo()
(250, ‘mx.google.com at your service, [72.148.19.136]\nSIZE 35651584\n8BITMIME\nAUTH外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
LOGIN PLAIN XOAUTH\nENHANCEDSTATUSCODES’)
smtpObj.login(username,password)
(235, ‘2.7.0 Accepted’)
smtpObj.sendmail(username,to_addr,msg.as_string())
smtpObj.close()`

如果你计算一下代码的行数,你只需要五行来设置消息,六行来发送消息。就代码效率而言,这还不错。您可能希望在最终的脚本中添加一些错误检查,但是编写一个有用的电子邮件发送工具应该不需要太多的代码。既然我们已经有了创建通用电子邮件发送者的基础,那么发送什么才是真正有用的呢?为什么不是你所有的短信?

SMS facade 提供了对批量或一次一条 SMS 消息的简单访问。如果你想得到一切,你应该使用smsGetMessages。在我们深入讨论之前,我们应该研究一下每条短信都有哪些信息。您可以做的第一件事是使用smsGetAttributes函数来查看您可以检索哪些数据。这是在模拟器上运行的样子:

>>> pprint.pprint(droid.smsGetAttributes().result) [u'_id', u'thread_id', u'address', u'person', u'date', u'protocol', u'read', u'status', u'type', u'reply_path_present', u'subject', u'body', u'service_center', u'locked', u'error_code', u'seen']

现在我们知道了什么是可用的,我们可以使用smsGetMessages函数创建一个列表,然后遍历这个列表,只提取我们感兴趣的信息。首先,我们需要在模拟器上创建一些消息供我们使用。这需要使用在第三章中介绍的 ADB 工具的一点命令行技巧。在 Windows 上,您必须打开一个命令窗口并键入telnet localhost 5554。图 7-3 显示了 telnet 屏幕和生成几条 SMS 消息所需的命令。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-3。*使用 telnet 向模拟器发送短信

现在我们可以使用smsGetMessages函数来读取所有的消息,通过传入一个False参数来表明我们不仅仅想要未读的消息。实际上,在这种情况下这并不重要,因为所有这些消息都是刚刚收到的,无论如何我们都会得到相同的结果。

`>>> msgs = droid.smsGetMessages(False)

pprint.pprint(msgs.result)
[{u’_id’: u’3’,
u’address’: u’3035551212’,
u’body’: u’“This is a another test message from Telnet”‘,
u’date’: u’1297814134176’,
u’read’: u’0’},
{u’_id’: u’2’,
u’address’: u’3025551212’,
u’body’: u’“This is test message 2 from Telnet”‘,
u’date’: u’1297814117225’,
u’read’: u’0’},
{u’_id’: u’1’,
u’address’: u’3015551212’,
u’body’: u’“This is test message 1 from Telnet”‘,
u’date’: u’1297814100976’,
u’read’: u’0’}]`

在这一点上值得注意的是,消息是按时间倒序导出的。另一个值得注意的项目是消息的内容。即使smsGetAttributes函数向我们展示了更多可能的字段,我们在这里只得到_idaddressbodydateread。对于 SMS 消息,地址实际上是一个电话号码。除非你知道你在看什么,否则这个字段可能看起来有点奇怪。

这就是 Python datetime库帮助我们的地方。事实证明,date字段实际上是从 1 月 1 日开始的毫秒数。因此,我们所要做的就是将date字段除以 1000,并将该数字传递给datetime,如下所示:

`>>> millis = int(msgs.result[0][‘date’])/1000

strtime = datetime.datetime.fromtimestamp(millis)
strtime
datetime.datetime(2011, 2, 15, 17, 55, 34)`

这里很酷的一点是strtime是一个对象,我们可以很容易地用它来获取内容:

>>> print('Message time = %d:%d:%d') % (strtime.hour, strtime.minute, strtime.second) Message time = 17:55:34

更简单的是使用strftime方法来格式化时间,如下所示:

>>> strtime.strftime("%m/%d/%y %H:%M:%S") '02/15/11 17:55:34'

现在,我们应该拥有了构建一个脚本来将设备上的所有 SMS 消息发送到一个电子邮件地址所需的所有组件。下面是最终代码的样子:

`import android, datetime, smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

droid = android.Android() smtp_server = ‘smtp.gmail.com’
smtp_port = 587
mailto = ‘paul’
mailfrom = ‘paul’
password = ‘password’

Build our SMTP compatible message msg = MIMEMultipart()

msg[‘Subject’] = ‘SMS Message Export’
msg[‘To’] = mailto
msg[‘From’] = mailfrom

Walk throu the SMS messages and add them to the message body

SMSmsgs = droid.smsGetMessages(False).result

body = ‘’
for message in SMSmsgs:
millis = int(message[‘date’])/1000
strtime = datetime.datetime.fromtimestamp(millis)
body += strtime.strftime(“%m/%d/%y %H:%M:%S”) + ‘,’ + message[‘address’] + ‘,’ +外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
message[‘body’] + ‘\n’

msg.attach(MIMEText(body, ‘plain’)) smtpObj = smtplib.SMTP(smtp_server,smtp_port)
smtpObj.starttls()
smtpObj.login(mailfrom,password)
smtpObj.sendmail(mailfrom,mailto,msg.as_string())
smtpObj.close()`

图 7-4 显示了收到的邮件在 Gmail 网络界面中的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-4。*带短信的电子邮件信息

通用电子邮件工具还有许多其他用途。这个例子向您展示了如何创建一个消息,然后使用smtpObj发送它。对于示例脚本,我们真正应该做的最后一件事是添加一个选项,在电子邮件发送后删除所有 SMS 消息。下面是一个五行脚本,它将删除所有短信。请小心使用,因为它在删除所有内容之前不会要求任何确认:

import android droid = android.Android() msgids = droid.smsGetMessageIds(False).result for id in msgids: droid.smsDeleteMessage(id)

位置感知应用

移动设备的一个显著优势是能够知道你在哪里。SL4A 提供了一个具有许多功能的位置外观,这些功能可以在有或没有 GPS 功能的情况下工作。这为利用这些信息的应用提供了许多可能性。我将看看其中几个你可能会感兴趣的,包括一条关于我的位置的推文,以跟踪我的旅行。

在推特上发布我的位置

这个应用将需要一些外部库来完成这项工作。我们稍后将讨论 Twitter 库。我们需要做的第一件事是检查由readLocation API 调用返回的数据结构。图 7-5 显示了一个调用startLocating后调用readLocation的例子。

关于从该呼叫中可获得的位置信息,需要指出一些事情。当你看图 7-5 时,你注意到的第一件事是有两种类型的位置信息可用。readLocation返回一个使用字典封装位置信息的结果对象。这个 dictionary 对象有两个键,它们的值依次是包含位置信息的多个键/值对的字典。因此,要访问基于 GPS 的纬度和经度,您可以使用如下内容:

lat = result.result['gps']['latitude'] lon = result.result['gps']['longitude']外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-5。*读取位置 API 调用示例

这里的另一个要点是,如果当前没有启用 GPS,您的设备可能不会返回 GPS 位置。事实上,如果您在模拟器上尝试这段代码,结果对象将是空的。因此,如果您试图用前面的代码读取 GPS 位置,而 GPS 是关闭的,您会得到类似如下的错误:

>>> lat = droid.readLocation().result['gps'] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'gps'

在 Python 中,可以使用keys方法查看字典中有哪些键。GPS 关闭时的readLocation结果如下所示:

>>> droid.readLocation().result.keys() [u'network']

你也可以在条件语句中使用keys方法,就像这样:

>>> if 'gps' in droid.readLocation().result: print 'gps' else: print 'network'

我们需要调查的下一件事是与 Twitter 的通信。当您在 SL4A 中安装 Python 解释器时,您会得到一些为您安装的库,包括twitter.py。坏消息是 Twitter 已经开始要求一种更强的认证方法来连接到它的 API。

如果您不知道 OAuth 是什么,那么您可能应该了解一下。OAuth 是一个用于安全 API 授权的开放协议。它基本上涉及多个密钥和一个多步认证过程。在oauth.net有一个社区网站,在那里您可以找到 OAuth 规范、文档和大量示例代码的副本。包括 Google 在内的许多公共服务已经开始采用 OAuth 作为主要的身份验证方法,或者至少作为一种替代方法。

如果你曾经使用过第三方 Twitter 应用,你可能已经经历过授权该应用时必须经历的步骤。出于这个原因,我们将使用另一个库tweepy,它可以从[code.google.com/p/tweepy](http://code.google.com/p/tweepy)获得。

我假设此时你已经有了一个 Twitter 账户,不会带你完成注册过程。如果你不知道,就直接去twitter.com并按照那里的指示去做。一旦你有了一个帐户,你就可以注册一个新的应用([twitter.com/apps/new](http://twitter.com/apps/new))。图 7-6 显示了注册页面的截图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-6。*推特应用注册

在页面底部有一个验证码框,你必须正确输入才能注册你的应用。有一些你应该知道的警告。首先,你不能以你的应用的名义使用 Twitter。其次,您必须在应用网站框中输入有效的 URL。它不必是一个真实的 URL,但必须是正确的格式。正确填写表单并输入 CAPTCHA 短语后,您就可以单击 Save 按钮了。

一旦完成,你将得到一个类似于图 7-7 的页面。您将需要复制并粘贴您在下面的示例中收到的代码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-7。*推特应用详情

您在应用中需要的两样东西是消费者密钥和消费者秘密。您可以复制这些字段,然后将其粘贴到另一个文档中以供将来参考。我只是在 Windows 上打开记事本,创建一个文本文件来保存这些信息。现在我们有了消费者密钥和秘密,我们准备好连接 Twitter 了。

我们的下一步是使用消费者密钥和秘密来获得相应的应用密钥和秘密。我们将使用一点 Python 代码和空闲控制台来获取所需的应用信息,如下所示:

`>>> import tweepy

CONSUMER_KEY = ‘insert your Consumer key here’
CONSUMER_SECRET = ‘insert your Consumer secret here’
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth_url = auth.get_authorization_url()
print 'Please authorize: ’ + auth_url`

这将显示一个 URL,您必须复制并粘贴到网络浏览器中才能获得所需的密钥。网页看起来会像图 7-8 中的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-8。*推特应用授权

点击“允许”后,将进入下一页,如图图 7-9 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-9。*推特 PIN 授权码

现在我们有了一个 PIN 码,我们只需要再执行几行代码。以下是空闲状态下的后续步骤:

`>>> auth.get_access_token(‘type your PIN here’)
<tweepy.oauth.OAuthToken object at 0x02C0CE90>

print “ACCESS_KEY = ‘%s’” % auth.access_token.key
ACCESS_KEY = ‘access key code’
print “ACCESS_SECRET = ‘%s’” % auth.access_token.secret
ACCESS_SECRET = ‘access secret code’`

复制这两个应用代码,并将它们保存在为消费者代码创建的同一个文本文件中。从现在开始,你需要所有四个代码来认证和与 Twitter 通信。获取这些新代码并在 Twitter 上发布更新非常简单。事实上,您可以通过大约六行额外的代码来实现,如下所示:

`>>> ACCESS_KEY = ‘your just-obtained access key’

ACCESS_SECRET = ‘your just-obtained access secret’
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_KEY, ACCESS_SECRET)
api = tweepy.API(auth)
api.update_status(“Hello from the Apress Book Sample”)`

图 7-10 显示了如果你去twitter.com看时间线会是什么样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-10。*Python 消息的 Twitter 时间轴

现在我们拥有了编写tweetmylocation脚本所需的一切。把所有的片段放在一起,我们得到了这个:

`import android, datetime, time, tweepy

CONSUMER_KEY = ‘my consumer key’
CONSUMER_SECRET = ‘my consumer secret’

ACCESS_KEY = ‘my access key’
ACCESS_SECRET = ‘my access secret’

auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_KEY, ACCESS_SECRET)
api = tweepy.API(auth)

droid = android.Android()
droid.startLocating()
time.sleep(15)
loc = droid.readLocation()
droid.stopLocating()

if ‘gps’ in loc.result:
lat = str(loc.result[‘gps’][‘latitude’])
lon = str(loc.result[‘gps’][‘longitude’])
else:
lat = str(loc.result[‘network’][‘latitude’])
lon = str(loc.result[‘network’][‘longitude’])

now = str(datetime.datetime.now())
outString = 'I am here: ’ + now + ’ ’ + lat + ’ ’ + lon

api.update_status(outString)`

图 7-11 显示了运行tweetmylocation脚本的结果,如果你去twitter.com查看时间线(除了假的 GPS 位置)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-11。 Twitter 位置信息时间表

追踪我的旅行

既然我们知道了如何使用定位功能,那么偶尔查询并将信息保存到文件中就是一个非常简单的任务了。这对于追踪你在一天的越野旅行中走了多长时间和多远是很有用的。为了让这个应用工作,我们必须做一些基本的假设。首先,由于这个脚本将需要 GPS,并将定期读取位置,它可能需要将设备插入充电器,否则电池将在短时间内耗尽。其次,我们将依靠 Python 计时器来安排我们的测量,这意味着脚本将持续运行。虽然这没什么大不了的,但这只是让你的设备连接到电源而不依赖电池的另一个原因。

说完那些小细节,我们来说几个看家物品。尽可能少地对您的环境做出假设是一个很好的编程实践,因此我们将尝试遵循这一点,并在脚本中配置我们需要的一切。首先,我们希望 GPS 能够提供最准确的位置信息。目前,您必须手动打开 GPS,所以我们需要提示用户这样做。下面是一个小片段,它将发出对startLocating API 函数的调用,并等待 GPS 出现在从readLocation返回的结果中:

`droid = android.Android()
droid.startLocating()

while not droid.readLocation()[1].has_key(‘gps’) :
print “Waiting on gps to turn on”
time.sleep(1)`

接下来,我们需要能够写出包含时间和位置的日志,以便以后检索。这里最重要的事情是在设备的 sd 卡上选择一个已知的目录或创建我们自己的目录。Python 的操作系统模块使这些任务变得简单。最简单的做法是创建我们自己的目录来存储文件。选择一个名字可能是此时最大的决定。为了实际创建目录,我们将使用os.mkdir。这可能是这样的:

import os os.mkdir('/sdcard/logs')

在调用os.mkdir之前,您可以使用os.path.exists函数来检查目录。从编程的角度来看,这更有意义。把这个加进去会得到如下结果:

if not os.path.exists('/sdcard/logs'): os.mkdir('/sdcard/logs')

Python 处理文件 I/O 的方式与其他编程语言非常相似。首先,打开一个文件进行写操作以获得一个文件对象。确保您传递了’a’参数来隐式打开文件进行追加。如果不这样做,每次都会创建一个新文件。然后使用 file 对象上的write方法写入文件。这里有一小段可能是这样的:

f = open('/sdcard/logs/logfile.txt','a') f.write('First header line in file\n') f.close()

用 Python 读取文件甚至更容易。如果您想打开一个文件并阅读其中的每一行,您可以使用如下代码:

f = open('/sdcard/logs/logfile.txt') for line in f: print line f.close()

Python 文件对象是可迭代的,这意味着您可以使用for line if f:语法逐行读取文件。您可以使用这种方法来读取日志文件,并创建包含所有条目的电子邮件。我将把这个选项留给读者。这就是我们需要把这个脚本放在一起的全部内容。这是最终版本的样子:

`import android, os, time, datetime

droid = android.Android()
droid.startLocating()

while not droid.readLocation()[1].has_key(‘gps’) :
print “Waiting on gps to turn on”
time.sleep(1)

if not os.path.exists(‘/sdcard/logs’):
os.mkdir(‘/sdcard/logs’)

Now we’ll loop until the user closes the application

while True:
loc = droid.readLocation()

lat = str(loc.result[‘gps’][‘latitude’])
lon = str(loc.result[‘gps’][‘longitude’])
alt = str(loc.result[‘gps’][‘altitude’]) now = str(datetime.datetime.now())
f = open(‘/sdcard/logs/logfile.txt’,‘a’)
outString = now + ‘,’ + lat + ‘,’ + lon + ‘,’ + alt + ‘\n’
f.write(outString)
print outString
f.close()

time.sleep(1)`

因为我们在这个脚本中显式地等待 gps,所以不需要检查来自readLocation的结果中是否有 GPS 条目。在程序运行时给用户一些反馈也是一个不错的做法。在这种情况下,我们只需将写入文件的同一行输出到控制台。出于测试目的,我们可以像前面一样使用telnet命令向模拟器发送模拟的 GPS 信息。图 7-12 显示了一个例子以及对geo fix命令的帮助。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-12。*用于模拟 GPS 的安卓控制台

以下是使用仿真器运行的日志文件和使用 Android 控制台geo fix命令模拟的 GPS 数据的示例:

2011-02-16 11:32:35.178488,30,-85,0 2011-02-16 11:32:36.287759,30,-85,0 2011-02-16 11:32:37.331069,30.1234,-85.1234,0 2011-02-16 11:32:38.449301,30.1234,-85.1234,0 2011-02-16 11:32:39.555303,30.1234,-85.1234,0 2011-02-16 11:32:40.639048,0,0,0 2011-02-16 11:32:41.749413,0,0,0 2011-02-16 11:32:42.849682,0,0,0 2011-02-16 11:32:43.936020,0,0,0 2011-02-16 11:32:45.041614,0,0,0 2011-02-16 11:32:46.106619,0,0,0 2011-02-16 11:32:47.181367,0,0,0 2011-02-16 11:32:48.297515,0,0,0
2011-02-16 11:32:49.374033,0,0,0 2011-02-16 11:32:50.509526,30.1,-85.0999983333,0 2011-02-16 11:32:51.612404,30.1,-85.0999983333,0 2011-02-16 11:32:52.727394,30.1,-85.0999983333,0 2011-02-16 11:32:53.838587,30.1,-85.0999983333,0 2011-02-16 11:32:54.977258,30.1,-85.0999983333,0

您可以从通知下拉屏幕轻松切换到 SL4A 控制台屏幕。您也可以通过按下菜单按钮并选择全部停止来终止脚本(参见图 7-13 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-13。*脚本监视器显示 trackmylocation.py 应用正在运行

WiFi 扫描仪

了解从您当前位置可用的 WiFi 接入点是一件好事。如果你用手机搜索可用的网络,大约需要三个步骤。SL4A 脚本只需一次点击就可以完成。步骤其实很简单。你要做的第一件事就是打开无线网络:

>>> droid.toggleWifiState(True)

如果您将参数True传递给函数toggleWifiState,它实际上会打开 WiFi。打开 WiFi 后,您可以开始扫描:

>>> droid.wifiStartScan() Result(id=0, result=False, error=None)

您需要给它一些时间来进行扫描,然后使用以下命令读取结果:

>>> scan = droid.wifiGetScanResults()[1]

这是一个酒店房间的扫描输出,它实际上只是一个 Python 字典:

>>> pprint.pprint(scan) [{u'bssid': u'00:1d:7e:33:ba:a4', u'capabilities': u'[WEP]', u'frequency': 2462, u'level': -83,
u'ssid': u'moes'}, {u'bssid': u'00:23:33:a4:08:80', u'capabilities': u'', u'frequency': 2412, u'level': -70, u'ssid': u'hhonors'}, {u'bssid': u'00:23:5e:d4:e8:90', u'capabilities': u'', u'frequency': 2462, u'level': -76, u'ssid': u'hhonors'}, {u'bssid': u'00:23:5e:1e:e8:40', u'capabilities': u'', u'frequency': 2462, u'level': -85, u'ssid': u'hhonors'}, {u'bssid': u'00:23:5e:d4:e3:f0', u'capabilities': u'', u'frequency': 2412, u'level': -89, u'ssid': u'hhonors'}, {u'bssid': u'00:02:6f:77:e8:c4', u'capabilities': u'', u'frequency': 2447, u'level': -92, u'ssid': u'Comfort'}, {u'bssid': u'00:02:6f:88:2b:52', u'capabilities': u'', u'frequency': 2412, u'level': -93, u'ssid': u'Comfort'}, {u'bssid': u'00:02:6f:85:b3:cf', u'capabilities': u'', u'frequency': 2462, u'level': -94, u'ssid': u'Comfort'}]

很容易从wifiGetScanResults获取输出并填充一个警告对话框。这将为您提供一种简单快捷的方式,只需点击一下鼠标即可搜索到 WiFi 接入点。下面是实现这一点的代码:

`import android
import time

def main():
global droid
droid = android.Android() # Wait until the scan finishes.
while not droid.wifiStartScan().result: time.sleep(0.25)

Build a dictionary of available networks.

networks = {}
while not networks:
for ap in droid.wifiGetScanResults().result:
networks[ap[‘bssid’]] = ap.copy()

droid.dialogCreateAlert(‘Access Points’)
droid.dialogSetItems([‘%(ssid)s, %(level)s, %(capabilities)s’ % ap
for ap in networks.values()])
droid.dialogSetPositiveButtonText(‘OK’)
droid.dialogShow()

if name == ‘main’:
main()`

图 7-14 显示了当你在你的设备上运行这个脚本时你会得到什么。您可能只会看到广播其 ssid 或您所连接的接入点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-14。*wifi scanner 脚本输出

HTTP 服务器

Python 知道如何执行 HTTP,并提供了许多库函数来简化 HTTP 服务器的创建。您将要使用的库是SimpleHTTPServer。如果您要从桌面运行以下代码,它将从端口 8000 上的当前目录启动一个 web 服务器:

import SimpleHTTPServer SimpleHTTPServer.test()

我们可以为 Android 设备扩展这个主题,将工作目录设置为相机应用保存图片的位置,然后启动 HTTP 服务器。实际上,这将提供一种通过局域网从相机中检索照片的方法。代码如下:

import SimpleHTTPServer from os import chdir chdir('/sdcard/DCIM/100MEDIA') SimpleHTTPServer.test()

图 7-15 是在模拟器中运行这个脚本的截图。它还显示了按下硬件后退按钮的结果。SL4A 将在实际退出应用之前提示您,如图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-15。*模拟器中运行的 SimpleHTTPserver 脚本

现在你只需要知道你的设备的 IP 地址。WiFi facade 包含一个函数wifiGetConnectionInfo,它将返回与 WiFi 无线电相关联的当前 IP 地址。唯一的问题是它返回值是一个长整数。不要害怕,有一个 Python 库可以帮助你。您实际上必须导入两个库才能得到我们需要的东西。这里有一个简短的脚本,它将获得当前的 IP 地址,并使用一个makeToast弹出窗口显示出来。

`import android, socket, struct

droid = android.Android()

ipdec = droid.wifiGetConnectionInfo().result[‘ip_address’]

ipstr = socket.inet_ntoa(struct.pack(‘L’,ipdec))

droid.makeToast(ipstr)`

现在我们将把这段代码添加到我们的四行 web 服务器中,以显示设备的 IP 地址。下面是更新后的代码:

`import android, socket, SimpleHTTPServer, struct
from os import chdir

droid = android.Android()

ipdec = droid.wifiGetConnectionInfo().result[‘ip_address’]
ipstr = socket.inet_ntoa(struct.pack(‘L’,ipdec))

chdir(‘/sdcard/DCIM/100MEDIA’)

print “connect to %s” % ipstr
SimpleHTTPServer.test()`

图 7-16 显示了服务器运行时屏幕的样子。请注意,您还将在主窗口中获得所有活动的日志。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-16。*httpd 2 . py 脚本的状态屏幕

图 7-17 显示了我运行SimpleHTTPServer2代码时在浏览器中看到的屏幕截图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-17。*简单 HTTPServer 应用的例子

要将一张图片从您的设备传输到本地机器,您只需用鼠标右键单击并选择另存为。

查杀正在运行的 App

有几种方法可以终止正在运行的应用。最简单的方法是使用设备上的设置菜单,选择应用,然后管理应用。这将显示当前运行的应用列表,并应包含 SL4A 和 Python For Android 的条目。如果您选择 Python For Android,那么您应该会看到类似于图 7-18 的屏幕。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-18。【Android 版 Python 的应用信息

如果您按下强制停止按钮,将导致应用退出。第二个选项是切换到设备上的通知页面并选择 SL4A 服务,如图 7-19 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-19。*简单 HTTPServer 应用示例

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-20。 SL4A 脚本监视器屏幕

该屏幕提供了一个控制页面,允许您查看所有活动的 SL4A 脚本,包括它们已经运行了多长时间。按下“全部停止”按钮将强制退出当前运行的脚本。如果选择httpd2.py行,如图图 7-20 所示,将会切换到该脚本显示的屏幕。一旦到达那里,你可以按下硬件返回按钮并退出脚本(参见图 7-15 )。

URL 文件检索器

有时候,将文件从互联网下载到 Android 设备上的特定位置并不是一件容易的事情。实际上,你可能会启动一个类似音乐播放器的程序,这取决于你的设备如何处理网页上的嵌入链接,而你真正想要的是下载一份拷贝。这可能会非常令人沮丧;也就是说,除非你用 Python 写个脚本替你做。

这个简单的脚本依靠 Python 的标准库模块之一urllib来完成大部分工作。它还使用 Android 剪贴板来获取下载链接。默认情况下,所有文件都下载到sdcard上的下载目录中。在下载开始之前,您有机会重命名文件。图 7-21 显示文件名对话框。如果您选择取消按钮而不是确定,您将简单地退出脚本。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-21。*文件名对话框

这个小脚本中另一段运行良好的代码是进度条。urlretrieve函数接受一个强制参数和三个可选参数。您必须传入一个要检索的 URL,所以这是唯一需要的参数。第二个参数是指定存储下载文件的文件名。第三个参数实际上是一个函数引用,Python 文档称之为 reporthook。一旦建立了网络连接,并在每个块读取完成后,将调用此函数。reporthook 将传递给它三个参数,包括到目前为止传输的块数、单个块的大小以及要传输的文件的总大小。这对于进度条来说是完美的,并且会使它易于实现:

`import android
import urllib
import os

downloads = ‘/sdcard/download/’ def _reporthook(numblocks, blocksize, filesize, url=None):
base = os.path.basename(url)
try:
percent = min((numblocksblocksize100)/filesize, 100)
except:
percent = 100
if numblocks != 0:
droid.dialogSetMaxProgress(filesize)
droid.dialogSetCurrentProgress(numblocks * blocksize)

def main():
global droid
droid = android.Android()

url = droid.getClipboard().result
if url is None: return

dst = droid.dialogGetInput(‘Filename’, ‘Save file as:’, os.path.basename(url)).result
droid.dialogCreateHorizontalProgress(‘Downloading…’, ‘Saving %s from web.’ % dst)
droid.dialogShow()
urllib.urlretrieve(url, downloads + dst,
lambda nb, bs, fs, url=url: _reporthook(nb,bs,fs,url))
droid.dialogDismiss()

droid.dialogCreateAlert(‘Operation Finished’,
‘%s has been saved to %s.’ % (url, downloads + dst))
droid.dialogSetPositiveButtonText(‘OK’)
droid.dialogShow()

if name == ‘main’:
main()`

当进度条第一次初始化时,它的最大值是 100。如果你仔细观察程序启动的时候,你可能会看到。一旦它开始了实际的下载,它将拥有用正确的数字填充进度条所需的信息。图 7-22 显示了下载文件过程中进度条的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-22。*URL 下载器脚本进度对话框

Python FTP 服务器

将文件下载到你的 Android 设备的相反过程显然是上传到它上面。同样的推理也适用于上传工具。有时,您没有线缆,但您希望能够将文件从笔记本电脑或其他计算机上传到您的设备。虽然对于这个问题有许多选择,但是最明显的解决方案是实现一个 FTP 服务器。这将给你上传和下载文件的能力,如果你愿意的话。

实现 FTP 服务器不像 HTTP 那么容易,至少不使用 Python 标准库。在谷歌上快速搜索 Python FTP 服务器,第一个结果就是pyftpdlib。这是一个纯 Python 库,实现了一个成熟的 FTP 服务器。如果您浏览该项目的源代码,您会看到一个名为ftpserver.py的大文件。这是这个项目中您需要的唯一文件。将其下载到您的主机,然后使用 ADB 命令将其推送到您的设备,如下所示:

adb push ftpserver.py /sdcard/sl4a/scripts/

这将把服务器代码放在与 SL4A Python 脚本的其余部分相同的目录中。它将允许 Python import命令加载库,而没有任何路径问题:

`import android, socket, struct
import ftpserver

droid = android.Android()

authorizer = ftpserver.DummyAuthorizer()
authorizer.add_anonymous(‘/sdcard/downloads’)
authorizer.add_user(‘user’, ‘password’, ‘/sdcard/sl4a/scripts’, perm=‘elradfmw’)
handler = ftpserver.FTPHandler
handler.authorizer = authorizer
ipdec = droid.wifiGetConnectionInfo().result[‘ip_address’]
ipstr = socket.inet_ntoa(struct.pack(‘L’,ipdec))
droid.makeToast(ipstr)
server = ftpserver.FTPServer((ipstr, 8080), handler)
server.serve_forever()`

一旦 FTP 服务器运行,您就可以用任何 FTP 客户端连接到它。FireFTP 是一个非常好的 Firefox 插件,它为您提供了一个双窗格显示(见图 7-23 )以方便主机(左边)和客户端(右边)之间的拖放文件操作。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-23。 FireFTP 连接安卓手机

默认情况下,FTP 使用 IP 端口 21,但是在这个例子中,我选择使用端口 8080。如果您使用这个例子,您需要配置您的 FTP 客户端来使用一个备用端口。在 Firefox FireFTP 插件中,这是使用编辑连接工具完成的。图 7-24 显示了该对话框的第一个选项卡。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-24。 FireFTP 账户管理器主页

要更改端口,您需要点击连接选项卡。这将弹出一个类似于图 7-25 中的对话框。要使用新端口,只需更改端口:文本框中的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-25。 FireFTP 连接配置页面

FireFTP 插件的伟大之处在于它是跨平台的,这意味着它可以在 Linux、Mac OS X 和 Windows 上工作。将它与我们的 FTP 服务器应用结合起来,您就有了一个无需线缆就可以在 Android 设备上来回移动文件的好方法。FTP 服务器应用将日志消息输出到 Python 标准输出屏幕。如果你想看到这些,你需要使用终端图标从 SL4A 启动应用(见图 7-26 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-26。*Python FTP 服务器的日志画面

图 7-27 显示了您将在 Python 终端窗口中看到的 FTP 日志。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 7-27。*Python FTP 服务器的日志屏幕

总结

本章向您展示了使用 Python 语言编写简短脚本的基础知识,以及一些实际可用的脚本,您可以根据需要进行修改。Python 是一种很好的语言,它提供了丰富的库来完成几乎所有的计算任务。

以下是本章的要点:

  • Python 库:快速的谷歌搜索会出现大量的 Python 库,但是你需要知道它们是否是用纯 Python 写的(纯 Python 意味着所有的代码都是用 Python 语言写的)。有些库只是一些基于 C 的库的包装,它们提供了真实库的编译二进制文件。除非它们已经被交叉编译以在 Arm 架构上运行,否则它们不会工作。
  • 使用电子邮件发送材料:使用 SL4A 发送电子邮件是小菜一碟。本章向您展示了如何创建邮件并通过 Gmail 发送。您一定会想到这种工具的许多其他用途。现在,您已经有了将一个组件组装起来以满足特定需求的构件。
  • 位置,位置,位置:反正房地产就是这么说的。每个 Android 设备都有能力从多个来源提供位置信息。有些比其他的更精确,但是你并不总是需要极度的精确。你还需要记住一些琐碎的事情(例如,当你在室内时,GPS 无法工作)。
  • 网络是一件大事:把你的小 Android 设备变成网络服务器总共需要两行 Python 代码。这一章仅仅触及了您在这里可以做的事情的表面。只要确保当你在公共场合打开文件浏览器时,不要完全忽视安全性。

八、基于 Python 对话框的图形用户界面

本章将介绍用 SL4A 构建基于对话框的图形用户界面(GUI)的可用选项。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 本章将讨论使用 Android 对话框 API 函数来构建呈现真实世界用户界面的应用。这些领域的一些背景会有所帮助,但不是绝对必要的。

SL4A 有两种基本的用户交互方式。首先,Android API 调用了股票对话框,比如 alerts。这是向用户呈现信息和接收反馈的最简单、最直接的方式。我们将在这里介绍这种方法。第二种方法使用 HTML 和 JavaScript 来构建用户界面(UI ),然后在幕后使用 Python 来处理任何额外的处理。我将在下一章向你展示如何用 HTML 做一个 UI。

用户界面基础

SL4A 包括一个 UI facade,用于通过 Android API 访问基本的对话框元素。使用这些元素构建脚本非常简单。本质上,您所要做的就是为按钮、项目和标题设置想要显示的文本,然后调用showDialog。您可以使用dialogGetResponse调用获得用户操作的结果。

当编写用户界面时,预料到意想不到的事情是很重要的。您的脚本需要能够处理用户可能执行的每个操作,包括什么也不做。我将从设置几个对话框的复习开始,然后研究一个示例应用。如果您需要做的只是向用户显示一条简短的消息,那么您可以使用makeToast API 函数。SL4A 帮助页面给出了一个简单的例子,也展示了getInput API 函数。代码如下所示:

`import android

droid = android.Android() name = droid.getInput(“Hello!”, “What is your name?”)
print name # name is a named tuple
droid.makeToast(“Hello, %s” % name.result)`

这将首先显示一个类似图 8-1 的输入对话框。它有一个标题(你好!)和一个提示(你叫什么名字?).默认情况下,getInput功能为用户输入和 Ok 按钮显示一个单行文本框。需要注意的是,SL4A 的最新版本已经弃用了getInput功能,取而代之的是dialogGetInput

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-1。*带有标题、提示、输入框和确定按钮的输入对话框

当用户按下 Ok 按钮时,getInput将返回一个结果对象作为命名元组。如果您使用 Python IDLE 工具在模拟器或真实设备上远程运行代码,您将能够看到打印名称代码的结果。在这一章中我将会用到 IDLE,因为当你需要单步调试代码或者只是查看不同 API 调用的结果时,它会让事情变得更简单。在这种情况下,结果将如下所示:

Result(id=0, result=u'Kentucky Rose', error=None)

为了便于跟踪,每个结果都被分配了一个惟一的 ID,这里我们得到了id=0。元组的第二个元素是result,包含用户在文本框中输入的文本字符串。每个结果还包含一个 error 元素,向调用者提供可能遇到的任何错误情况的反馈。在这个例子中,我们看到了error=None,意思是没有错误。当按下 Ok 时,你应该会看到一个类似于图 8-2 的弹出信息显示一小段时间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-2。*显示用户输入的 makeToast 对话框

我们将用来创建对话框的主要 API 调用是dialogCreateAlert。它接受两个可选参数来设置对话框标题和一个在对话框中显示的消息字符串。消息字符串是向用户描述您希望他们在对话框中做什么的好地方。图 8-3 显示了以下代码的结果:

droid.dialogCreateAlert('Settings Dialog','Chose any number of items and then press OK') droid.dialogShow()外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-3。*带有标题和消息的基本警报对话框

警报对话框可以比作台式机上的弹出对话框。它允许你创建三个按钮,提供三个不同的返回值。要创建按钮,您必须使用任何一个dialogSetNegativeButtonTextdialogSetNeutralButtonTextdialogSetPositiveButtonText API 调用来启用按钮并设置要显示的文本。下面的代码添加了两个按钮,分别用于一个肯定结果和一个否定结果:

droid.dialogSetPositiveButtonText('Done') droid.dialogSetNegativeButtonText('Cancel')

图 8-4 显示了我们的对话框在文本中添加了按钮后的样子。基本警报对话框只显示文本,不返回任何内容。这对于交流信息是有用的,但是一旦你显示一个警告对话框,你必须通过调用dialogDismiss来消除它,或者用户必须按下返回硬件按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-4。*有两个按钮的警告对话框

要找出用户按下了哪个按钮,您必须像这样调用dialogGetResponse:

`>>> response = droid.dialogGetResponse()

response
Result(id=10, result={u’canceled’: True}, error=None)`

如果你需要用户给你某种类型的文本输入,你会想要使用dialogGetInput函数。下面是提示一条消息并将结果设置为等于变量ans的代码:

ans = droid.dialogGetInput("Message Title","Message Text","Default").result外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-5。*获取输入对话框

如果用户按下 Cancel 按钮,您将看到如下所示的空返回:

Result(id=0, result=None, error=None)

当您调用dialogGetResponse时,它将返回用户完成的最后一个动作。因此,如果你有一个多选对话框显示,用户简单地按下取消,你的结果将是标签为取消按钮的输出。例如,以下是在 Python 空闲控制台的设置对话框中多次传递的结果:

`>>> droid.dialogSetItems([‘one’,‘two’,‘three’,‘four’,‘five’,‘six’,‘seven’,‘eight’,‘nine’])
Result(id=16, result=None, error=None)

droid.dialogShow()
Result(id=17, result=None, error=None)
droid.dialogGetResponse()
Result(id=18, result={u’canceled’: True, u’which’: u’positive’}, error=None)
droid.dialogShow()
Result(id=19, result=None, error=None)
droid.dialogGetResponse()
Result(id=20, result={u’item’: 2}, error=None)
droid.dialogShow()
Result(id=21, result=None, error=None)`

第一行创建一个警告对话框,其中有九个元素添加到上一个示例中定义的两个按钮上。图 8-6 显示了结果对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-6。*带有项目列表和两个按钮的警告对话框

让我们通过使用Result id来识别每一个来看看所有这些。第一个响应id=18返回一个 Python 字典,其中包含两个名为'canceled'和’which'的元素。它们的值分别是'True''positive'。这告诉我们,用户取消了操作,而没有通过按下肯定按钮来选择任何项目,在我们的例子中标记为'Done'

下一个结果id=20是用户选择列表中一个项目的例子。注意,结果仅仅是{u'item': 2}。同样,我们有一个字典作为结果返回,但是这次它只有一个元素:'item''item'的值是 2,翻译成文本行'three'。这是因为 Python 使用从零开始的索引。您看不到按钮的任何值,因为当用户选择列表中的一项时,对话框将关闭。对于这种类型的用户交互,用户只需要一个按钮就可以取消所有操作。

使用 Python 空闲控制台检查对话框按钮响应的最后一个示例如下:

>>> droid.dialogGetResponse() Result(id=22, result={u'canceled': True, u'which': u'neutral'}, error=None)

Result id=22是用户按下取消按钮时所期望看到的。在我们的例子中,我们定义了积极和中立按钮,因此字典值。我们的设置脚本需要的最后一个 UI 对话框是dialogCreateInput。在下一节中,我们将使用它来提示用户何时需要输入。

书名搜索

现在让我们看一下前面的例子,展示如何显示一个条目列表,并使用dialogCreateInput函数调用来提示书名,然后在显示结果之前进行 Google 图书搜索。图 8-7 显示了我们的对话框,提示输入搜索词。一旦我们有了我们的术语,我们就把它发送给 Google search API,然后用返回的标题列表填充警告对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-7。*谷歌图书搜索输入对话框

执行搜索的代码如下所示:

`service = gdata.books.service.BookService()
service.ClientLogin(email, pw)

titles = []
for bookname in service.get_library():
titles.append(bookname.dc_title[0].text)

droid.dialogCreateAlert()
droid.dialogSetItems(list)
droid.dialogShow()`

该代码将显示一个类似于图 8-8 中所示的对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-8。*带有结果列表的警告对话框

现在我们已经解决了这个问题,我们将快速地看一下其他一些您可能在某个时候想要使用的 UI 元素。

便捷对话框

对话框外观包括许多方便的功能,如日期选择器。图 8-9 显示了以下代码的结果:

droid.dialogCreateDatePicker(2011) droid.dialogShow()

此函数的参数是可选的,但是如果使用的话,应该是表示年、月和日的整数。如果没有输入,对话框将默认为 1970 年 1 月 1 日的初始日期。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-9。*日期选择器对话框初始化为 2011 年 1 月 1 日

要读取用户的响应,您需要如下调用dialogGetResponse:

>>> droid.dialogGetResponse() Result(id=27, result={u'year': 2011, u'day': 6, u'which': u'positive', u'month': 3},![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) error=None)

如果使用 Python 的 IDLE 工具,可以很容易地检查这些函数返回的结果。将结果分配给变量date l ets,您可以轻松地寻址不同的命名值:

`>>> date = droid.dialogGetResponse().result

date
{u’year’: 2011, u’day’: 7, u’which’: u’positive’, u’month’: 3}
date[“year”]
2011
date[“month”]
3
date[“day”]
7`

另一个助手对话框是createTimePicker功能。就像createDatePicker一样,您可以提供输入来设置显示的初始时间。下面的代码将产生如图图 8-10 所示的对话框。

`>>> droid.dialogCreateTimePicker()
Result(id=9, result=None, error=None)

droid.dialogShow()
Result(id=10, result=None, error=None)`

请注意,您会立即从dialogCreateTimePicker中获得一个 result 对象,因为它让您知道您成功地设置了一个时间选择器。现在您可以继续使用dialogShow调用来实际显示对话框。这里我选择不使用预设时间,所以对话框显示 12:00 AM 或午夜。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-10。*显示默认时间的时间选择器对话框

如果您知道要显示的内容,日期和时间选取器对话框都接受起始值。对于时间选择器,第一个输入应该是表示小时的整数,第二个输入应该是表示要显示的分钟的整数。第三个可选输入参数是用于设置 24 小时模式的布尔值,默认情况下设置为 false。如果这个参数作为true传入,您将看到 24 小时以内的值。

通常,您会希望在键入密码的每个字符后都回显一个星号。该对话框将立即显示,无需调用showDialog。它将回显每个键入的字符,以便用户可以得到一些关于按下了什么字符的反馈。键入一个新字符会用星号覆盖前一个字符。图 8-11 显示了最后一个字符仍然显示的对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-11。*显示最后输入字符的获取密码对话框

您必须调用dialogGetResponse来返回输入的密码或确定按下了哪个按钮。下面是使用 IDLE 时的情况:

`>>> droid.dialogGetPassword()
Result(id=5, result=u’Password’, error=None)

droid.dialogGetResponse()
Result(id=6, result={u’which’: u’positive’, u’value’: u’Password’}, error=None)
droid.dialogGetPassword()
Result(id=7, result=None, error=None)
droid.dialogGetResponse()
Result(id=8, result={u’which’: u’negative’, u’value’: u’'}, error=None)`

在第一行(id=5),输入密码并按下 Ok 按钮。你可以看到它返回结果'Password'。使用对dialogGetResponse的调用显示按下了肯定按钮,并且返回了值'Password'。对于下一个打给dialogGetPassword的电话,用户只需按下取消按钮。这里的结果(id=7)显示'None'。使用对dialogGetResponse的另一个调用显示按下了负按钮,在本例中为 Cancel,并且返回了一个空值。

进度对话框

让用户知道你的应用正在做什么总是一个好主意。如果您需要做一些需要几秒钟以上的处理,您应该考虑使用进度对话框。SL4A 为水平进度条和微调对话框提供了 API facade。使用进度对话框的最大挑战是确定如何度量进度,然后显示进度。

在前一章中,我使用了一个水平进度条来显示文件下载进度。在这种情况下,文件的大小用于确定进度。调用dialogCreateHorizontalProgress的时候不用指定什么。这只会显示一个进度对话框,范围从 0 到 100。图 8-12 显示了你将从代码中得到什么:

droid.dialogCreateHorizontalProgress() droid.dialogShow()外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-12。*带有默认选项的水平进度条

对话框显示后,您可以使用dialogSetMaxProgress更改显示的最大值。您必须使用dialogSetCurrentProgress来更新您的申请进度。下面的代码将进度条更新到 50%,假设最大进度已经设置为4096:

droid.dialogSetCurrentProgress(2048)

图 8-13 显示了这段代码将会产生的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-13。*水平进度条在 50%

还有一些时候,您只需要让用户知道应用正在进行某种类型的处理。这将调用“微调器进度”对话框。下面是你需要做的一切来启动一个:

droid.dialogCreateSpinnerProgress("Spinner Test","Spinner Message")

图 8-14 显示了你将会得到什么。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-14。*微调对话框

两个进度对话框都需要调用dialogDismiss来关闭。

模态与非模态对话框

在构建用户界面时,对话框行为实际上只有两种选择。模式对话框或窗口通常是另一个进程或窗口的子进程,这意味着它有一个父进程或更高级别的窗口可以返回。处理将等待或阻塞,直到用户与新对话框交互。在警告对话框的例子中,它本质上是模态的,这意味着它不会关闭,直到你做一些事情。

这里有一些代码来演示我所说的内容:

`# Demonstrate use of modal dialog. Process location events while

waiting for user input.

import android
droid=android.Android()
droid.dialogCreateAlert(“I like swords.”,“Do you like swords?”)
droid.dialogSetPositiveButtonText(“Yes”)
droid.dialogSetNegativeButtonText(“No”)
droid.dialogShow()
droid.startLocating()
while True: # Wait for events for up to 10 seconds.
response=droid.eventWait(10000).result
if responseNone: # No events to process. exit.
break
if response[“name”]
“dialog”: # When you get a dialog event, exit loop
break
print response # Probably a location event.

Have fallen out of loop. Close the dialog

droid.dialogDismiss()
if responseNone:
print “Timed out.”
else:
rdialog=response[“data”] # dialog response is stored in data.
if rdialog.has_key(“which”):
result=rdialog[“which”]
if result
"positive":
print “Yay! I like swords too!”
elif result==“negative”:
print “Oh. How sad.”
elif rdialog.has_key(“canceled”): # Yes, I know it’s mispelled.
print “You can’t even make up your mind?”
else:
print “Unknown response=”,response
print droid.stopLocating()
print “Done”`

这段代码将显示一个类似于图 8-15 中所示的对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-15。*用于演示模态显示的警告对话框

如果用户什么都不做,对话框将超时并被关闭。使用 IDLE,您可以看到结果:

Timed out. Result(id=7, result=None, error=None) Done

如果用户按下 Yes 按钮,您应该会看到以下结果:

Yay! I like swords too! Result(id=7, result=None, error=None) Done

按下“否”按钮将显示以下信息:

Oh. How sad. Result(id=7, result=None, error=None) Done

如果用户碰巧按下硬件返回按钮来取消应用,您将在空闲的主窗口中看到以下内容:

You can't even make up your mind? Result(id=7, result=None, error=None) Done

重要的是使用事件来实现超时特性。普通模式对话框不会超时,除非用户取消整个应用。这里的eventWait函数调用用于等待按下其中一个按钮或 1000 毫秒或 1 秒,然后继续处理。事件不起作用,除非已经启动了一个活动,例如startLocating。这将生成位置事件,必须对其进行过滤,以便只查找感兴趣的事件。这是使用如下所示的代码行完成的:

if response["name"]=="dialog": # When you get a dialog event, exit loop

这一行允许脚本关闭'dialog'事件并继续处理,同时忽略基于位置的事件。

选项菜单

许多 Android 应用利用选项菜单来允许用户设置应用行为的偏好或任何选项。SL4A 提供了一种使用addOptionsMenuItem调用创建选项菜单项的方法。

`import android
droid=android.Android()

droid.addOptionsMenuItem(“Silly”,“silly”,None,“star_on”)
droid.addOptionsMenuItem(“Sensible”,“sensible”,“I bet.”,“star_off”)
droid.addOptionsMenuItem(“Off”,“off”,None,“ic_menu_revert”)

print “Hit menu to see extra options.”
print “Will timeout in 10 seconds if you hit nothing.”

droid.webViewShow(‘file://sdcard/sl4a/scripts/blank.html’)

while True: # Wait for events from the menu.
response=droid.eventWait(10000).result
if responseNone:
break
print response
if response[“name”]
“off”:
break
print “And done.”`

需要调用webViewShow来显示除了添加到选项菜单中的系统屏幕之外的内容。您不允许更改正常的系统选项,因此您需要运行某种应用来修改选项菜单。图 8-16 显示了如果你按下硬件菜单按钮,运行前一个脚本的结果应该是什么样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-16。*示例选项菜单

如果用户按下 Sensible 按钮,您将得到以下结果:

{u'data': u'I bet.', u'name': u'sensible', u'time': 1301074971174000L}

请注意,这个结果实际上是一个事件的输出,包括命名的项目datanametime。然后,您需要根据用户按下的菜单选项执行额外的处理。

用 dialogCreateAlert 列出文件

有时你需要得到一个文件列表,并在一个对话框中显示它们,如图 8-17 中的所示。这里有一个简短的脚本可以做到这一点:

`import android, os

droid=android.Android()

list = []
for dirname, dirnames, filenames in os.walk(‘/sdcard/sl4a/scripts’):
for filename in filenames:
list.append(filename)

droid.dialogCreateAlert(‘/sdcard/sl4a/scripts’)
droid.dialogSetItems(list)
droid.dialogShow()
file = droid.dialogGetResponse().result
print(list[file])`外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-17。*简单的文件选择器对话框

这段代码稍有不同的地方是增加了深入子目录的功能。如果您只是简单地测试用户选择的项目是否实际上是一个目录,这是非常容易的。如果是,您只需清除项目并用新子目录的内容填充它。新的对话框看起来类似于图 8-18 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-18。*显示目录的简单文件选择器对话框

请注意,目录前面有一个*号,当前路径显示在对话框的标题字符串中。现在我们有了一个全功能的文件选择器对话框,可以在后面的例子中使用。下面是我添加的代码行,用于检查所选项目是否是目录:

if os.path.isdir(start + '\\' + list[file['item']][1:]):

这里我们使用isdir函数来检查文件的完整路径名,并且我们使用 Python 的切片符号来获取星号后面的所有内容。

作为 Python 对象的对话框

从用户界面处理处理或决策的一种方法是在 Python 中定义一个函数,以帮助清理代码并提供一个更加模块化的逻辑流。下面是我们的 UI 列表测试代码的样子:

`# Test of Lists
import android,sys
droid=android.Android()

#Choose which list type you want. def getlist():
droid.dialogCreateAlert(“List Types”)
droid.dialogSetItems([“Items”,“Single”,“Multi”])
droid.dialogShow()
result=droid.dialogGetResponse().result if result.has_key(“item”):
return result[“item”]
else:
return -1

#Choose List
listtype=getlist()
if listtype<0:
print “No item chosen”
sys.exit()

options=[“Red”,“White”,“Blue”,“Charcoal”]
droid.dialogCreateAlert(“Colors”)
if listtype0:
droid.dialogSetItems(options)
elif listtype
1:
droid.dialogSetSingleChoiceItems(options)
elif listtype==2:
droid.dialogSetMultiChoiceItems(options)
droid.dialogSetPositiveButtonText(“OK”)
droid.dialogSetNegativeButtonText(“Cancel”)
droid.dialogShow()
result=droid.dialogGetResponse().result

droid.dialogDismiss() # In most modes this is not needed.

if result==None:
print “Time out”
elif result.has_key(“item”):
item=result[“item”];
print “Chosen item=”,item,“=”,options[item]
else:
print “Result=”,result
print “Selected=”,droid.dialogGetSelectedItems().result
print “Done”`外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-19。*带有选项列表的初始对话框

下一个出现的对话框取决于用户的选择。如果用户选择项目,他们会看到一个类似图 8-20 的对话框。该对话框提供了四个选项和两个按钮。如果用户选择其中一项,如白色,代码将返回以下内容:

Chosen item= 1 = White Done外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-20。从选择项中显示对话框

从初始对话框中选择 Single 将显示一个类似于图 8-21 所示的对话框。此对话框演示了使用单选按钮提示用户进行单次输入的一种略有不同的方式。在这个对话框中,你需要一个 Ok 按钮在选择一个特定的项目后关闭这个对话框。选择 Cancel 按钮将为用户提供不做任何选择就退出对话框的选项。从该对话框中选择白色的结果如下:

Result= {u'which': u'positive'} Selected= [1] Done

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示如果您远程运行任何需要引用文件系统的应用,您需要知道它将在您的本地文件系统上寻找,而不是在设备或模拟器上。通过在主驱动器上创建一个名为/sdcard的目录,然后添加一个名为sl4a的子目录,然后在 sl4a 下添加另一个名为scripts的子目录,可以镜像设备或仿真器上的相同结构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-21。*对话框显示从选择单个

最后一个选项是多选,允许用户从列表中选择多个项目。假设用户选择了图 8-22 中所示的选项,你会得到如下结果:

Result= {u'which': u'positive'} Selected= [0, 1, 2] Done外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-22。从选择多个中显示对话框

如果用户选择取消按钮,您会看到一个结果,表明选择了否定响应按钮,如:

Result= {u'which': u'negative'} Selected= [] Done

??" 播客应用

我发现我的 Android 手机令人讨厌的一点是音乐播放器。如果你有大量的音乐文件,而你只是想听一些类似播客的东西,这可能是个问题。这个问题的部分原因是媒体播放器在你的 MP3 文件中使用 ID3 标签来按照专辑、艺术家甚至是单首歌曲对你的音乐进行分类。如果您想要播放的文件碰巧没有正确设置 ID3 标签,您可能无法使用媒体播放器界面找到它,除非它显示在Unknown标签下。

SL4A 拥有我们构建一个简单的小应用所需的一切,可以显示目录的内容,然后将选定的文件发送到媒体播放器。我们首先要使用的是我们之前使用的目录浏览器代码。图 8-23 显示了运行从/sdcard/sl4a目录开始的代码会看到什么。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-23。*文件选择器对话框

填充警告对话框的所有工作都是通过一个名为show_dir的函数来完成的。代码做的第一件事就是使用 Python os.path.exists()函数来确定base_dir中指定的路径是否存在。如果没有,它将使用os.makedirs(base_dir)创建子目录。检查之后,代码将使用 Python 的os.listdir()函数来检索base_dir目录中所有目录和文件的列表。下面是这段代码的样子:

`nodes = os.listdir(path)

Make a way to go up a level.

if path != base_dir: nodes.insert(0, ‘…’)

droid.dialogCreateAlert(os.path.basename(path).title())
droid.dialogSetItems(nodes)
droid.dialogShow()

Get the selected file or directory.

result = droid.dialogGetResponse().result`

在这个关头,有几件事需要指出。因为我们将使用递归的编程结构,当用户在文件系统中移动时,重复显示一个新的警告对话框,所以需要测试。这确保了用户不会去base_path目录和任何子目录之外的任何地方。如果用户当前不在顶层,它也可以使用'nodes.insert(0,'..')'向上一级目录(见图 8-23 中的第一个条目)。对droid.dialogGetResponse()的调用将被阻塞或等待,直到用户选择一个目录或文件,或者使用硬件按钮退出程序。

当用户做一些事情时,结果中应该有数据来确定应用下一步做什么。如果用户选择一个目录,应用将加载该目录的内容,并创建一个新的警告对话框。如果用户选择一个文件,它将检查以确保它是一个mp3文件,然后使用这行代码启动媒体播放器:

droid.startActivity('android.intent.action.VIEW', 'file://' + target_path, 'audio/mp3')

如果你碰巧在你的设备上安装了多个应用来播放媒体,你会得到另一个对话框提示选择使用哪一个。您还可以选择将该选择作为文件类型mp3的默认选择。当用户选择一个目录时,应用使用递归重新加载下一个目录,代码如下:

if os.path.isdir(target_path): show_dir(target_path)

如果用户打开了子目录,另一个选项是向上一级。对此进行测试的代码行如下:

if target == '..': target_path = os.path.dirname(path)

因此,如果用户选择带有'..'的行,代码会将target_path设置为字符串path。调用show_dir函数时,path 的初始值被设置为string base_dir,如下所示:

def show_dir(path=base_dir):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意递归是创建受控用户界面的一种很好的方式——这意味着相同的代码会被执行多次,直到用户以你想要的方式退出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-24。【podplayer.py 的. mp3 文件列表

我们的 Podplayer 应用的最终用户界面如图 8-24 所示。完整的代码如下所示:

`import android, os, time

droid = android.Android() # Specify our root podcasts directory and make sure it exists.
base_dir = ‘/sdcard/sl4a/scripts/podcasts’
if not os.path.exists(base_dir): os.makedirs(base_dir)

def show_dir(path=base_dir): “”“Shows the contents of a directory in a list view.”“”

The files & directories under “path”.

nodes = os.listdir(path)

Make a way to go up a level.

if path != base_dir: nodes.insert(0, ‘…’)

droid.dialogCreateAlert(os.path.basename(path).title())
droid.dialogSetItems(nodes)
droid.dialogShow() # Get the selected file or directory.
result = droid.dialogGetResponse().result
droid.dialogDismiss()
if ‘item’ not in result:
return
target = nodes[result[‘item’]]
target_path = os.path.join(path, target)

if target == ‘…’: target_path = os.path.dirname(path)

If a directory, show its contents.

if os.path.isdir(target_path): show_dir(target_path)

If an MP3, play it.

elif os.path.splitext(target)[1].lower() == ‘.mp3’:
droid.startActivity(‘android.intent.action.VIEW’,
‘file://’ + target_path, ‘audio/mp3’)

If not, inform the user.

else:
droid.dialogCreateAlert(‘Invalid File’,
‘Only .mp3 files are currently supported!’)
droid.dialogSetPositiveButtonText(‘Ok’)
droid.dialogShow()
droid.dialogGetResponse()
show_dir(path)

if name == ‘main’: show_dir()`

在这个例子中,还有一些事情值得讨论,Python 让这些事情变得非常简单。测试特定的文件扩展名只需要一行代码,如下所示:

os.path.splitext(target)[1].lower()

此外,您应该注意到这个脚本中使用的另外两个os.path方法,os.path.joinos.path.isdiros.path library模块有很多方法可以让处理文件系统和文件变得轻而易举。

构建 mysettings 应用

设置脚本背后的基本思想是构建一个小的工具,该程序将创建适合特定电话设置组合的脚本。我们将显示一个对话框,其中有不同的设置可供选择,然后让用户选择保存它们的文件名。所有用户需要做的是创建一个链接到设置文件夹,然后将有一种方式来配置手机的两个触摸。我们将使用多项选择,以便用户能够选择不同的功能来启用。

我们将使用标准 Python 代码写出我们的最终脚本,并将其保存到我们的目录中。对于这个例子,我们将简单地使用一个硬编码的目录,但是您可以给用户一个选项,而不需要太多额外的编码。最大的问题是确保选择的目录在 sd 卡上,并且用户有权限写入。我们将使用/sdcard/sl4a/mysettings作为我们的目标目录。脚本运行时要做的第一件事是检查该目录是否存在,如果不存在,它将创建它。这总共需要三行 Python 代码:

import os if not os.path.exists('/sdcard/sl4a/settings'): os.mkdir('/sdcard/sl4a/settings')

执行完这段代码后,我们确定有一个目录可以用来保存我们的设置脚本。用户可以创建该目录的快捷方式,以便单击访问不同的设置脚本。我们的脚本没有做的另一件事是检查任何不一致的地方。把飞行模式打开,把 Wifi 或者蓝牙设置成开,真的没什么意义。飞行模式设置背后的意图是允许脚本关闭飞行模式并打开其他模式。大多数手机都有一个相当简单的方法来打开飞行模式,所以我们不会试图重现这一点。图 8-25 显示了我们最终设置对话框的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-25。*带有项目列表和两个按钮的警告对话框

当您单击 Done 按钮时,您将看到一个新的对话框,允许您命名脚本。要退出此对话框,您必须按“完成”或“取消”。如果您愿意,也可以使用硬件后退按钮退出应用。

图 8-26 显示了提示用户输入文件名的最终对话框的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 8-26。*提醒对话框提示输入保存设置脚本的名称

我们需要查看的最后一段代码处理多重选择对话框的返回。首先,您必须检查用户按下了哪个按钮。如果取消按钮被按下,我们想退出脚本,不做任何事情。这需要调用dialogGetResponse来确定哪个按钮被按下了。实际读取响应需要调用dialogGetSelectedItems。这将返回所选项目的列表。下面是获得用户响应的一段代码:

`response = droid.dialogGetResponse().result

if ‘canceled’ in response: droid.exit()
else:
response = droid.dialogGetSelectedItems().result`

一旦我们有了选定的值,我们就可以选择将什么写到我们的最终脚本中。为了做到这一点,我们将使用一些 Python 技巧,从包含与我们需要完成的积极和消极行动相对应的条目的列表中提取特定的一行。toggles列表由元组组成,每个元组包含两个字符串,因此该列表总共有五个元素。下面是我们的toggles列表:

toggles = [ ('droid.toggleAirplaneMode(True)', 'droid.toggleAirplaneMode(False)'), ('droid.toggleBluetoothState(True)', 'droid.toggleBluetoothState(False)'), ('droid.toggleRingerSilentMode(True)', 'droid.toggleRingerSilentMode(False)'), ('droid.setScreenBrightness(0)', 'droid.setScreenBrightness(255)'), ('droid.toggleWifiState(True)', 'droid.toggleWifiState(False)'), ]

现在我们可以使用enumerate函数,它接受一个 iterable 在这种情况下,list toggles得到一个包含每个条目的索引和条目本身的元组列表,分别为itoggle

for i, toggle in enumerate(toggles): if i in response: script += toggles[i][0] else: script += toggles[i][1] script += '\n'

下面是用户选择“确定”时文件的样子:

`import android

droid = android.Android() droid.toggleAirplaneMode(False)
droid.toggleBluetoothState(True)
droid.toggleRingerSilentMode(False)
droid.setScreenBrightness(255)
droid.toggleWifiState(True)

droid.dialogCreateAlert(‘Profile Enabled’, ‘The “default” profile has been activated.’) droid.dialogSetPositiveButtonText(‘OK’)
droid.dialogShow()`

差不多就是这样。脚本中的前两行是导入android模块和实例化我们的droid对象所必需的。

`import android, os

script_dir = ‘/sdcard/sl4a/scripts/settings/’

if not os.path.exists(script_dir):
os.makedir(script_dir)

droid = android.Android()
toggles = [
(‘droid.toggleAirplaneMode(True)’, ‘droid.toggleAirplaneMode(False)’),
(‘droid.toggleBluetoothState(True)’, ‘droid.toggleBluetoothState(False)’),
(‘droid.toggleRingerSilentMode(True)’, ‘droid.toggleRingerSilentMode(False)’),
(‘droid.setScreenBrightness(0)’, ‘droid.setScreenBrightness(255)’),
(‘droid.toggleWifiState(True)’, ‘droid.toggleWifiState(False)’),
]

droid.dialogCreateAlert(‘Settings Dialog’, ‘Chose any number of items and then press OK’)
droid.dialogSetPositiveButtonText(‘Done’)
droid.dialogSetNegativeButtonText(‘Cancel’)

droid.dialogSetMultiChoiceItems([‘Airplane Mode’,
‘Bluetooth On’,
‘Ringer Silent’,
‘Screen Off’,
‘Wifi On’])

droid.dialogShow()
response = droid.dialogGetResponse().result

if ‘canceled’ in response:
droid.exit()
else:
response = droid.dialogGetSelectedItems().result droid.dialogDismiss()
res = droid.dialogGetInput(‘Script Name’,
‘Enter a name for the profile script.’,
‘default’).result

script = ‘’'import android

droid = android.Android()
‘’’

for i, toggle in enumerate(toggles):
if i in response:
script += toggles[i][0]
else:
script += toggles[i][1]
script += ‘\n’

script += ‘’’
droid.dialogCreateAlert(‘Profile Enabled’, ‘The “%s” profile has been activated.’)
droid.dialogSetPositiveButtonText(‘OK’)
droid.dialogShow()‘’’ % res

f = open(script_dir + res + ‘.py’, ‘w’)
f.write(script)
f.close()`

总结

本章向您展示了通过可用对话框与用户交互的基础知识。

这是本章的要点列表:

  • 对话框基础知识:SL4A 对话框外观提供了许多标准的方式来呈现信息和获取用户输入。了解如何以及何时使用每一个将有助于您构建既有用又易于使用的脚本。
  • 理解结果:理解不同输入对话框的预期结果以及如何处理用户可能选择的每个按钮是很重要的。
  • 模态和非模态对话框:当你继续执行前需要用户输入时,使用模态对话框。
  • 使用来自 Python 标准库的模块:这些模块对于处理日常文件系统事务非常有用。
  • 良好的编程实践:良好的编程实践是无可替代的,包括处理用户可能采取的所有行动。
  • 使用多个对话框:你可以将多个对话框类型链接在一起,构建一个更复杂的 UI,通过createAlertDialog进行提示,并使用dialogSetItems函数调用递归输出一个列表框。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值