Android-SL4A-脚本编程高级教程-全-

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

原文:Pro Android Scripting with SL4A

协议:CC BY-NC-SA 4.0

零、前言

众所周知,传统的计算模式正在经历一场彻底的变革。处理能力不断增强的智能手机的普及只会加速这一过程。平板设备作为智能手机平台的延伸,已经得到了更广泛的采用,而以前缩小通用计算机尺寸的尝试失败了。虽然从用户的角度来看,最流行的移动设备的操作系统可能有所不同,但它与桌面系统的共同点比你想象的要多。

谷歌的 Android 平台在过去的一年里有了巨大的增长,并正在挑战苹果 iOS 的市场份额。苹果在应用方面的广泛领先优势一直在稳步缩小,尽管在质量方面还没有定论。在很大程度上,构建这些应用仅限于 iOS 的 Objective C 和 Android 的 Java。如果考虑到 MonoTouch 和 MonoDroid 项目,还有一些其他选择,但仅此而已。

移动设备可能永远不会完全取代传统电脑,尽管活动的分工将继续向你接触最多的那一方倾斜。这本书是关于以编写简单程序或脚本来完成特定任务的形式,将你从台式计算机中获得的一些灵活性带给你。我知道我一路走来学到了很多,我真诚地希望通过阅读这本书,你也能收集到一些东西。

一、简介

这本书是关于主要使用 Python 语言和一点点 JavaScript 为 Android 平台编写真实世界的应用。虽然 Java 没有任何问题,但当你需要做的只是打开或关闭 Android 设备上的一些设置时,它真的是大材小用了。Android 脚本层(SL4A)项目就是为了满足这一特定需求而启动的。这本书将向您介绍 SL4A,并让您能够以您从未想过的方式自动化您的 Android 设备。

为什么选择 SL4A?

你对这本书的第一个问题可能是,“为什么我要用 SL4A 而不是 Java?”这个问题有几个答案。一是不是每个人都是 Java 的粉丝。Java 语言对一些人来说太重了,而且不完全是开源的。它还需要使用编辑/编译/运行设计循环,这对于简单的应用来说可能是乏味的。一个同样合理的答案是“我想使用 X”,其中 X 可以是任何数量的流行语言。

Google 提供了一个全面的软件开发工具包(SDK ),专门针对 Java 开发人员,Android 市场上的大多数应用可能都是用 Java 编写的。我将在第三章中讲述 Android SDK,并在整本书中使用它附带的一些工具。

images 注意 SL4A 目前支持 Beanshell、JRuby、Lua、Perl、PHP、Python、Rhino。

SL4A 的真正目标是那些寻找一种方法来编写简单的脚本以在使用任何支持的语言的 Android 设备上自动化任务的人,包括通过 Beanshell 的 Java。它提供了一个交互式控制台,您可以在其中键入一行代码并立即看到结果。在许多情况下,它甚至可以重用您为桌面环境编写的代码。底线是 SL4A 使得用 Java 以外的语言为基于 Android 的设备编写代码和以一种更具交互性的方式编写代码成为可能。

安卓的世界

2005 年,谷歌收购安卓公司,大举进军移动操作系统领域。它在如此短的时间内取得了如此大的进步,真是令人惊讶。Android 社区非常庞大,已经产生了大量的会议、书籍和支持材料,这些都可以在互联网上轻松获得。

这是定义几个术语的好时机,你将在本书的其余部分看到这些术语。Android 应用通常被打包成.apk文件。这些实际上只是包含应用所需的一切的.zip文件。事实上,如果你将一个.apk文件重命名为.zip,,你可以用任何存档工具打开它并检查其内容。

大多数 Android 设备来自制造商,其系统文件受到保护,以防止任何无意或恶意的操作。Android 操作系统(OS)本质上是以 Linux 为核心的,它提供了许多与任何 Linux 桌面相同的功能。有很多方法可以解锁系统区域,并提供对 Android 设备上整个文件系统的根用户访问。这个过程被恰当地称为你的设备,一旦完成,该设备被描述为。SL4A 不需要根设备,但是如果您选择了这个路径,它将在一个根设备上工作。

Android 应用剖析

Android 基于 Linux 操作系统(在撰写本文时,Linux 内核的版本为 2.6)。Linux 提供了所有的核心管道,如设备驱动程序、内存和进程管理、网络堆栈和安全性。内核还在硬件和应用之间增加了一个抽象层。用一个解剖学的比喻来说,您可能会认为 Linux 是机器人身体的骨骼、肌肉和器官。

Android 堆栈的下一层是 Dalvik 虚拟机(DVM)。这一部分提供了核心的 Java 语言支持和 Java 编程语言的大部分功能。DVM 是大脑,大部分处理工作都在其中进行。每个 Android 应用都在 DVM 的私有实例中运行在自己的进程空间中。应用框架提供了 Android 应用所需的所有必要组件。来自 Google Android 文档:

“开发人员可以完全访问核心应用使用的相同框架 API。应用架构旨在简化组件的重用。任何应用都可以发布其功能,然后任何其他应用都可以利用这些功能(受框架实施的安全约束的约束)。同样的机制允许用户更换组件。

所有应用的基础是一组服务和系统,包括:

  • 一组丰富且可扩展的视图,可用于构建应用,包括列表、网格、文本框、按钮,甚至嵌入式网络浏览器
  • 使应用能够从其他应用(如联系人)访问数据或共享自己数据的内容供应器
  • 资源管理器,提供对本地化字符串、图形和布局文件等非代码资源的访问
  • 一个通知管理器,允许所有应用在状态栏显示自定义提醒
  • 一个活动管理器,管理应用的生命周期,并提供一个通用的导航 back stack "1

所有 Android 应用都基于三个核心组件:活动、服务和接收者。这些核心组件通过名为 intents 的消息激活。SL4A 通过其 API facade 为您提供了许多核心 Android 功能,因此了解一些基础知识是个不错的主意。第三章和第五章详细看 Android SDK 和 Android 应用编程接口(API),具体的我留到后面再说。现在,我将向您介绍活动和意图,因为它们将被广泛使用。

活动

Android 文档将活动定义为“一个应用组件,提供一个用户可以与之交互的屏幕,以便做一些事情,例如拨打电话、拍照、发送电子邮件或查看地图。每个活动都有一个窗口,可以在其中绘制用户界面。窗口通常会充满整个屏幕,但也可能会比屏幕小,并浮动在其他窗口的顶部。

Android 应用由一个或多个松散耦合在一起的活动组成。每个应用通常都有一个“主”活动,该活动可以依次启动其他活动来完成不同的功能。

意图

来自 Google 文档:“意图是一个简单的消息对象,它表示做某事的意图。例如,如果您的应用想要显示一个网页,它通过创建一个意图实例并将其交给系统来表达其查看 URI 的意图。系统会找到其他一些知道如何处理这个意图的代码(在本例中是浏览器)并运行它。意图还可以用于在系统范围内广播有趣的事件(如通知)。”

可以使用一个 intent 和startActivity来启动一个活动broadcastIntent将其发送给任何感兴趣的BroadcastReceiver组件,startService(Intent)bindService(Intent, ServiceConnection, int)与后台服务进行通信。意图使用您必须以参数形式提供的主要和次要属性。

有两个主要属性:

  • 动作:将要执行的一般动作,如VIEW_ACTIONEDIT_ACTIONMAIN_ACTION
  • 数据:要操作的数据,比如联系人数据库中的人员记录,表示为统一资源标识符(URI)

1

二级属性有四种类型:

  • 类别:给出关于要执行的动作的附加信息。例如,LAUNCHER_CATEGORY意味着它应该作为顶层应用出现在启动器中,而ALTERNATIVE_CATEGORY意味着它应该包含在用户可以对一段数据执行的替代操作列表中。
  • 类型:指定意图数据的显式类型(MIME 类型)。通常,类型是从数据本身推断出来的。通过设置此属性,可以禁用该计算并强制显式类型。
  • component: 指定一个组件类的显式名称,用于 intent。通常,这是通过查看意图中的其他信息(动作、数据/类型和类别)并将其与可以处理它的组件进行匹配来确定的。如果设置了该属性,则不执行任何计算,并且该组件完全按原样使用。通过指定该属性,所有其他意图属性都成为可选属性。
  • extras: 任何附加信息的捆绑包。这可用于向组件提供扩展信息。例如,如果我们有一个发送电子邮件的动作,我们也可以在这里包含额外的数据片段,以提供主题、正文等等。

SL4A 历史

SL4A 于 2009 年 6 月在 Google 开源博客上首次公布,最初命名为 Android 脚本环境(ASE)。主要是通过达蒙·科勒的努力,这个项目才得以实现。随着项目的不断成熟,其他人也做出了贡献。在撰写本文时,最新的版本是 r4,尽管您也可以在 SL4A 网站([code.google.com/p/android-scripting](http://code.google.com/p/android-scripting))上找到实验版本。

SL4A 架构

在最底层,SL4A 本质上是一个脚本主机,这意味着作为一个应用,它托管不同的解释器,每个解释器处理一种特定的语言。如果您要浏览 SL4A 源代码库,您会看到每种语言的源代码树的副本。使用 Android 原生开发工具包(NDK)为 ARM 架构进行交叉编译,并在 SL4A 启动特定解释器时作为库加载。此时,脚本将被逐行解释。

SL4A 的基本架构类似于您在分布式计算环境中看到的。图 1-1 以图示的形式显示了当你启动 SL4A 然后运行一个脚本(在本例中为hello.py))时的执行流程。每个 SL4A 脚本都必须导入或获取一个外部文件,比如 Python 的android.py,它将定义许多与 Android API 通信所需的代理函数。

SL4A 和底层 Android 操作系统之间的实际通信使用远程过程调用(RPC)机制和 JavaScript 对象符号(JSON)。您通常会在分布式体系结构中发现 RPC,在这种体系结构中,信息在客户机和服务器之间传递。以 SL4A 为例,服务器是 Android OS,客户端是 SL4A 脚本。这在 SL4A 和 Android 操作系统之间增加了一层隔离,以防止任何恶意脚本做任何有害的事情。

安全性是一个问题,也是 SL4A 使用 RPC 机制的原因之一。SL4A wiki 是这样描述的:

" RPC 身份验证: SL4A 通过要求所有脚本由相应的 RPC 服务器进行身份验证来强制执行每个脚本的安全沙箱。为了使认证成功,脚本必须向相应的服务器发送正确的握手秘密。这是通过完成的

  1. 读取AP_HANDSHAKE环境变量。
  2. 用值AP_HANDSHAKE作为参数调用 RPC 方法_authenticate

_authenticate方法必须是第一个 RPC 调用,并且应该在 Android 库初始化期间发生。例如,参见 Rhino 的或 Python 的 Android 模块”。 2

images

图 1-1。 SL4A 执行流程图


2

SL4A 概念

在我们实际使用 SL4A 之前,有许多概念需要介绍。在很高的层次上,SL4A 提供了许多协同工作的功能部件。每种受支持的语言都有一个解释器,可以在 Android 平台上运行。伴随解释器的是 Android API 的抽象层。这个抽象层以每种语言所期望的形式提供了一个调用接口。解释器和原生 Android API 之间的实际通信使用进程间通信(IPC)作为额外的保护层。最后,它支持在设备上交互测试脚本的环境。

虽然图 1-1 显示 Python 是解释器,但是这个概念对于所有支持的语言都是一样的。每个解释器在自己的进程中执行语言,直到进行 API 调用。然后使用 RPC 机制将其传递给 Android 操作系统。解释器和 Android API 之间的所有通信通常都使用 JSON 来传递信息。

JavaScript 对象符号(JSON)

SL4A 大量使用 JSON 来传递信息。如果您以前从未见过 JSON,您可能想访问一下[www.json.org](http://www.json.org)网站。最简单的 JSON 只是一种定义数据结构或对象的方式,就像在程序环境中一样。在大多数情况下,您会看到 JSON 结构以一系列名称/值对的形式出现。名称部分将始终是一个字符串,而值可以是任何 JavaScript 对象。

在 SL4A 中,您会发现许多 API 调用使用 JSON 返回信息。幸运的是,在创建、解析和使用 JSON 时有多种选择。Python 将 JSON 视为一等公民,拥有完整的工具库,可以在 JSON 和其他本地 Python 类型之间来回转换。Python 标准库pprint模块是一种以可读性更好的格式显示 JSON 响应内容的便捷方式。

Python 标准库包括一个 JSON 模块,其中有许多方法可以使处理 JSON 变得更加容易。因为 JSON 对象可以包含几乎任何类型的数据,所以必须使用编码器和解码器将原生 Python 数据类型转换成 JSON 对象。这是通过json.JSONEncoderjson.JSONDecoder方法完成的。当您将一个 JSON 对象从一个地方移动到另一个地方时,您必须序列化然后反序列化该对象。这需要用json.load()json.loads()函数进行解码,用json.dump()json.dumps()进行编码。

有大量的 web 服务已经采用 JSON 作为实现 API 的标准方法。这里有一个来自雅虎的图片:

{ "Image": { "Width":800, "Height":600, "Title":"View from 15th Floor", "Thumbnail": { "Url":"http:\/\/scd.mm-b1.yimg.com\/image\/481989943", "Height": 125, "Width": "100" }, "IDs":[ 116, 943, 234, 38793 ] } }

事件

Android 操作系统使用事件队列作为处理特定硬件生成的动作的手段,例如当用户按下某个硬件键时。其他可能性包括任何设备传感器,例如加速度计、GPS 接收器、光传感器、磁力计和触摸屏。在检索信息之前,必须明确打开每个传感器。

SL4A API facade 提供了许多 API 调用,这些调用将启动导致事件的某种类型的操作。其中包括以下内容:

  • startLocating()
  • startSensing()
  • startTrackingPhoneState()
  • startTrackingSignalStrengths()

这些调用中的每一个都将开始收集某种类型的数据,并生成诸如“位置”事件或“电话”事件之类的事件。任何受支持的语言都可以注册一个事件处理程序来处理每个事件。startLocating()调用有两个参数,允许您指定更新之间的最小距离和最小时间。

语言

SL4A 带来了许多语言选择。在撰写本书时,这些选择包括 Beanshell、Lua、JRuby Perl、PHP、Python 和 Rhino(在下面的章节中给出了这些版本)。如果愿意,您还可以编写或重用 shell 脚本。毫无疑问,所有这些语言中最流行的是 Python。到目前为止,对其他语言的支持还没有接近 Python 的水平,但是如果你愿意的话,也可以使用它们。

Beanshell 2.0b4

Beanshell 是一种有趣的语言,因为它基本上是用 Java 解释的。这有点回避了这样一个问题:当你可以使用 Android SDK 编写原生 Java 时,你为什么想要一个解释的 Java 呢?Beanshell 解释器确实提供了一个编写和测试代码的交互式工具。它肯定不会是最快的代码,但是您可能会发现它对于测试代码片段很有用,而不需要经历整个编译/部署/测试周期。

检查android.bsh文件显示了用于设置 JSON 数据结构的代码,这些数据结构用于向 Android 操作系统传递信息和从 Android 操作系统接收信息。下面是基本调用函数的样子:

call(String method, JSONArray params) { JSONObject request = new JSONObject(); request.put("id", id); request.put("method", method); request.put("params", params); out.write(request.toString() + "\n"); out.flush(); String data = in.readLine();

if (data == null) { return null; } return new JSONObject(data); }

这里有一个简单的hello_world.bsh脚本:

source("/sdcard/com.googlecode.bshforandroid/extras/bsh/android.bsh"); droid = Android(); droid.call("makeToast", "Hello, Android!");

月球 5.1.4

Lua.org 将 Lua 描述为“一种扩展编程语言,旨在用数据描述工具支持通用过程编程”。 3 术语扩展编程语言的意思是 Lua 意在通过脚本来扩展现有程序。这很符合 SL4A 的理念。

从语法角度来看,Lua 有点像 Python,因为它不使用花括号来包装代码块,也不需要分号来结束语句,尽管如果您愿意,您可以这样做。在函数定义的情况下,Lua 使用保留字function开始代码块,然后使用保留字end标记结束。

Lua 拥有现代语言中大多数标准的数据类型,还包括表的概念。在 Lua 中,表是动态创建的对象,可以像传统语言中的指针一样进行操作。表必须在使用前显式创建。表也可以引用其他表,使它们非常适合递归数据类型。操纵表格的通用函数列表包括table.concat.insert.maxn.remove.sort

下面是 Lua 网站上的一小段 Lua 代码,它创建了一个循环链表:

list = {} -- creates an empty table current = list i = 0 while i < 10 do current.value = i current.next = {} current = current.next i = i+1 end current.value = i acurrent.next = list


3

下面是实现 RPC 调用函数的 Lua 代码:

function rpc(client, method, …) assert(method, 'method param is nil') local rpc = { ['id'] = id, ['method'] = method, params = arg } local request = json.encode(rpc) client:send(request .. '\n') id = id + 1 local response = client:receive('*l') local result = json.decode(response) if result.error ~= nil then print(result.error) end return result end

必须的 Lua hello world 脚本:

`require "android"

name = android.getInput("Hello!", "What is your name?") android.printDict(name) -- A convenience method for inspecting dicts (tables).
android.makeToast("Hello, " .. name.result)`

Lua wiki 提供了包含大量有用代码片段的样例代码链接。

Perl 5.10.1 版

如果不算 shell,Perl 可能是 SL4A 中最古老的语言。它可以追溯到 1987 年,已经被用于你能想到的几乎所有类型的计算应用中。使用 Perl 的最大优势是可以从中提取大量代码示例。编写hello_world.pl脚本看起来很像其他语言:

use Android; my $a = Android->new(); $a->makeToast("Hello, Android!");

下面是启动 SL4A 脚本所需的 Perl 代码:

`# Given a method and parameters, call the server with JSON,

and return the parsed the response JSON. If the server side

looks to be dead, close the connection and return undef.

sub do_rpc {
my self=shift;if(self->trace) {
show_trace(qq[do_rpc: $self: @_]);
}`

my $method = pop; my $request = to_json({ id => $self->{id}, method => $method, params => [ @_ ] }); if (defined $self->{conn}) { print { $self->{conn} } $request, "\n"; if ($self->trace) { show_trace(qq[client: sent: "$request"]); } $self->{id}++; my $response = readline($self->{conn}); chomp $response; if ($self->trace) { show_trace(qq[client: rcvd: "$response"]); } if (defined $response && length $response) { my $result = from_json($response); my $success = 0; my $error; if (defined $result) { if (ref $result eq 'HASH') { if (defined $result->{error}) { $error = to_json( { error => $result->{error} } ); } else { $success = 1; } } else { $error = "illegal JSON reply: $result"; } } unless ($success || defined $error) { $error = "unknown JSON error"; } if (defined $error) { printf STDERR "$0: client: error: %s\n", $error; } if ($Opt{trace}) { print STDERR Data::Dumper->Dump([$result], [qw(result)]); } return $result; } } $self->close; return; }

PHP 5.3.3

毫无疑问,PHP 是创建动态网页最成功的通用脚本语言之一。从最初的个人主页,缩写 PHP 现在代表 PHP:超文本预处理器。PHP 是一种免费的开源语言,几乎可以在所有主流操作系统上免费实现。

下面是通过 RPC 启动 SL4A 脚本所需的 PHP 代码:

`public function rpc($method, \(args) { \)data = array(
'id'=>this>id,method=>method,
'params'=>args);request = json_encode(data);request .= "\n";
sent=socketwrite(this->_socket, request,strlen(request));
response=socketread(this->_socket, 1024, PHP_NORMAL_READ) or die("Could notimages
read input\n");
this>id++;result = json_decode($response);

ret=array(id=>result->id,
'result' => result>result,error=>result->error
);
return $ret;
}`

PHP 版本的hello_world.php看起来是这样的:

<?php require_once("Android.php"); $droid = new Android(); $name = $droid->getInput("Hi!", "What is your name?"); $droid->makeToast('Hello, ' . $name['result']);

在安装 PHP 和基本的hello_world.php时,您会得到许多其他的示例脚本。

犀牛 1.7R2

Rhino 解释器为您提供了一种编写独立 JavaScript 代码的方法。JavaScript 实际上在 ECMA-262 下被标准化为ECMAScript。你可以从[www.ecma-international.org/publications/standards/Ecma-262.htm](http://www.ecma-international.org/publications/standards/Ecma-262.htm)下载标准。拥有一个 JavaScript 解释器的好处有很多。如果您计划使用 HTML 和 JavaScript 构建任何类型的定制用户界面,您可以构建 JavaScript 部分的原型,并使用 Rhino 解释器进行测试。

Rhino 的android.js文件在许多方面与其他语言的文件相似。RPC 调用定义如下所示:

this.rpc = function(method, args) { this.id += 1; var request = JSON.stringify({'id': this.id, 'method': method, 'params': args}); this.output.write(request + '\n'); this.output.flush(); var response = this.input.readLine(); return eval("(" + response + ")"); },

这里有一个简单的 Rhino hello_world.js脚本:

load("/sdcard/sl4a/extras/rhino/android.js"); var droid = new Android(); droid.makeToast("Hello, Android!");

JRuby 1.4

任何开源项目的潜在危险之一是被忽视。在撰写本文时,基于 SL4A r4 的 JRuby 解释器受到了忽视,甚至没有运行hello_world.rb脚本。无论如何,这个脚本看起来是这样的:

require "android" droid = Android.new droid.makeToast "Hello, Android!"

JRuby 解释器确实启动了,您可以用它来尝试一些基本的 JRuby 代码。下面是 Android 类在 Ruby 中的样子:

`class Android

def initialize()
@client = TCPSocket.new('localhost', AP_PORT)
@id = 0
end

def rpc(method, *args)
@id += 1
request = {'id' => @id, 'method' => method, 'params' => args}.to_json()
@client.puts request
response = @client.gets()
return JSON.parse(response)
end

def method_missing(method, *args)
rpc(method, *args)
end

end`

外壳

如果您是一个 shell 脚本向导,那么您将会对 SL4A 的 shell 解释器如鱼得水。它本质上与您在典型的 Linux 终端提示符下看到的 bash 脚本环境相同。你会发现所有熟悉的操作文件的命令,如cplsmkdirmv

Python

Python 有着广泛的用途和大量的追随者,尤其是在 Google 内部。事实上,它的追随者如此之多,以至于他们雇佣了这种语言的发明者吉多·范·罗苏姆。Python 已经存在很长时间了,并且有许多用这种语言编写的开源项目。就 SL4A 而言,它也是最令人感兴趣的,所以你会在论坛上找到比其他语言更多的例子和讨论。出于这个原因,我将花多一点时间来介绍这种语言,试图突出从 SL4A 的角度来看很重要的东西。

语言基础

了解 Python 语言不是这本书的绝对要求,但会有帮助。关于 Python,你需要知道的第一件事是一切都是对象。第二件事是空白在 Python 中是有意义的。我的意思是 Python 使用制表符或实际空格(ASCII 32)而不是花括号来控制代码执行(见图 1-2 )。第三,重要的是要记住 Python 是一种区分大小写的语言。

images

图 1-2。Python 中空格用法的例子

在讲授“计算机编程入门”课程时,Python 是一种很好的语言。标准 Python 的每个安装都带有命令行解释器,您可以在其中键入一行代码并立即看到结果。要启动解释器,只需在命令提示符(Windows)或终端窗口(Linux 和 Mac OS X)输入python。此时,您应该看到几行版本信息,后面是三箭头提示(>>>),让您知道您在 Python 解释器中,如下所示:

`C:\Users\paul>python
Python 2.6.6 (r266:84297, Aug 24 2010, 18:13:38) [MSC v.1500 64 bit (AMD64)] on
win32
Type "help", "copyright", "credits" or "license" for more information.

`

Python 使用了许多命名约定,如果您仔细研究 Python 代码,就会发现这些约定。第一个是双下划线,它在 Python 中用来“破坏”或更改名称,作为定义类内部使用的私有变量和方法的一种方式。您将看到这种符号用于“特殊”方法,如__init__(self)。如果一个类有特殊的__init__方法,每当该类的新实例化发生时,它就会被调用。

例如:

`>>> class Point:
def init(self, x, y):
self.x = x
self.y = y

xy = Point(1,2)
xy.x, xy.y
(1, 2)`

从例子中可以看出,self在 Python 中被用作保留字,指的是方法的第一个参数。它实际上是一个 Python 约定,实际上,对 Python 没有特殊的意义。然而,因为这是一个被广泛接受的约定,所以你应该坚持使用它以避免任何潜在的问题。从技术上讲,self是对类或函数本身的引用。一个类中的方法可以通过使用self参数的方法属性来调用同一个类中的其他方法。

Python 有一个简短的内置常量列表。你会遇到的主要有FalseTrueNoneFalseTrue属于bool类型,主要出现在逻辑测试中或者创建一个无限循环。

这种语言的新用户经常感到困惑的事情之一是数据类型的多样性。接下来的部分给出了使用 Python 和 SL4A 所需的关键数据类型的快速概述。

字典:需要唯一键的一组无序的键/值对

Python 字典直接映射到 JSON 数据结构。定义字典的语法使用花括号将条目括起来,并在键和值之间使用冒号。下面是一个简单的字典定义:

students = {'barney' : 1001, 'betty' : 1002, 'fred' : 1003, 'wilma' : 1004}

要引用条目,请使用键,如下所示:

students['barney'] = 999 students['betty'] = 1000

您还可以使用dict()构造函数来构建字典。当键是一个简单的字符串时,您可以使用传递给dict()的参数创建一个新的字典,如下所示:

students = dict(barney=1001, betty=1002, fred=1003, wilma=1004)

因为 Python 中的一切都是一个对象,所以您可以期望看到与 dictionary 对象相关联的方法。如您所料,有一些方法可以从字典中返回键和值。下面是这本students字典的样子:

`>>> students

students.keys()
['barney', 'betty', 'fred', 'wilma']
students.values()
[1001, 1002, 1003, 1004]`

方括号约定表示列表。对students.keys()语句求值会返回来自students字典的键列表。

列表:内置的 Python 序列,类似于其他语言中的数组

在 Python 中,序列被定义为“一个 iterable,它支持通过__getitem__()特殊方法使用整数索引进行有效的元素访问,并定义了一个返回序列长度的len()方法。”一个可迭代被定义为“一个能够一次返回一个成员的容器对象”Python 为迭代提供了直接的语言支持,因为它是编程中最常见的操作之一。列表对象提供了许多方法来简化它们的使用。来自 Python 文档:

  • list.append( x ) :在列表末尾增加一项;相当于a[len(a):] = [x]
  • list.extend( L ) :通过追加给定列表中的所有项目来扩展列表;相当于a[len(a):] = L
  • list.insert( I,x ) :在给定位置插入一个项目。第一个参数是要插入的元素的索引,所以a.insert(0, x)插入到列表的前面,a.insert(len(a), x)相当于a.append(x)
  • list.remove( x ) :删除列表中第一个值为 x 的项目。如果没有此项,则为错误。
  • list.pop(【I】):移除列表中给定位置的项目并返回。如果没有指定索引,a.pop()删除并返回列表中的最后一项。(方法签名中的 i 周围的方括号表示该参数是可选的,并不是说您应该在那个位置键入方括号。)您将在 Python 库参考中经常看到这种符号。
  • list.index( x ) :返回列表中第一项的索引,其值为 x 。如果没有此项,则为错误。
  • list.count( x ) :返回 x 在列表中出现的次数。
  • list.sort :对列表中的项目进行排序,就位。
  • list.reverse( x ) :反转列表中的元素,就位。
字符串:由 ASCII 或 Unicode 字符组成的不可变序列

字符串定义中的关键词是不可变,意思是创建后不可改变。这有助于许多操作的快速实现,也是 Python 能够以非常高效的方式处理字符串的原因。反斜杠()字符用于转义具有特殊含义的字符,包括反斜杠字符本身。如果在字符串前面加上小写或大写的“r”,则不需要反斜杠字符,因为每个字符都将被视为“原始”字符串。您可以使用单引号或双引号来定义字符串文字,如下所示:

name = "Paul Ferrill" initials = 'PF' directory = r'C:\users\paul\documents'

string 类有许多可用的方法。下面是来自 Python 文档的列表: 4

capitalize() center(width[, fillchar]) count(sub[, start[, end]]) decode( [encoding[, errors]]) encode( [encoding[,errors]]) endswith( suffix[, start[, end]]) expandtabs( [tabsize]) find( sub[, start[, end]]) index( sub[, start[, end]]) isalnum( ) isalpha( ) isdigit( ) islower( ) isspace( ) istitle( ) isupper( ) join( seq) ljust( width[, fillchar]) lower( ) lstrip( [chars]) partition( sep) replace( old, new[, count]) rfind( sub [,start [,end]]) rindex( sub[, start[, end]]) rjust( width[, fillchar]) rpartition( sep) rsplit( [sep [,maxsplit]]) rstrip( [chars]) split( [sep [,maxsplit]]) splitlines( [keepends]) startswith( prefix[, start[, end]]) strip( [chars]) swapcase( ) title( ) translate( table[, deletechars]) upper( ) zfill( width)


4

在 Python 2.6 中,内置的strunicode类通过str.format()方法提供了非常全面的格式化和替换功能。还有一个Formatter类,它有一组更广泛的方法,适合实现模板功能。Python 也使用带字符串的百分号进行格式化。以下是在打印语句中使用百分号的示例:

`>>> Pi = 3.141593

print "Pi = %10.2f" % Pi
Pi = 3.14
print "Long Pi = %10.6f" % Pi
Long Pi = 3.141593`

这是一个谈论在 Python 中使用的好地方。任何有多个条目的对象,比如一个字符串,都可以用一种叫做切片的符号来寻址。为了引用字符串s,中从jk的项目,使用s[j:k].向切片符号添加第三个项目包括一个步骤,以便s[j:k:l]引用从jk的项目,增量为l。从列表的开始到第 n 项,使用s[:n],从第 n 项到末尾使用s[n:]。例如:

`>>> mylist = [1,2,3,4,5,6,7,8,9]

mylist[:3]
[1, 2, 3]
mylist[3:]
[4, 5, 6, 7, 8, 9]`

注意 Python 使用从零开始的引用。因此,mylist 的第一个或第零个元素将等于 1。语法mylist[:3]说“返回 mylist 的所有元素,从开始到第四个元素,但不包括第四个元素。”负索引从列表末尾开始计数。

元组:一个不可变的列表

与字符串类型一样,元组是不可变的,这意味着一旦创建就不能更改。元组使用与列表相同的语法来定义,只是它们用圆括号而不是方括号括起来。下面是一个元组的例子:

`>>> Sentence = ("Today", "is", "the", "first", "day", "of", "the", "rest", "of", "your",images
"life.")

Sentence
('Today', 'is', 'the', 'first', 'day', 'of', 'the', 'rest', 'of', 'your', 'life.')
Sentence[0]
'Today'
Sentence[1:3]
('is', 'the')
Sentence[-1]
'life.'`

Python 标准库

Python 语言的最大优势之一是 Python 标准库,它包含了各种各样的例程,使您的编码生活更加简单。尽管本简介没有足够的篇幅来介绍整个库,但我将尝试指出一些将在后面章节中出现的关键函数。

Python 语言的所有文档都可以在[docs.python.org](http://docs.python.org)找到,包括旧版本。SL4A 使用 Python 2.6,所以您需要查看旧版本以获得正确的信息。对于 Python 标准库,您将希望从[docs.python.org/release/2.6.5/library/index.html](http://docs.python.org/release/2.6.5/library/index.html)开始。

Python 解释器有大量的内置函数,这些函数总是可用的。其中一个函数是dir(),它显示一个模块定义的所有名字。如果您在 Python 提示符下键入dir(),您将收到当前本地范围内的名称列表,如下所示:

`>>> dir()
['builtins', 'doc', 'name', 'package']

import sys
dir()
['builtins', 'doc', 'name', 'package', 'sys']
import os
dir()
['builtins', 'doc', 'name', 'package', 'os', 'sys']`

如果您向dir()传递一个参数,您将获得该对象所有属性的列表。Python 标准库中的许多模块都实现了一个名为__dir__()的方法,这个方法在被调用时实际上会产生属性列表。如果没有__dir__()方法,该函数将尝试使用对象的__dir__()属性来获取它需要的信息。

您可以在任何对象上使用dir()来检查模块的属性。以下是您在 android 对象上使用dir()时得到的结果:

`>>> import android

dir(android)
'Android', 'HANDSHAKE', 'HOST', 'PORT', 'Result', 'author', 'builtins', 'doc',![images
'file', 'name', 'package', 'collections', 'json', 'os', 'socket', 'sys']`

另一个例子是定义一个字符串来查看有哪些方法可用:

`>>> a = "Hello"

dir(a)
'add', 'class', 'contains', 'delattr', 'doc', 'eq', 'format',![images'ge', 'getattribute', 'getitem', 'getnewargs', 'getslice', 'gt',images
'hash', 'init', 'le', 'len', 'lt', 'mod', 'mul', 'ne',images
'new', 'reduce', 'reduce_ex', 'repr', 'rmod', 'rmul',images
'setattr', 'sizeof', 'str', 'subclasshook', '_formatter_field_name_split',images
'_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith',images
'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower',images
'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition',images
'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split',images
'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']`

有时候,您可能正在使用 Python 标准库,甚至是其他库,但您不知道返回变量的类型。为此,您可以像这样使用type()内置函数:

>>> type(a) <type 'str'>

数字转换是经常需要的函数之一,Python 在这方面做得很好。这里有几个例子:

`>>> bin(12345678)
'0b101111000110000101001110'

hex(12345678)
'0xbc614e'
oct(12345678)
'057060516'`

这些函数的结果是一个字符串。要将字符串转换成整数,使用int()函数如下:

>>> print int('0xbc614e',16) 12345678

Python 中的文件输入和输出使用非常简单的方法。要打开一个文件,使用open,如图 1-3 中的所示。

images

图 1-3。Python 文件打开的例子

支持的文件模式包括追加(' a ')、读取(' r ')和写入(' w ')。open返回一个类型为file的对象,包含所有选择的方法和属性。看起来是这样的:

`>>> infile = open(r'c:\users\paul\documents\dependents.txt')

type(infile)
<type 'file'>
dir(infile)
'class', 'delattr', 'doc', 'enter', 'exit', 'format',![images'getattribute', 'hash', 'init', 'iter', 'new', 'reduce',images
'reduce_ex', 'repr', 'setattr', 'sizeof', 'str', 'subclasshook',images
'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name',images
'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace',v
'tell', 'truncate', 'write', 'writelines', 'xreadlines']`

os模块为底层操作系统提供独立于平台的接口。如果你打算对文件名做些什么,你会想了解一下os模块。如果你导入操作系统,然后执行dir(os):,你会得到以下结果

>>> dir(os) 'F_OK', 'O_APPEND', 'O_BINARY', 'O_CREAT', 'O_EXCL', 'O_NOINHERIT', 'O_RANDOM', 'O_RDONLY',![images'O_RDWR', 'O_SEQUENTIAL', 'O_SHORT_LIVED', 'O_TEMPORARY', 'O_TEXT', 'O_TRUNC', 'O_WRONLY',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'P_DETACH', 'P_NOWAIT', 'P_NOWAITO', 'P_OVERLAY', 'P_WAIT', 'R_OK', 'SEEK_CUR', 'SEEK_END',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'SEEK_SET', 'TMP_MAX', 'UserDict', 'W_OK', 'X_OK', '_Environ', '__all__', '__builtins__',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) '__doc__', '__file__', '__name__', '__package__', '_copy_reg', '_execvpe', '_exists',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) '_exit', '_get_exports_list', '_make_stat_result', '_make_statvfs_result',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) '_pickle_stat_result', '_pickle_statvfs_result', 'abort', 'access', 'altsep', 'chdir',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'chmod', 'close', 'closerange', 'curdir', 'defpath', 'devnull', 'dup', 'dup2', 'environ',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'errno', 'error', 'execl', 'execle', 'execlp', 'execlpe', 'execv', 'execve', 'execvp',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'execvpe', 'extsep', 'fdopen', 'fstat', 'fsync', 'getcwd', 'getcwdu', 'getenv', 'getpid',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'isatty', 'linesep', 'listdir', 'lseek', 'lstat', 'makedirs', 'mkdir', 'name', 'open',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'pardir', 'path', 'pathsep', 'pipe', 'popen', 'popen2', 'popen3', 'popen4', 'putenv',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'read', 'remove', 'removedirs', 'rename', 'renames', 'rmdir', 'sep', 'spawnl', 'spawnle',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'spawnv', 'spawnve', 'startfile', 'stat', 'stat_float_times', 'stat_result',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'statvfs_result', 'strerror', 'sys', 'system', 'tempnam', 'times', 'tmpfile', 'tmpnam',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'umask', 'unlink', 'unsetenv', 'urandom', 'utime', 'waitpid', 'walk', 'write']

如您所见,有相当多的方法可用。处理文件和目录的另一个方便的模块是glob,您可以使用它根据通配符获得特定目录中的文件列表,如下所示:

>>> import glob for file in glob.glob("*.jpg"): print file

这将打印当前工作目录中所有.jpg文件的列表。命中列表上的下一个模块是datetime。如果你需要处理日期或时间,你将需要datetime。以下是使用该模块的几个示例:

`>>> print datetime.date.today()
2011-04-26

print datetime.datetime.now()
2011-04-26 14:35:25.045000
print datetime.date.weekday(datetime.datetime.now())
1`

文件工具列表的最后一项是shutil。该模块提供了许多文件工具,如shutil.copyshutil.copytreeshutil.move,shutil.rmtree。如果您导入shutil并使用dir()查看这些方法,您会得到以下结果:

`>>> import shutil

dir(shutil)
'Error', 'all', 'builtins', 'doc', 'file', 'name', 'package',![images'_basename', '_samefile', 'abspath', 'copy', 'copy2', 'copyfile', 'copyfileobj',images
'copymode', 'copystat', 'copytree', 'destinsrc', 'fnmatch', 'ignore_patterns', 'move',images
'os', 'rmtree', 'stat', 'sys']`

处理存储在逗号分隔值(CSV)文件中的数据是另一项常见任务。Python 2.6 包含了用于这种任务的csv模块。下面是一个简单的脚本,它使用csv模块简单地打印出文件中的所有行:

import csv reader = csv.reader(open("some.csv", "rb")) for row in reader: print row

您也可以用类似的方式写入 CSV 文件:

import csv writer = csv.writer(open("some.csv", "wb")) writer.writerows(someiterable)

此示例显示了如何将数据解析为列表中的单独项目:

`>>> import csv

for row in csv.reader(['one,two,three']):
print row

['one', 'two', 'three']`

如果不知道文件中有多少列,可以使用string.count方法,如下所示:

`>>> import string

string.count('one,two,three,four,five',',')
4`

我们要看的来自csv模块的最后一个方法是DictReader.在大多数情况下,您应该知道 CSV 文件中包含哪些字段。如果确实如此,您可以使用 DictReader 函数将文件读入字典。下面是一个包含姓名、地址和电话号码的示例文本文件:

John Doe|Anycity|ST|12345|(800) 555-1212 Jane Doe|Anycity|ST|12345|(800) 555-1234 Fred Flinstone|Bedrock|ZZ|98765|(800) 555-4321 Wilma Flinstone|Bedrock|ZZ|98765|(800) 555-4321 Bambam Flinston|City|ST|12345|(800) 555-4321 Barney Rubble|Bedrock|ZZ|98765|(800) 555-1111

我们的示例代码还需要一个模块:itertools。这个模块提供了创建 Python 使用的高效迭代器的函数,带有关键字for。读取文件并打印结果的代码如下所示:

`import itertools
import csv

HeaderFields = ["Name", "City", "State", "Zip", "PhoneNum"]

infile = open("testdata.txt")

contacts = csv.DictReader(infile, HeaderFields, delimiter="|")

for header in itertools.izip(contacts):
print "Header (%d fields): %s" % (len(header), header)`

最后,输出如下:

Header (1 fields): ({'City': 'Anycity', 'State': 'ST', 'PhoneNum': '(800) 555-1212', 'Name':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'John Doe', 'Zip': '12345'},) Header (1 fields): ({'City': 'Anycity', 'State': 'ST', 'PhoneNum': '(800) 555-1234', 'Name':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'Jane Doe', 'Zip': '12345'},) Header (1 fields): ({'City': 'Bedrock', 'State': 'ZZ', 'PhoneNum': '(800) 555-4321', 'Name':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'Fred Flinstone', 'Zip': '98765'},) Header (1 fields): ({'City': 'Bedrock', 'State': 'ZZ', 'PhoneNum': '(800) 555-4321', 'Name':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'Wilma Flinstone', 'Zip': '98765'},) Header (1 fields): ({'City': 'City', 'State': 'ST', 'PhoneNum': '(800) 555-4321', 'Name':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'Bambam Flinston', 'Zip': '12345'},) Header (1 fields): ({'City': 'Bedrock', 'State': 'ZZ', 'PhoneNum': '(800) 555-1111', 'Name':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'Barney Rubble', 'Zip': '98765'},) Header (1 fields): ({'City': 'Bedrock', 'State': 'ZZ', 'PhoneNum': '(800) 555-1111', 'Name':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'Betty Rubble', 'Zip': '98765'},)

weekday方法返回周一的0,周二的1,依此类推。您以后可能需要的一项功能是将系统时间戳值转换为人类可读的字符串。下面是在后面章节中使用的一段代码,它将 SMS 消息的时间戳转换成一个字符串:

b = '' for m in SMSmsgs: millis = int(message['date'])/1000 strtime = datetime.datetime.fromtimestamp(millis) b += strtime.strftime("%m/%d/%y %H:%M:%S") + ',' + m['address'] + ',' + m['body'] + '\n'

为移动设备编写代码将不可避免地涉及到以某种方式与网站进行通信。Python 标准库有两个模块来帮助完成这项任务:urlliburllib2。尽管这两个模块提供了相似的功能,但它们以不同的方式实现。当您导入两者并检查它们的方法时,您会得到以下结果:

`>>> import urllib

dir(urllib)
'ContentTooShortError', 'FancyURLopener', 'MAXFTPCACHE', 'URLopener', 'all',![images'builtins', 'doc', 'file', 'name', 'package', 'version',images
'_ftperrors', '_have_ssl', '_hextochr', '_hostprog', '_is_unicode', '_localhost',images
'_noheaders', '_nportprog', '_passwdprog', '_portprog', '_queryprog', '_safemaps',images
'_tagprog', '_thishost', '_typeprog', '_urlopener', '_userprog', '_valueprog', 'addbase',images
'addclosehook', 'addinfo', 'addinfourl', 'always_safe', 'basejoin', 'ftpcache',images
'ftperrors', 'ftpwrapper', 'getproxies', 'getproxies_environment', 'getproxies_registry',images
'localhost', 'main', 'noheaders', 'os', 'pathname2url', 'proxy_bypass',images
'proxy_bypass_environment', 'proxy_bypass_registry', 'quote', 'quote_plus', 'reporthook',images
'socket', 'splitattr', 'splithost', 'splitnport', 'splitpasswd', 'splitport', 'splitquery',images
'splittag', 'splittype', 'splituser', 'splitvalue', 'ssl', 'string', 'sys', 'test',images
'test1', 'thishost', 'time', 'toBytes', 'unquote', 'unquote_plus', 'unwrap',images
'url2pathname', 'urlcleanup', 'urlencode', 'urlopen', 'urlretrieve', 'warnings']
import urllib2
dir(urllib2)`

'AbstractBasicAuthHandler', 'AbstractDigestAuthHandler', 'AbstractHTTPHandler',![images'BaseHandler', 'CacheFTPHandler', 'FTPHandler', 'FileHandler', 'HTTPBasicAuthHandler',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'HTTPCookieProcessor', 'HTTPDefaultErrorHandler', 'HTTPDigestAuthHandler', 'HTTPError',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'HTTPErrorProcessor', 'HTTPHandler', 'HTTPPasswordMgr', 'HTTPPasswordMgrWithDefaultRealm',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'HTTPRedirectHandler', 'HTTPSHandler', 'OpenerDirector', 'ProxyBasicAuthHandler',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'ProxyDigestAuthHandler', 'ProxyHandler', 'Request', 'StringIO', 'URLError',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'UnknownHandler', '__builtins__', '__doc__', '__file__', '__name__', '__package__',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) '__version__', '_cut_port_re', '_opener', '_parse_proxy', 'addinfourl', 'base64', 'bisect',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'build_opener', 'ftpwrapper', 'getproxies', 'hashlib', 'httplib', 'install_opener',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'localhost', 'mimetools', 'os', 'parse_http_list', 'parse_keqv_list', 'posixpath',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'proxy_bypass', 'quote', 'random', 'randombytes', 're', 'request_host', 'socket',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'splitattr', 'splithost', 'splitpasswd', 'splitport', 'splittype', 'splituser',![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'splitvalue', 'sys', 'time', 'unquote', 'unwrap', 'url2pathname', 'urlopen', 'urlparse']

如果你只是想下载一个 URL 的内容,就像你在台式机上右击并另存为一样,你可以使用urllib.urlretrieve。在urllib中有许多构建或解码 URL 的辅助方法,包括pathname2urlurl2pathnameurlencodequoteunquote。下面是urllib的一段代码:

`import urllib

class OpenMyURL(urllib.FancyURLopener):

read a URL with HTTP authentication

def setpasswd(self, user, passwd):
self.__user = user
self.__passwd = passwd

def prompt_user_passwd(self, host, realm):
return self.__user, self.__passwd

urlopener = OpenMyURL()
urlopener.setpasswd("user", "password")

f = urlopener.open("http://www.aprivatesite.com")
print f.read()`

如果你需要通过 FTP 下载文件,你会想要使用urllib2。这是我在stackoverflow.com上找到的一个例子:

`>>> files = urllib2.urlopen('ftp://ftp2.census.gov/geo/tiger/TIGER2008/01_ALABAMA/')images
.read().splitlines()

for l in files[:4]: print l

drwxrwsr-x 2 0 4009 4096 Nov 26 2008 01001_Autauga_County
drwxrwsr-x 2 0 4009 4096 Nov 26 2008 01003_Baldwin_County
drwxrwsr-x 2 0 4009 4096 Nov 26 2008 01005_Barbour_County
drwxrwsr-x 2 0 4009 4096 Nov 26 2008 01007_Bibb_County`

为了实现一个基本的 HTTP 服务器,有一个简单的 HTTP server。以下是您得到的结果:

`>>> import SimpleHTTPServer

dir(SimpleHTTPServer)
'BaseHTTPServer', 'SimpleHTTPRequestHandler', 'StringIO', 'all', 'builtins',![images'doc', 'file', 'name', 'package', 'version', 'cgi', 'mimetypes',images
'os', 'posixpath', 'shutil', 'test', 'urllib']`

我将在第七章中使用这个模块来构建一个快速脚本,让你从网络浏览器访问你的 Android 设备上的任何目录。您可能需要本地 IP 地址的值。套接字库在这里起了作用。获得自己的地址的一个技巧是连接到一个众所周知的网站,并从连接细节中检索您的地址。下面是如何做到这一点:

`>>> s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

s.connect(("gmail.com",80))
print s.getsockname()
('192.168.1.8', 64300)`

有一些函数返回代表 IP 地址的十进制或十六进制数。套接字库提供了两个函数来帮助你从一个十进制数字转换到标准 IP 地址符号中的四个数字,反之亦然。您还需要 Python struct模块。代码如下:

`>>> import socket, struct

struct.unpack('L', socket.inet_aton('192.168.1.8'))[0]
134326464
socket.inet_ntoa(struct.pack('L',134326464))
'192.168.1.8'`

要从 Python 访问任何 Android API 函数,您必须import android然后像这样实例化一个对象:

`>>> import android

droid = android.Android()`

完成后,您就可以完全访问所有 API 函数了。下面是从android.py开始的 Android 类的样子:

`class Android(object):

def init(self, addr=None):
if addr is None:
addr = HOST, PORT
self.conn = socket.create_connection(addr)
self.client = self.conn.makefile()
self.id = 0
if HANDSHAKE is not None:
self._authenticate(HANDSHAKE)

def _rpc(self, method, *args):
data = {'id': self.id,
'method': method,
'params': args}
request = json.dumps(data)
self.client.write(request+'\n')
self.client.flush() response = self.client.readline()
self.id += 1
result = json.loads(response)
if result['error'] is not None:
print result['error']

namedtuple doesn't work with unicode keys.

return Result(id=result['id'], result=result['result'],
error=result['error'], )

def getattr(self, name):
def rpc_call(*args):
return self._rpc(name, *args)
return rpc_call`

下面是 SL4A 安装中的一个简单 Python hello_world.py脚本:

`>>> import android

droid = android.Android()
name = droid.getInput("Hello!", "What is your name?")
print name
Result(id=0, result=u'Paul', error=None)
droid.makeToast("Hello, %s" % name.result)
Result(id=1, result=None, error=None)
`

当您将 Python 语言的简单性与 Python 标准库的广度相结合时,您将获得一个在桌面、服务器或 Android 设备上使用 SL4A 实现有用脚本的强大工具。

图 1-4 显示了当你运行简单的hello_world.py脚本时,你在屏幕上看到的第一个东西。当你像我一样输入你的名字并按下 Ok 按钮时,你会看到如图图 1-5 所示的弹出信息,这是调用makeToast API 的结果。

images

图 1-4。 Hello World 输入对话框

images

图 1-5。make toast API 调用的结果

总结

本章从一般意义上介绍 Android 平台,更具体地介绍 SL4A。在你继续阅读本书的其余部分时,这里有一些事情值得注意和记住。

这是你应该从这一章中抓住的:

  • Linux 基础:Android 操作系统本质上是幕后的 Linux。许多相同的目录和程序都在那里。
  • Java 是 Android 的语言:基于 Android SDK 构建的应用运行在 Java DVM 中。虽然这不是绝对必要的,但了解一点 Java 有助于理解 SL4A 背后的一些管道,这没有什么坏处。
  • SL4A 是一个容器应用:它托管所有不同的语言解释器,并使用 RPC 向底层操作系统提供接口。
  • JSON 是传递数据的方式:格式良好的 JSON 不难阅读,事实上,它有非常逻辑的结构。一旦您习惯了语法,您应该能够处理任何 API 函数。
  • 语言选项:虽然 Python 是最流行的,但是您也可以选择其他语言,包括 Beanshell、JRuby、Lua、Perl、PHP 和 Rhino。

二、入门指南

这一章将会给你所有你需要的关于谷歌 Android 脚本层(SL4A)的信息。

images 本章将介绍一些主题,稍后会有更详细的介绍。

好吧。我们开始吧。以下是我将在本章中介绍的内容:

  • 在您的设备上安装核心文件
  • 安装 Android SDK
  • 远程连接到您的设备
  • 执行简单的程序

通过使用这里给出的说明,您将能够在很短的时间内开始使用 SL4A。请密切注意有关配置您的设备和桌面的章节,因为需要完成这些说明才能使两者进行通信。当我带你浏览这些例子时,你会发现跟随它们是有帮助的。

在设备上安装 SL4A

开始使用 SL4A 的最快方法是简单地将其安装在 Android 设备上。有几种方法可以做到这一点。如果您导航到 SL4A 主页(http:// code.google.com/p/android-scripting,您将找到的下载链接。apk 文件和二维码,用于安装在您的设备上的条形码扫描仪。下面列出了安装 SL4A 需要完成的步骤:

  1. Download the SL4A .apk file (see Figure 2-1).images

    图 2-1。下载。apk 文件

  2. Launch the .apk file from the notifications screen (see Figure 2-2).images

    图 2-2。下水了。apk 文件

  3. Select Install on the next screen to actually install SL4A (see Figure 2-3).images

    图 2-3。安装 SL4A

不要担心这些警告,因为它们只是表明 SL4A 有能力执行这些任务中的任何一项,但是除非您实际编写一个脚本来执行这些任务,否则不会这样做。如果您选择在模拟器中安装,只需从模拟器中的浏览器导航到 SL4A 网站,然后单击 QR 码或下载下的sl4a_rx.apk链接。

首次启动 SL4A 应用时,系统会询问您是否允许收集匿名使用信息。无论哪种方式,您都可以通过首选项菜单随时改变主意。

现在您已经安装了 SL4A 主应用,您仍然需要安装您最喜欢的解释器。这可以通过启动 SL4A 应用,然后按下菜单按钮来完成(参见图 2-4 )。

images

图 2-4。 SL4A 菜单按钮弹出

这将在屏幕底部显示许多按钮,包括一个带标签的视图。触摸该按钮会弹出一个选择对话框,包括解释器选项(见图 2-5 )。

images 注意因为这本书主要是关于使用触摸作为主要用户交互的 Android 设备,所以你会看到经常使用的词语触摸选择。还会提到其他基于手指的动作,如向下拖动或从左向右滑动。

images

图 2-5。 SL4A 查看按钮弹出

选择解释器选项会将你带到一个默认屏幕,该屏幕只列出 Shell 作为可用选项。要安装额外的解释器,再次按下菜单按钮,然后触摸添加按钮(参见图 2-6 )。

images

图 2-6。口译员屏幕选项菜单

这将显示一个可供下载和安装的解释器列表(见图 2-8 )。选择 Python 2.6.2 这样的版本将会从 SL4A 主网站下载主要的 Python 解释器包。你必须再次访问通知栏,从显示屏顶部向下滑动,通过触摸文件名启动 Python 安装程序(见图 2-7 )。

images 注意在这一点上,花点时间查看解释器屏幕菜单选项中的终端帮助是值得的。在口译员屏幕可见的情况下,按硬件菜单按钮,然后选择帮助。在那里,选择终端帮助并阅读关于输入和编辑文本。

images

图 2-7。 Python 解释器下载通知

images

图 2-8。 SL4A 增加一个翻译

在这一点上,我应该提到还有一种安装 Python 解释器的替代方法。这种方法包括下载.apk文件,然后使用 Android 调试桥(ADB)安装它。如果您希望尝试任何新的 Python for Android 版本,您将需要使用这种方法,因为 SL4A 的基础安装通常指向最新的正式发布版本。在这种情况下,您可以使用 web 浏览器导航到 Google code 站点([code.google.com/p/python-for-android](http://code.google.com/p/python-for-android)),然后使用右键单击并另存为的方法下载PythonForAndroid_rx.apk文件,其中 x 代表您想要测试的版本。接下来,您将使用以下 ADB 命令将.apk文件安装到仿真器或物理设备上:

adb install PythonForAndroid_r6.apk

触摸安装按钮开始实际安装,并显示与 SL4A 安装完成时相同的“打开”和“完成”按钮。使用早期版本的 Python 解释器,你会在触摸“打开”后看到一个安装按钮(见图 2-9 )。用于 Android 的 Python 的最新版本将呈现一个类似于图 2-10 中的屏幕。添加了三个新按钮以方便模块管理。我所说的模块是指普通 Python 发行版中不包含的额外库模块。Python for Android 项目提供了几个这样的模块,当你点击“浏览模块”按钮时,你可以看到它们。这将在 Python for Android wiki 网站上打开一个网页,让您有机会下载它们(参见图 2-11 )。

images

图 2-9。为 Android 安装 Python

images

图 2-10。 Python for Android 安装程序

如果一切运行正常,您应该会看到一个快速弹出的对话框,上面写着安装成功。此时,将出现一个屏幕,其中列出了 Shell 和 Python 2.6.2 解释器。可以以类似的方式添加额外的解释器。选择 Python 2.6.2 选项将启动 Python 解释器。

images

图 2-11。 Python-for-android 模块页面

现在你终于可以在标准的 Python 命令提示符下输入代码了,如图 2-13 所示。您可以输入任何有效的 Python 代码,并立即看到结果。如果您想要访问任何 android 功能,您必须导入 Android 模块。实现典型的“Hello World”程序总共需要三行代码,如下所示:

`>>> import android

droid = android.Android()
droid.makeToast('Hello, Android World')`

当你在第三行之后点击回车键,你应该会看到一个弹出的对话框,里面有文字“你好,安卓世界”(见图 2-12 )。几秒钟后,对话框将自动关闭。

images

图 2-12。make toast 函数调用结果

images

图 2-13。 SL4A Python 解释器提示

如果您在解释器中按下菜单按钮,您会在屏幕底部看到四个按钮,分别标记为“强制大小”、“电子邮件”、“首选项”和“退出并编辑”。这些按钮对于每个解释器都是通用的,因此您可以从 Python、BeanShell 或您选择安装的任何其他解释器中访问它们。图 2-14 显示了这些按钮的样子。

images 注意所有脚本都存储在您设备的 SD 卡上的/sdcard/sl4a/scripts 目录下。如果您使用 USB 电缆将设备连接到主机,SD 卡将不可用,您的脚本也不可见。

images

图 2-14 。SL4A Python 解释器菜单

力度按钮允许你改变解释器的屏幕尺寸。默认值为 80 × 25,这非常适合横向模式下的屏幕。一旦你选择了你的尺寸并选择了调整尺寸按钮,你的屏幕将会调整到新的尺寸。图 2-15 显示了调整大小对话框。

images

图 2-15。 SL4A 解释器屏幕调整对话框

电子邮件菜单选项将捕获解释器屏幕中的所有文本,并将其加载到电子邮件中,允许您将键入的所有内容发送给自己(或任何人)。下面是前面的“Hello World”代码的电子邮件文本,为了清楚起见,对实际消息进行了一些编辑,添加了回车:

`Python 2.6.2 (r262:71600, Sep 19 2009, 11:03:28)
[GCC 4.2.1] on linux2
Type "help", "copyright", "credits" or "license" for more information.

import android
droid = android.Android()
droid.makeToast('Hello, Android World')
Result(id=1, result=None, error=None)
`

这是一个停下来谈论在你的主机和设备之间移动文件的好时机。最简单的方法是使用 USB 电缆连接您的设备,并将连接类型设置为磁盘驱动器。有了这个设置,您应该能够在任何操作系统(OS)上使用普通的文件管理器应用浏览设备上的文件。此时,在主机和设备之间移动文件变成了简单的拖放操作。在后面的章节中,当我们更详细地讨论 Android SDK 时,我们将会看到一些其他的方法。

现在回到解释器选项菜单。对于 SL4A R3,“退出和编辑”按钮是灰色的,这意味着它当前没有实现。选择 Preferences 按钮会显示一个新的可滚动页面,其中有多个条目,按功能区域分组,允许您配置任意数量的不同选项。这些选项中的一些,例如字体大小,在不同的标题下重复,使得在编辑器工具和解释器窗口中改变字体大小成为可能。

有几个选项你应该注意。如果在第一次安装 SL4A 时选择了允许使用情况跟踪,则可以通过首选项屏幕上的第一个条目进行更改。图 2-16 显示了首选项屏幕的前四个选项,包括一般使用跟踪、脚本管理器显示所有文件、脚本编辑器字体大小和终端滚动大小。其中大多数都是不言自明的,但是我将强调一些有趣的东西。

images

图 2-16。SL4A 偏好菜单的第一部分

启用脚本管理器标题下的显示所有文件选项将显示设备上/sdcard/sl4a/scripts目录中的所有文件。如果您使用其他文件作为应用的一部分,并且希望验证它们确实位于正确的目录中,这将非常方便。脚本编辑器标题下的字体大小选项将仅为文本编辑器设置字体大小。在终端标题下还有另一个字体大小选项,当你打开解释器命令提示符时,它将设置终端窗口中字符的大小。

终端标题还有其他一些有趣的设置。旋转模式可让您选取终端窗口可见时屏幕的行为方式。选项包括默认、强制横向、强制纵向和自动。您可能希望终端窗口总是以横向模式打开,这种模式更容易看到,但是您可以使用的屏幕空间有限。当您旋转设备时,自动选项将为您旋转屏幕。

“终端”窗口的默认屏幕颜色是黑色背景上的白色文本。您可以使用色轮选择器或滑块控件将它们更改为您喜欢的任何形状。出于截屏的目的,我设置了与默认颜色完全相反的颜色,即白色背景上的黑色文本。“颜色选择器”对话框显示当前颜色的 HSV、RGB 和 YUB 值,以及可用于 CSS 的十六进制代码。图 2-17 显示了颜色选择器的样子。您可以通过选择“接受”按钮来接受您的更改,或者使用“恢复”按钮恢复到之前的状态。

images

图 2-17。终端窗口颜色选择器

您可能还想启用一个终端标题选项,那就是保持屏幕清醒选项。默认情况下它应该是打开的,但是如果当你正在思考下一行代码时,你的屏幕开始消失,你将知道该寻找哪个选项。接下来,我将带您在开发机器上安装 Android SDK,以利用那里的一些工具。

安装 Android SDK

Android SDK 提供了许多工具,让开发人员的工作变得更加轻松。我将在这里解决安装 SDK 的问题,并在后面的章节中深入探讨它的内容。第一步是下载适合您的操作系统的安装文件。谷歌为 Linux、Mac OS X 和 Windows 提供安装程序。如果您的机器上还没有安装 Java 开发工具包(JDK ),您还需要先安装它。我将解决所有三个平台上的安装问题,以确保基础都包括在内。

Linux

为了在 Linux 上安装 SDK,我将从全新安装 64 位版本的 Ubuntu 10.10 桌面开始。在继续操作之前,您还需要确保系统安装了最新的安全补丁。您必须安装的第一件事是基础 Java 包。这可以通过终端窗口中的以下命令来实现:

$ sudo add-apt-repository ppa:sun-java-community-team/sun-java6 $ sudo apt-get update $ sudo apt-get install sun-java6-jre sun-java6-bin sun-java6-jdk

为了让它正常工作,您需要执行另一个命令行安装来让 SDK 完全正常工作。这与 SDK 使用 32 位包有关。创建一个新的 Android 虚拟设备(AVD)使用了mksdcard工具,它依赖于 ia32-libs 包。要安装此依赖项,您需要在终端窗口中输入另一个命令:

$ sudo apt-get install ia32-libs

对于 Linux,在 Android SDK 下载页面上有一个包含所有基础 SDK 文件的.tgz文件([developer.android.com/sdk](http://developer.android.com/sdk))。如果你的浏览器使用 Firefox,你应该可以选择使用 File Roller 工具打开文件。将整个目录解压到某个方便的地方。一旦您提取了文件,您需要运行 SDK 管理器应用来实际安装一个或多个版本的 Android 平台。

SDK Manager 应用在所有三个平台上看起来几乎是一样的,所以我将只描述一次下载特定版本的过程。在 Linux 上,管理器应用被命名为android,存在于主 SDK 根目录下的tools子目录中。您可以通过双击android文件从 Nautilus 文件管理器窗口启动它。如果您真的很好奇,可以在文本编辑器中打开该文件,因为它实际上是一个将启动sdkmanager.jar的 shell 脚本。这将调出 SDK 管理器主屏幕,如图图 2-18 所示。

images

图 2-18。 Android SDK 管理器屏幕

images 注意如果您通过代理访问互联网,您需要在设置菜单中添加该信息,如图图 2-19 所示。

images

图 2-19。代理服务器设置页面

选择安装哪个版本主要取决于您将支持的设备上运行的 Android 版本。对于本书中的示例,我将安装所有 2.x 版本和示例。为此,您只需单击这些项目旁边的复选框,以及 Android SDK 平台工具和文档条目,然后单击 Install Selected 按钮。如果有相当快的网络连接,整个过程应该不到十分钟。

在桌面上创建一些由 SDK 安装的常用程序的快捷方式以备后用是有意义的。你可以用 GNOME 右键点击桌面并选择创建启动器。该对话框提示在图标下显示名称以及要执行的命令。如果您单击命令文本框旁边的浏览按钮,您将能够导航到 SDK 工具目录。点击android文件,将其设置为新启动器的执行目标。现在,您可以通过桌面上的图标快速访问 SDK Manager 应用。

SDK 安装说明建议您也将toolsplatform-tools目录添加到您的路径中。如果您将 SDK 文件解压缩到您的主目录中,您可以从命令行使用以下命令进行设置:

export PATH=${PATH}:~/android-sdk-linux-x86/tools:~/android-sdk-linux-x86/platform-tools

images 注意注意 SDK 版本之间的细微变化,这些变化可能会让你陷入困境。谷歌从 SDK 版本 8 到 9 (Android 2.2 到 2.3)改变了惯例,包括几个最常用的工具的目录。

Mac OS X

在 Mac 上安装包括下载一个 zip 文件,然后解压其中的内容。您必须运行 Android SDK 和 AVD 管理器应用,然后选择要安装的版本。要开始这个过程,您可以从 Finder 启动 Android 应用,或者从您解压缩 SDK 下载的目录中的终端窗口使用以下命令启动:

$ tools/android

第一次运行 SDK Manager 应用时,您将看到一个可供选择的 SDK 版本列表。从命令行的角度来看,OS X 看起来几乎和 Linux 一样,包括设置路径语句。您可以使用与以前相同的语法,注意指定 SDK 目录的正确路径。在我的例子中,我在主目录的顶层解包了 SDK,并使用了以下导出语句:

export PATH=${PATH}:~/android-sdk-mac-x86/tools:~/android-sdk-linux-x86/platform-tools

您需要将这一行添加到。bash_profile 文件,以便在下次登录时修改路径。您可能想要在 dock 上创建一个快捷方式来快速启动 SDK 管理器。你所要做的就是把android文件从tools目录拖到 dock 的右边。现在,您可以一键访问 SDK 管理器,并从那里启动设备仿真器。我们将在第三章中深入探讨如何使用设备模拟器。

Windows

Google 提供了一个 zip 文件和一个可执行文件来在 Windows 上安装 SDK。下载文件的大小几乎是一样的,所以选择一个不应该有任何区别。如果您下载了。exe 文件,您就少了一个解压文件的步骤。这也是推荐的选择,所以这就是我要说的。对于所有的 Windows 开发和示例,我将使用 64 位版本的 Windows 7 旗舰版。要开始这个过程,我只需点击 Android SDK 下载页面上的installer_r08-windows.exe链接。一旦文件下载完毕,你需要双击来启动安装程序。如果你没有 Java 开发工具包(JDK),你会看到如图图 2-20 所示的屏幕。

images Android SDK。exe 安装程序会查找 32 位版本的 JDK,如果您没有安装它,安装程序将不会继续。

images

图 2-20 。缺少 JDK 屏幕

尽管我在 64 位 Windows 上进行测试,但我发现必须安装 32 位 JDK 才能安装 Android SDK。一旦你下载了 JDK,你只需双击文件名开始安装。完成该步骤后,您现在可以继续安装 Android SDK 了。安装完成后,您将可以选择启动 SDK 管理器。在 Windows 上首次启动 SDK 管理器看起来与 Linux 和 Mac OS X 略有不同。图 2-21 显示了您将看到的对话框,允许您选择要安装的特定软件包。

您会注意到用于选择 SDK 版本的对话框略有不同,它要求您在对话框中选择一个特定的行,然后单击 Reject 单选按钮取消选择一个版本。因为我将只使用 SDK 的 2.x 版本,所以我取消了 1.5 和 1.6 版本选项。完成后,我点击了安装按钮,然后一切运行,没有干预。当你选择.exe安装程序时,你会得到一个新的选项添加到你的 Windows 程序菜单,标签为 Android SDK 工具。您也可以右键单击 SDK 管理器图标,并将其拖到桌面上以便快速访问。请注意,如果您只是简单地从 Windows 程序菜单中拖动图标,实际上您会将它移动到桌面上。

images

图 2-21。初始 Android SDK 管理器屏幕

安装 Python

虽然你可以直接在你的设备上输入代码,但你会很快发现这个过程非常乏味,除非你碰巧有一个全尺寸的键盘。从主机远程连接为开发和测试应用提供了一个更高效的环境。我将在第四章中介绍如何使用 Eclipse 来实现这一目的,但现在我将向您展示如何使用 Android SDK 工具来做本质上相同的事情。

然而,还有一件事需要先做。如果你碰巧使用 Linux 或者 Mac OS X,你很可能已经安装了 Python 的一个版本。在这两个平台上,您可以通过打开终端窗口并启动 Python 来确定。图 2-22 显示了在我运行 OS X 雪豹 10.6.5 版本的 Mac Mini 上的样子。

images 注意任何 2.6 版本的 Python 都应该可以在主机上工作,但是要注意 SL4A 是基于 2.6.2 的,以防你在从主机远程运行脚本到设备时看到一些奇怪的行为。

images

图 2-22。运行在 Mac OS X 上的 Python

默认情况下,Python 不会安装在 Windows 上。要安装 Python,请转到[python.org](http://python.org)并找到发布页面([python.org/download/releases](http://python.org/download/releases))。在那里,您可以找到所有的主要版本,包括版本 2.6.6。选项包括 32 位和 64 位 Windows 版本。出于测试目的,我选择了 64 位版本。双击。msi 文件启动安装程序,提示您是否允许安装。

完成后,您可以修改 Windows 路径来添加 Python26 目录。完成该任务的最快方法是按下 Windows 键并键入单词 system 。您应该在控制面板标题下看到编辑系统环境变量选项。单击该行启动系统属性对话框,然后单击环境变量按钮。在系统变量部分找到 Path 变量,然后单击编辑按钮。这将显示一个带有当前系统路径语句的文本框。导航到字符串的末尾,并添加以下文本:

;C:\Python26

这将把 Python 目录添加到搜索路径中,并使 Python 可以在任何命令窗口中使用。要验证您是否正确安装了 Python 并正确设置了路径,请启动一个命令窗口并键入单词 Python 。你应该会看到类似图 2-23 的东西。

images

图 2-23。运行在 Windows 7 上的 Python

远程连接到设备

从主机连接到您的设备需要 Android SDK 和 ADB 工具。我将在后面的章节中深入研究这个工具,但是现在我将讨论如何使用这个命令来远程连接到设备。实际上,您正在设置一个代理,通过特定端口将通信传递给设备。

要从 Windows 连接到您的设备,需要克服一些障碍。第一个也可能是最大的障碍是让 Windows 识别你的设备。下载 SDK 包时的可选组件之一是用于 Windows 的 USB 驱动程序。如果您希望计算机识别您的设备,这是必需的。图 2-24 显示了可用包窗口下的该项目。

选择此选项,然后单击“安装选定内容”按钮。这会将驱动程序文件下载到 SDK 根目录下的子目录中。接下来的步骤将取决于您尝试连接到 Windows 电脑的设备类型。

images

图 2-24。用于 Windows 的 USB 驱动程序

如果你的设备碰巧是 G1、myTouch 3G、Verizon Droid 或 Nexus One,你应该已经准备好了。如果没有,你将有更多的工作要做。连接到另一个设备,如 HTC EVO 4G 智能手机,需要将附加信息添加到驱动程序.inf文件中,以便驱动程序识别该设备。这个文件可以在 google-usb_driver 子目录中的 SDK 所在的目录树中找到。您真正需要知道的唯一事情是设备的供应商 ID (VID)和产品 ID (PID)。你可以在谷歌上搜索几下,为你的手机找到合适的号码。另一个寻找 ADB 连接问题答案的好地方是 Google groups 上的 Android Developers group([groups.google.com/group/android-developers](http://groups.google.com/group/android-developers))。

作为最后的手段,你必须进入侦查模式。当您将设备连接到 Windows 时,它会尝试为您安装合适的驱动程序。如果它找不到,你会得到一个弹出信息,表明该设备未能正确安装。此时,您必须使用设备管理器来发现所需的信息。启动设备管理器最简单的方法是按下 Windows 键,开始在搜索框中键入设备。这会给你一个选项列表,包括设备管理器。选择设备管理器会弹出一个类似于图 2-25 的对话框。

由于 Windows 无法识别您连接的设备,您将看到 ADB 列在“其他设备”下。现在,您必须右键单击 ADB 设备并选择 Properties 来查找您的设备的 VID 和 PID 列表。我将向您展示 HTC EVO 4G 的外观,希望您可以使用相同的方法来连接您的设备。

images

图 2-25 。Windows 设备管理器显示未知设备

显示 ADB 属性屏幕后,您需要选择详细信息选项卡,然后从下拉框中选择硬件 id。图 2-26 显示了您应该看到的 HTC EVO 4G 信息。接下来,您需要 VID 和 PID 信息来修改驱动程序.inf文件。

images 注意如果你采用了 Android SDK 的默认值。exe 安装程序,你会在以下目录找到 USB 驱动文件:C:\ Program Files(×86)\ Android \ Android-SDK-windows \ Google-USB _ driver。

images

图 2-26。亚行设备的设备属性

有了 VID 和 PID,您需要编辑 android_winusb.inf 文件。您需要添加的兴趣行如下:

; ;HTC EVO 4G %SingleAdbInterface% = USB_Install, USB\VID_0BB4&PID_0C8D![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) %CompositeAdbInterface% = USB_Install, USB\VID_0BB4&PID_0C8D&MI_01

进行更改的最简单方法是使用记事本编辑文件。如果您希望能够将记事本保存回同一个目录,您必须以管理员身份启动记事本。为此,你可以按下 Windows 键,开始输入记事本。右键单击弹出窗口中的记事本,然后选择以管理员身份运行。打开记事本后,导航到google-usb_driver目录,然后双击android_winusb.inf文件。当您打开文件时,您需要找到带有标签[Google]的部分。NTx86]并复制下面的前三行。对我来说,这是 HTC 梦想设备的一个入口。将这些行粘贴到[Google.NTx86][Google.NTamd64]部分的末尾,更改您之前发现的 VID 和 PID 值。我没有改变 VID,因为 EVO 4G 是 HTC 设备。

现在,您应该准备好使用 USB 电缆将设备连接到主机。我还必须将设备上的 USB 连接类型改为 HTC Sync。这将在手机上打开一个对话框,因为它试图连接到主机上的 HTC Sync 应用。此时,您可以按下设备上的后退键,或者等待连接尝试超时。通过再次启动设备管理器并右键单击其它设备下的 ADB 设备,更新 ADB 设备的驱动程序。从弹出对话框中选择更新驱动程序,然后从下一个屏幕中选择浏览我的电脑以查找驱动程序软件。该对话框允许您浏览放置编辑过的.inf文件的目录。确保您选择了google-usb_driver目录,然后单击下一步。如果一切顺利,您应该会看到一个屏幕,指示 Windows 已成功更新您的驱动程序软件。

最后,在您的设备上启用 USB 调试。这发生在设备设置屏幕上。从设置,触摸应用,然后开发。在开发屏幕上,您必须选中 USB 调试旁边的复选框来启用该功能。

如果您成功地正确配置了一切,您将能够打开一个命令窗口或终端会话并发出adb命令。在 Windows 上,你应该可以看到你的设备带有如图 2-27 所示的命令。

images

图 2-27。输出adb设备命令

如果您使用的是 Linux 或 Mac OS X,您应该不会有任何连接设备的问题。在连接 USB 电缆后,我可以在两个平台上使用 adb 设备命令查看 HTC EVO 4G,无需任何额外的步骤。

设备设置

远程连接到您的设备需要在设备和桌面上执行一些步骤。在设备上,您必须从解释器屏幕启动服务器。为此,您必须完成以下步骤:

  1. 从设备上的所有应用屏幕启动 SL4A。
  2. 按菜单按钮,然后选择查看选项。
  3. 从列表中选择口译员。
  4. 再次按下菜单按钮,然后选择启动服务器。
  5. 如果您想通过 WiFi 连接,请选择公共;如果您通过 USB 连接,请选择私人。

此时,您的服务器应该已经启动,并准备好通过特定端口进行访问。要找出分配了什么端口号,您必须通过从设备屏幕顶部向下拖动状态窗口来打开脚本监视器。这将显示 SL4A 服务和此时应该为 1 的运行脚本的数量。要确定端口号,您必须单击(触摸)SL4A 服务线路以调出脚本监视器。您应该会看到类似于图 2-28 的内容,端口号显示在 localhost: string 之后。

images

图 2-28。 SL4A 服务器模式,端口地址

您现在可以远程连接到您的设备了。要实现这一点,您必须再输入几个命令来定义一个环境变量并启用端口转发。这些命令和 Python 本身需要从以管理员身份运行选项启动的命令窗口中运行。环境变量必须命名为 AP_PORT,默认值为 9999,这样示例才能运行。为了启用端口转发,使用 adb 命令将端口 9999 的所有内部 tcp 流量转发到远程服务器的端口号。图 2-29 显示了这在 Windows 上应该是什么样子。

Linux、Mac OS X 和 Windows 之间的命令略有不同。在 Linux 和 Mac OS X 上,您可以使用 export 命令创建一个环境变量:

$ export AP_PORT=9999

您可能还想将它添加到启动脚本中。在 Linux 上,这将是~/.bash_profile~/.bashrc。在 Mac OS X 上,它会出现在你的主目录中的.bash_profile

images

图 2-29。远程控制的 Windows 环境变量和 adb 命令

在基于 Windows 的计算机上,您可以通过之前使用的环境变量屏幕将其添加为永久环境变量。这次你用 new 按钮创建一个新的用户变量,然后输入名称 AP_PORT 和值 9999 ,如图 2-30 中所示。

images

图 2-30。为 AP_PORT 创建一个永久的 Windows 环境变量

执行简单的程序

一旦你把所有东西都连接起来,你可能想开始使用 Python 和 IDLE 进行探索。IDLE 是一个用 Python 和 Tkinter GUI 工具包编写的简单的跨平台集成开发环境(IDE)。首先,它提供了一个 Python 解释器命令行,您可以在其中键入代码行,并获得代码结果的即时反馈。还可以打开编辑器窗口来创建和修改 Python 脚本。可以保存这些脚本,然后在主解释器窗口中运行输出。在执行脚本之前,您还可以在编辑器窗口中获得关于任何语法错误的反馈。

这种方法的巧妙之处在于,您可以在桌面计算机上编写代码,并在您的设备上测试代码。请记住,您获取的任何库都需要在您的桌面计算机上可用。你需要注意的另一件事是 Python 版本号。截至本书写作时,SL4A 使用的是 Python 的 2.6.2 版本。在 2.6.2 之后,您可能可以使用任何 2.6 版本,但是请注意,如果您在桌面上使用不同于 2.6.2 的版本,您可能会遇到一些难以理解的兼容性问题。图 2-31 显示空闲运行“Hello World”程序。

您需要将android.py文件复制到与代码相同的目录中,或者复制到开发机器上的默认 Python 安装目录中。当您在脚本中执行import android代码时,解释器必须能够定位android.py

images

图 2-31。 Python 空闲程序连接到设备

您还可以使用 IDLE 在单独的编辑器窗口中编辑和运行脚本。如果您单击文件菜单并选择新窗口,您将看到一个空白的编辑器屏幕,用于输入 Python 代码。您也可以在即时窗口和编辑器窗口之间进行剪切和粘贴。用“Hello Android World”代码尝试一下,只需要少量的编辑就可以删除额外的提示字符。图 2-32 显示了结果。

images

图 2-32。 Python 空闲编辑窗口

开始探索 SL4A 的另一个很好的方法是看看随 Python 解释器一起安装的一些示例程序。其中之一是Test.py,它是一个示例程序,使用 SL4A 支持的许多对话框类型以及一些其他测试用例。这是一个导入默认安装在设备上的几个模块的程序,但是除非您明确安装它们,否则它们可能不会在您的主机上。

您可以通过启动 SL4A 并从文件列表中选择Test.py在您的设备上运行Test.py。当你从 SL4A 主窗口中选择一个文件时,你会看到一个弹出的小对话框,里面有很多图标。图 2-33 显示了您应该看到的内容。

images

图 2-33。 SL4A 脚本启动选项

终端图标在设备的终端窗口中启动脚本,因此当齿轮图标在后台启动脚本时,您可以看到任何错误或调试消息。选择铅笔图标将在文本编辑器中打开脚本,而磁盘图标将允许您重命名脚本。如果您选择垃圾桶图标,您将有一次机会改变删除的想法,因为将会打开一个“是/否”对话框,提示您确认这是您想要做的。

脚本编辑器提供了一种在设备上输入或编辑脚本的简单方法。它在小型设备上效率不是很高,但在平板电脑等大屏幕设备上效果相对较好。在将脚本加载到实际设备之前,您也可以在模拟器中使用它来测试脚本。完成编辑后,您必须按下菜单按钮,调出选项菜单,以保存并退出或保存并运行。图 2-34 显示了它在你的设备上的样子。

images

图 2-34。 SL4A 脚本编辑器选项菜单

Preferences 按钮将调出与之前相同的菜单,其中包含 SL4A 应用的所有选项。选择帮助按钮将显示一个对话框,其中有三个选项,包括 Wiki 文档、YouTube 截屏和终端帮助。前两个选项将打开一个 web 浏览器,并重定向到您在 SL4A 项目的主页上找到的相同页面。

API 浏览器按钮将显示一个列出所有可用 API 函数的屏幕(参见图 2-35 )。

images

图 2-35。 API 浏览器工具

如果你长按(意味着触摸一条线并保持),你会看到一个类似于图 2-36 中的屏幕。选择“提示”将会弹出一个额外的对话框,其中有单独的文本框,供您填写 API 函数调用所需的必要参数(参见图 2-37 )。这是探索一些更复杂的 API 函数并让 SL4A 解释器引导您填写正确条目的好方法。

images

图 2-36。 API 浏览器选项

images

图 2-37。 API 浏览器提示选项

总结

这一章包含了很多信息,但是希望到此时,您已经在设备和主机上完全配置好了 SL4A 测试环境。主机的操作系统不重要,所以你可以自由选择你最喜欢的平台。如果你碰巧在某个地方遇到困难,谷歌搜索是你的朋友。

作为总结,让我们确定本章的要点:

  • 安装 SL4A :这发生在物理设备上或者模拟器中。您首先安装 SL4A。apk 文件,然后从 SL4A 菜单添加解释器。
  • 安装 Android SDK :这发生在你的开发机器上,它可以运行 Linux、Mac OS X 或 Windows。如果您的操作系统还没有安装 Java,您可能还必须先安装它。
  • 在 Windows 上配置 USB 驱动:这可能是最棘手的一步,不幸的是,如果你是在 Windows 机器上开发,这是无法回避的。您必须让这个工作在您的开发机器和一个物理设备之间建立通信。
  • 在 Windows 上安装 Python:同样,你不会在任何库存的 Windows 机器上找到 Python,所以你必须自己安装。幸运的是,这很简单。

不要害怕在这里踢轮胎。如果你不习惯在你闪亮的新手机上尝试这些东西,可以使用模拟器。这就是它存在的目的。要知道,总有一天你将不得不使用该器件,因为并非真实器件的所有功能都可以在仿真器中模拟。好消息是,这些能力的列表非常短。

三、Android SDK 导航

本章将深入探讨 Android 软件开发工具包(SDK)以及如何使用它来开发针对 Android 脚本层的代码。

images 本章所有例子都基于 Android SDK 的 release 8。

在这一章中,我会这样分解它:

  • 费力地阅读 SDK 文档
  • 检查不同的 SDK 组件
  • 使用 Android 模拟器进行测试
  • 探索 Android 调试桥(ADB)
  • 使用 Dalvik 调试监控服务(DDMS)进行调试和更多工作

准备好进入 Android SDK 的世界吧。Android 的每个版本都有一个特定于该版本的 SDK。在构建原生 Android 应用时,您可以选择特定的版本,以便利用最新的功能。这意味着您可以使用最新的 SDK 版本,并且仍然可以构建和测试在旧版本上运行的应用。我将使用版本 8 (r8)作为目标,因为这是我将测试的所有设备上的 Android 版本。

费力地阅读 SDK 文档

如果您查看 SDK 安装的根目录,您会看到一个名为docs的条目。这实际上是主要 Android 开发者网站上文档的副本,供本地访问。打开docs目录,你会看到几个.html文件。Index.html是您打开网络浏览器并导航至developer.android.com时看到的画面。Offline.html提供了帮助您安装 SDK 的链接(如果您还没有安装的话),以及一个指向主站点的注释,“获取最新文档和全功能体验”

第一次浏览 Android 开发者网站时,你不难被淹没。那里的信息量非常惊人。最好的办法是退后一步,挑选几个你想阅读的领域,然后专注于这些主题。每个页面的顶部都有一个搜索框,它将按排序顺序返回您的请求,还有一个选项卡列表,让您可以快速将结果缩小到文档树的特定区域,包括开发指南、参考、开发人员小组、Android 源代码和博客。

如果你没有从架构的角度来看 Android 平台,那么你可能想从应用基础([developer.android.com/guide/topics/fundamentals.html](http://developer.android.com/guide/topics/fundamentals.html))部分开始,以便很好地了解 Android 的优势。我们详细讨论了许多顶级概念,这将有助于您理解应用如何与特定 Android 应用编程接口(API)提供的底层功能进行通信。在本书的上下文中,特别感兴趣的是不同的进程如何使用远程过程调用(RPC)进行通信。Android 脚本层(SL4A)使用这种机制将信息从脚本解释器传递到 Android API。

了解 Android 中的内容提供者将使您更容易使用脚本可用的信息。图 3-1 显示了 Android 开发者文档的开发指南部分的快照,内容统一资源标识符(URI)是什么样子的以及如何解释它。内容 URI 的关键部分是权威部分,它看起来很像一个网址。一旦你看到这个模式,你就能读懂其中的一个 URIs,并确切地知道它的意思。

images

图 3-1。安卓内容供应器描述

另一个值得花时间阅读的地方是用户界面指南部分。任何以某种方式与用户交互的程序都应该努力遵守谷歌在按钮、图标和文本输入方面的惯例。SL4A 提供了对大量用户界面元素的访问,您最好花些时间了解如何以及何时使用它们。一个很好的例子是什么时候使用上下文菜单,什么时候使用选项菜单。一个选项菜单通常在用户按下菜单按钮时出现,而一个上下文菜单类似于你在桌面操作系统上右击鼠标时得到的菜单。在 Android 上,长按或触摸并保持动作等同于用鼠标右键单击。

在第二章的中,我介绍了一个名为makeToast的 Android 实用函数。这个小函数创建了一个短的弹出消息,出现在屏幕上,但没有获得焦点或暂停当前活动。它是通知标题下的一类消息的一部分。一个吐司通知是给用户反馈他们刚刚做的事情的一个简单方法,比如设置一个闹钟在特定的时间响起。Android 还支持状态栏通知,以便在系统的状态栏中添加图标和可选的短消息,以及通知窗口中的扩展消息。你也可以发出声音或振动来给予额外的反馈。

对话框是其他值得一读的用户界面元素。DatePickerDialog 和 TimePickerDialog 是两个特殊的对话框,使在小屏幕上输入日期和时间变得更加容易。ProgressDialog 是一个用户反馈元素,用于为长期运行的活动提供进度信息。毫无疑问,AlertDialog 是所有对话框中最灵活的,也可能是最常用的。警告对话框还可以包含项目列表、复选框和单选按钮。图 3-2 显示了一个带有文本和按钮的警告对话框。

images

图 3-2。带有文本和按钮的警告对话框

文档中另一个有趣的地方是 WebView。如果你想建立一个除了按钮和对话框之外的用户界面,你必须使用 WebView。在第九章中,我会花很多时间讨论如何使用 WebView 构建界面,所以理解这些基础知识会有所帮助。

检查不同的 SDK 组件

如果您查看安装 Android SDK 的目录树,您应该会看到包含文档、示例代码和许多工具的文件夹列表。还有一个名为market_licensing的目录,里面有关于如何销售你完成的应用的信息。在 SDK 的第 8 版中,Google 对目录结构做了一些更改,影响了一些更常用工具的位置。如果您为以前安装的 SDK 版本设置了快捷方式或修改了路径,您将需要更改目标目录。

图 3-3 显示了 Windows 7 机器上顶层目录的屏幕截图。这显示了安装在 64 位版本的 Windows 7 上的 32 位 SDK,因此有了Program Files (x86)父目录。

images

图 3-3。Windows 上的 Android SDK 文件结构

如果您想要在 Windows 上创建 SDK 管理器应用的快捷方式,请确保在android-sdk-windows目录中右键单击 SDK 管理器应用,将其拖到桌面上,然后选择在此处创建快捷方式。如果你不这样做,你将会移动应用或者复制一个不能在桌面上运行的应用。

导航到 tools 子目录将显示许多可执行文件。第一个也可以说是最重要的是 Android 调试桥(ADB)工具。您可以使用 ADB 将文件从本地机器移动到设备,就像从命令行复制文件一样。我将在本章的稍后部分深入研究 ADB。SDK Manager 应用是您进行 SDK 更新、创建和启动虚拟设备以及查找第三方插件的起点。

使用 Android 模拟器进行测试

在使用 Android 模拟器之前,您必须做的第一件事是配置一个目标设备。Android 虚拟设备(AVD)由许多不同的文件组成,包括配置和虚拟存储,仿真器需要这些文件来完成工作。您可以根据需要创建任意数量的 avd 来模拟不同的设备。创建 AVD 最简单的方法是使用 SDK 和 AVD 管理器应用,如图 3-4 所示。

images

图 3-4。 Android SDK 和 AVD 管理器工具

要开始创建新的 AVD,只需单击 new 按钮。这将弹出一个对话框,如图 3-5 所示。你要做的第一件事是给你的新 AVD 一个名字。您应该使其具有描述性,但是名称不能有任何空格。选择名称后,您必须选择一个目标环境。最后,你需要为你的 SD 卡选择一个尺寸。如果您专门使用它来测试 SL4A,应该不需要太多空间。本例中使用了 100 MB 的值。

images 提示三星 Galaxy Tab 等其他目标设备可通过 SDK Manager 获得,而其他设备可能直接从供应商处获得,如 Barnes & Noble Nook Color。您还可以修改其中一个通用设备,以适应真实设备的功能,如硬件键盘或特定的屏幕尺寸。

屏幕分辨率将默认为内置设备之一,除非您选中分辨率单选按钮并指定具体尺寸。要添加或删除硬件功能,请单击硬件部分旁边的新建按钮,并从下拉列表中选择一项功能。要删除键盘等功能,首先通过单击“新建”按钮添加键盘支持,然后通过单击“值”列并选择“否”来更改其值

images

图 3-5。新的 AVD 对话框

您可以从 SDK 管理器屏幕启动任何已定义的 avd,方法是选择您的设备,然后单击启动按钮。这将打开另一个对话框,让您有机会在它开始之前设置一些选项。您想要更改的选项之一是“将显示比例调整为实际大小”复选框。这将启用“屏幕大小”和“监视器 DPI”文本框,您可以在其中选择希望模拟器在屏幕上显示的大小。这将取决于您的实际显示器尺寸,尽管我发现对于典型的 20 英寸显示器来说,10 英寸是个不错的选择。

最后一个复选框“擦除用户数据”为您提供了一种快速启动虚拟设备的方法,无需任何先前会话中的数据。此功能允许您测试首次运行时行为不同于正常行为的应用,而无需每次都重新创建新的 AVD。图 3-6 显示了屏幕尺寸选项设置为十英寸时该对话框的样子。

images

图 3-6。 AVD 启动选项对话框

对于带键盘的设备,从主机键盘到设备上的操作有一组标准的映射。表 3-1 显示了保存在default.keyset文件主目录下的.android子目录中的默认定义集。您可以通过编辑default.keyset文件或创建您自己的keyset文件,然后在模拟器命令行上添加–keyset选项来更改这些设置。

表 3-1。仿真器键映射

| `BUTTON_CALL` | 第三子代 | | :-- | :-- | | `BUTTON_HANGUP` | 法乐四联症 | | `BUTTON_HOME` | 主页 | | `BUTTON_BACK` | 逃跑 | | `BUTTON_MENU` | F2, 页面上传 | | `BUTTON_STAR` | Shift+F2,向下翻页 | | `BUTTON_POWER` | F7 | | `BUTTON_SEARCH` | F5 | | `BUTTON_CAMERA` | Ctrl+小键盘 _5,Ctrl+F3 | | `BUTTON_VOLUME_UP` | 小键盘+Ctrl+F5 | | `BUTTON_VOLUME_DOWN` | 键盘减号,Ctrl+F6 | | `TOGGLE_NETWORK` | F8 | | `TOGGLE_TRACING` | F9 | | `TOGGLE_FULLSCREEN` | Alt-Enter | | `BUTTON_DPAD_CENTER` | 键盘 _5 | | `BUTTON_DPAD_UP` | 键盘 _8 | | `BUTTON_DPAD_LEFT` | 键盘 _4 | | `BUTTON_DPAD_RIGHT` | 键盘 _6 | | `BUTTON_DPAD_DOWN` | 键盘 _2 | | `TOGGLE_TRACKBALL` | F6 | | `SHOW_TRACKBALL` | 删除 | | `CHANGE_LAYOUT_PREV` | 键盘 _7,Ctrl+F11 | | `CHANGE_LAYOUT_NEXT` | 键盘 _9,Ctrl+F12 | | `ONION_ALPHA_UP` | 键盘 _ 乘法 | | `ONION_ALPHA_DOWN` | 键盘 _ 除法 |

用键盘启动一个普通的 AVD 将会弹出一个类似于图 3-7 的屏幕。

images

图 3-7。通用仿真窗口

除了一些小的例外,模拟器应该像真实设备一样工作。您应该记住电脑键盘上的哪些键是模拟硬件按钮键的,因为您会经常使用它们。默认情况下,PC 到设备的映射是 Home 到 Home,F2 到 Menu,Esc 到 Back,F5 到 Search。电脑上的鼠标取代了你在真实设备上的手指。左击鼠标与触摸或按压屏幕是一样的。如果您点击屏幕底部中间类似棋盘的图标,您将启动如图图 3-8 所示的应用屏幕。

images

图 3-8。模拟器应用启动屏幕

要查看通知区域中的消息,您首先必须在设备屏幕顶部附近,在与信号强度、电池电量和时间图标相同的白色条区域中单击并按住鼠标。接下来,用鼠标像用手指一样向下拖动。这类似于拉下窗帘。该动作将随时显示通知窗口,如果没有通知,则显示“无通知”消息。同样的技术可以用来模拟从左到右的滑动,反之亦然,使用鼠标点击、按住并拖动仿真器设备屏幕。

蓝牙在模拟器中不起作用,所以如果你需要使用蓝牙进行测试,你必须在真实的设备上进行。WiFi 也是没有的。模拟器支持 3G 数据连接,这意味着你可以连接到互联网。您可以通过选择联系人或直接输入电话号码来模拟呼叫。所有呼叫都显示在呼叫日志中,为您提供了另一个测试数据源。虽然您可以模拟拨打和接听电话,但没有真正的语音功能,所以不要期望实际拨打真正的电话。这也意味着你不能在模拟器上测试任何语音识别功能。

出于测试目的,您可能需要在模拟器上配置电子邮件帐户,这就像在真实设备上一样。图 3-9 显示了首次启动应用时的打开屏幕。

images

图 3-9。模拟器电子邮件配置屏幕

要配置 Gmail 帐户,请输入您的完整地址,包括[@gmail.com](http://@gmail.com)部分。输入密码后,单击“下一步”按钮。如果您输入的信息正确,您应该会看到一个屏幕,要求您输入一个(可选的)帐户名称和一个要添加到外发邮件中的名称(签名)。

为了方便起见,您可以在模拟器主屏幕上添加一个指向 SL4A 应用和脚本文件夹的快捷方式。为此,单击并按住主屏幕上的任意位置。这将启动添加到主屏幕对话框。此时,您可以选择添加快捷方式、小工具、文件夹或壁纸。选择快捷方式条目,为 SL4A 应用添加一个快捷方式。这将显示另一个对话框,其中包含所有支持快捷方式的可用应用和操作。选择应用,并从选择活动对话框中选择 SL4A。

另一种快速访问脚本的简便方法是在主屏幕上添加一个文件夹。除了从“添加到主目录”对话框中选择“文件夹”之外,您的操作方式与添加快捷方式相同。选择文件夹后,您应该会看到一个标有“选择文件夹”的新对话框,其中包含可用文件夹的列表。选择标记为 Scripts 的条目,您应该已经准备好了。现在,当您点击主屏幕上的脚本图标时,您应该会看到类似图 3-10 的屏幕。这将显示该文件夹中所有脚本的列表,并让您一键访问运行它们。我将在第八章的中使用这个特性来构建一个方便的手机设置脚本,只需点击两下就可以改变多个设置。

images

图 3-10。主屏幕脚本文件夹

另一个不能在模拟器上运行的是实时短信。你可以打开消息应用,输入一条消息,但实际上什么也不会发出。如果您需要进行测试,它将使用传出消息填充数据库。要模拟一条短信,你必须使用 ADB 工具,这是下一个讨论的话题。

安卓调试桥

第二章对 Android Debug Bridge (ADB)有一个简单的介绍,但实际上只是刷了一下你可以用这个工具做的事情的表面。亚行实际上需要三个独立的组成部分来完成它的工作。在您的开发机器上,ADB 由一个客户端和一个服务器组成。ADB 服务器处理运行在开发机器上的客户机和运行在仿真器或目标设备上的守护进程之间的所有通信。

命令行选项用于指示 ADB 执行特定任务。表 3-2 给出了这些命令的简要描述。对于安装、同步和卸载命令,有一些选项可用于修改命令的行为方式。如果您碰巧运行了一个仿真器并且连接了一个真实的设备,那么您必须指定您希望 ADB 命令在哪里执行。要将 ADB 命令导向真实设备,使用选项–d,对于仿真器,使用–e

表 3-2。亚行命令列表

| `adb push ` | 将文件/目录复制到设备 | | :-- | :-- | | `adb pull []` | 从设备复制文件/目录 | | `adb sync [ ]` | 仅在发生更改时复制主机->设备(-l 表示列出但不复制) | | `adb shell` | 交互式运行远程 shell | | `adb push ` | 将文件/目录复制到设备 | | `adb shell ` | 运行远程 shell 命令 | | `adb emu ` | 运行仿真器控制台命令 | | `adb logcat [ ]` | 查看设备日志 | | `adb forward ` | 前向插座连接 前向规格如下: `tcp: localabstract: localreserved: localfilesystem: dev: jdwp:`(仅远程) | | `adb jdwp` | 列出主持 JDWP 传输的进程的 PID | | `adb install [-l] [-r] [-s] ` | 将此包文件推送到设备上并安装 ( `-l`表示正向锁定 app) ( `-r`表示重新安装 app,保留其数据) ( `-s`表示安装在 SD 卡上,而不是内部存储) | | `adb uninstall [-k] ` | 从设备 中移除此应用包(`-k`表示保留数据和缓存目录) | | `adb bugreport` | 返回设备中应包含在错误报告中的所有信息 | | `adb help` | 显示此帮助消息 | | `adb version` | 显示版本号 |
文件和应用

有三个命令用于处理主机与仿真器或物理设备之间的文件复制。push将文件从主机拷贝到目标,而pull将文件从目标拷贝到主机。sync尝试在主机和目标上的目录之间同步文件。您也可以将-l选项传递给sync,它将简单地列出目录的内容。

.apk文件安装或卸载到仿真器或物理设备分别使用 adb installuninstall命令。install命令的选项包括-l向前锁定应用、-r重新安装保留所有旧数据,以及-s将应用安装在 SD 卡上而不是内部设备存储上。

images 提示您可以使用 adb push工具在仿真设备上快速加载触点以进行测试。如果您在 Gmail 帐户中存储了联系人,您可以轻松地将其导出到 vCard 文件,然后使用 adb push将其移动到设备上的 SD 卡中。剩下的就是启动通讯录,从 SD 卡导入 vCard 文件。

贝壳

adb shell命令提供了一种向设备发送 shell 命令并显示结果或在本地启动交互式 shell 的方法。您可以使用shell命令来自动测试一个设备。这使用了shell input keyeventsendevent命令。Input 是一个应用,它位于设备的/system/bin目录中,可以模拟任何类型的键盘输入。您可以使用以下命令推送文本字符串:

adb shell input text "ANDROID"

要模拟在物理或虚拟键盘上按键,请在输入命令中使用keyevent限定符,并使用一个整数来表示您希望调用的特定键码。键码列表如表 3-3 所示。您可以通过以下方式模拟按下菜单键:

adb shell input keyevent 1

images

images

如果您计划开发任何与电话功能交互的应用(发起电话呼叫;发送或接收 SMS 消息),您必须使用telnet命令连接到仿真器控制台。在 Windows 7 上,默认情况下没有安装 Telnet 程序。幸运的是,它是操作系统的一部分,只需启用即可。要启用 Telnet,您必须打开控制面板并选择程序类别。这将弹出一个类似于图 3-11 的窗口。您也可以通过按 Windows 键并键入单词功能来直接启动 Windows 功能对话框。这将显示一个选项列表,包括标题“控制面板”下的几个选项。选择“打开或关闭 Windows 功能”项将弹出如图 3-12 所示的对话框。

images

图 3-11。 Windows 7 控制面板程序页面

要启用 Telnet 客户端程序,只需在 Windows 功能对话框中单击 Telnet 客户端旁边的复选框。如图图 3-12 所示。如果您希望您的计算机也作为 Telnet 服务器,您可以选中 Telnet 服务器框,尽管出于安全原因,大多数 Windows 防火墙会阻止传入的 Telnet 流量。

images

图 3-12。 Windows 7 功能对话框

假设您已经使用正常的默认值启动了一个 ADB 设备,您可以在 Linux 或 Mac OS X 上使用以下命令从终端窗口或 Windows 的命令提示符启动一个 telnet 会话:

telnet localhost 5554

现在,您可以直接向设备发出命令了。图 3-13 显示了 telnet 会话开始时help命令的输出。

images

图 3-13。在 Linux 上 Telnet 会话到仿真器

模拟传入的 SMS 消息需要如下命令:

sms send 3015551212 "This is a test SMS message from Telnet"

在模拟器上,您应该会看到一条消息。打开消息应用,您应该会看到类似图 3-14 的内容。

images

图 3-14。显示模拟短信的短信应用

为了测试基于位置的应用,您可以使用geo命令,它将发送一个 GPS NMEA 语句或一个简单的 GPS 定位。这两个命令如下所示:

`geo fix -82.411629 28.054553

geo nmea $GPGGA,001431.092,0118.2653,N,10351.1359,E,0,00,,-19.6,M,4.1,M,,0000*5B`

如果除了模拟当前的纬度和经度之外,您还想做任何事情,您将不得不使用NMEA命令。$GPGGA代码代表全球定位系统定位数据。字段从左到右依次是句子标识符($ GPGGA)、时间(00:14:31.092)、纬度、北纬、经度(103 度 51 分 13.59 秒)、卫星数量、水平精度因子(HDOP)、海拔高度、WGS84 椭球面上方的大地水准面高度、自上次 DGPS 更新以来的时间、DGPS 参考站 id 和校验和。

有一个 shell 命令允许您在模拟器上更改日期和时间。如果您正在测试任何基于时间的逻辑,如警报或运行时间应用,这将非常方便:

adb shell date secs

使用这个命令的唯一缺点是,您必须输入自 1970 年 1 月 1 日以来的秒数;也称为 UNIX 纪元。在 Linux 或 Mac OS X 终端提示符下,可以使用以下命令来确定当前时间的秒值:

date +%s

作为 Python 标准库的一部分,Python 语言有许多方便的特性和功能。Python 只用几行代码就能处理时间和日期操作。下面是一个将任何日期和时间转换成正确的秒值的简短脚本:

清单 3-1。 Python 纪元时间转换器

`#!/usr/bin/python

-----------------------------

This script will convert arg1 (Date) and arg2 (Time) to Epoch Seconds

import sys, time, datetime

if len(sys.argv) < 3:
print "Usage pyEpoch YYYY-MM-DD HH:MM"
else:
intime = sys.argv[1] + " " + sys.argv[2]

t = time.strptime(intime, "%Y-%m-%d %H:%M")
t = datetime.datetime(*t[:6])
print "Epoch Seconds:", time.mktime(t.timetuple())`

在谷歌上快速搜索 UNIX 纪元转换器,会出现几个可以转换日期和秒的网站。请注意,执行该命令将会生成一条错误消息,指出settimeofday失败了,但它确实工作了。图 3-15 显示了一个设置了若干时间的 Windows 命令提示符,并使用不带参数的相同日期命令读取时间,以显示当前时间。

images

图 3-15。用 shell 命令设置仿真器日期

您可以使用 ActivityManager 的命令行界面启动模拟器上的任何应用,如下所示:

adb shell am start command

图 3-16 显示了当您在 telnet 提示符下输入 am 时所给出的帮助输出。

images

图 3-16。使用 shell am 命令在模拟器上启动活动

要启动 web 浏览器,您可以使用以下命令:

adb shell am start 'http://www.google.com'

命令adb shell dumpsys提供了关于任何连接的 Android 设备的当前状态的几乎所有信息。如果你在模拟器上运行这个命令,你会得到一个可用子命令的列表,如表 3-4 所示。

表 3-4。dump sys 子命令列表

| `SurfaceFlinger` | `accessibility` | `account` | | :-- | :-- | :-- | | `activity` | `alarm` | `appwidget` | | `audio` | `backup` | `battery` | | `batteryinfo` | `clipboard` | `connectivity` | | `content` | `cpuinfo` | `device_policy` | | `devicestoragemonitor` | `diskstats` | `dropbox` | | `entropy` | `hardware` | `input_method` | | `iphonesubinfo` | `isms` | `location` | | `media.audio_flinger` | `media.audio_policy` | `media.camera` | | `media.player` | `meminfo` | `mount` | | `netstat` | `network_management` | `notification` | | `package` | `permission` | `phone` | | `power` | `search` | `sensor` | | `simphonebook` | `statusbar` | `telephony.registry` | | `throttle` | `uimode` | `usagestats` | | `vibrator` | `wallpaper` | `wifi` | | `window` | | |

对于模拟器,命令adb shell dumpsys battery的输出如下所示:

Current Battery Service state: AC powered: true USB powered: false status: 2 health: 2 present: true level: 50
scale: 100 voltage:0 temperature: 0 technology: Li-ion

另一个有趣的子命令是location。如果您查看模拟器的命令adb shell dumpsys location的输出,您不会看到太多。在真实设备上运行同样的命令,你会看到各种信息。如果您检查命令adb shell dumpsys activity的输出,您会看到一长串关于设备当前活动状态的信息。

您可以通过command adb shell dumpsys package获得由PackageManagerService维护的信息列表。这个命令的输出有许多不同的部分,第一部分是活动解析器表。本节包含 MIME 类型、非数据动作和 MIME 类型动作的列表,以及用于启动特定动作的意图。非数据操作启动不需要任何数据即可启动的活动。其中一个例子就是com.android.contacts.action.LIST_ALL_CONTACTS。从名称上看,这种意图的行为相当明显,但是您可以看到使用该命令的结果:

adb shell am start –a com.android.contacts.action.LIST_ALL_CONTACTS

要通过意图机制启动更复杂的操作,必须指定许多不同的字段。如图 3-16 中的所示,你有许多选项可用,包括-a指定动作、-c指定类别、-d指定数据 URI、-t指定 MIME 类型、-e指定附加项目。您可以使用以下命令启动联系人应用来添加条目:

adb shell am start -a android.intent.action.INSERT -d 'content://contacts/people' -t![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 'vnd.android.cursor.dir/person' -c 'android.intent.category.DEFAULT' -e 'name' 'Paul'![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) -e 'phone' '1112223333'

在这一点上,你可能想参考 Android 开发者文档,并仔细阅读 Intent 主题。理解如何使用意图来启动一个活动将会让你洞察到你需要为你的应用提供什么。虽然您可以使用 ADB 工具从命令行启动几乎任何 Android 活动,但是您也可以以编程方式启动活动。后面的章节将使用这一概念来构建脚本以自动化多个活动。

同样的技术可用于启动 SL4A 来执行脚本,如下所示:

am start -a com.googlecode.android_scripting.action.LAUNCH_FOREGROUND_SCRIPT -n com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher -e com.googlecode.android_scripting.extra.SCRIPT_PATH “/sdcard/sl4a/scripts/hello_world.py"

在下一章中,我将向您展示如何使用 Eclipse 将解决方案自动部署到模拟器或目标设备并启动它。我发现最后一个非常有用的命令将在设备上启动一个私有服务器来启用远程调试:

adb shell am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER -n com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher

日志猫

logcat是 ADB 命令的名称,用于从仿真器或硬件设备转储或抓取当前日志文件。如果您在 Windows 的命令提示符下或者在 Linux 或 Mac OS X 的终端窗口中键入该命令,它将转储当前日志并继续显示新条目,直到您按 Ctrl+C 停止它。该工具将帮助您调试意外停止且没有错误的应用。它也可以从 Eclipse 窗口中获得,您将在下一章中看到。

达尔维克调试监控服务(DDMS)

DDMS 是 ADB 的补充工具,实际上使用 ADB 主机服务器到设备守护进程进行所有通信。如果您在不同的目录中使用 ADB 工具从 SDK 版本升级,这将变得非常明显。图 3-17 显示了当前连接了仿真器和物理设备的 DDMS 用户界面。

images

图 3-17。 DDMS 显示

DDMS 有许多你想了解更多的特色。一个是设备菜单中的文件浏览器工具。要浏览特定设备上的文件,请在 DDMS 应用的左上方窗格中选择该设备,然后打开文件浏览器。请注意,您将无法在普通设备上看到系统区域中的任何文件,因为它们受到保护。如果设备已经被根目录化,您将能够看到这些文件。术语root是指通过使用第三方应用或一些其他非重要方法获得对设备的 root 访问权限。大多数设备都将系统文件区设置为只读,因此用户不能进行任何更改。在仿真设备上允许完全浏览,看起来有点像图 3-18 。这里您可以看到联系人数据库,它实际上是一个 SQLite 数据库文件。

images

图 3-18。 DDMS 文件浏览器

您可以通过选择屏幕截图从 DDMS 设备菜单中截取任何已连接设备的屏幕截图。如果您正在编写关于特定应用的文档,并且需要包含屏幕图像,这个特性就非常方便。图 3-19 显示了一个使用 SL4A 发行版中hello_world.py文件的 DDMS 的示例屏幕截图。

images

图 3-19。 DDMS 截屏示例

在下一章中,我将向您展示这些工具如何直接与 Eclipse 集成,以提供您为 Android 编写、测试、调试和部署任何应用所需的一切。

总结

本章重点介绍了如何完全安装和配置 Android SDK,使其更易于在仿真和真实设备上开发和测试。这里讨论的许多工具和概念将在以后用于帮助简化开发过程,并且在某些情况下,自动化开发过程。下一章将介绍使用 Eclipse 和 Android 开发工具包(ADT)作为主要开发平台。

以下是本章的要点:

  • Android SDK :它真的很大,可能比你想象的要多,但是它确实有很多工具,比如 ADB、设备模拟器和 DDMS,这些都非常有用
  • Linux 命令行:如果你不习惯在 Linux 命令行或 Windows DOS 提示符下输入命令,你可能需要花些时间阅读一下这个主题。许多像 ADB 这样的工具使用命令行来完成任务,您也可以学习如何利用它们。
  • 使用模拟器:模拟器最大的好处是你可以删除它,然后重新开始。模拟器应该可以很好地处理您想要尝试的许多事情。在真实设备上尝试之前,最好在模拟器上测试一些东西。不要害怕删除一个模拟器,并重新开始,如果它变得非常慢或似乎不能正常工作。
  • 使用批处理/脚本文件:我做的第一件事就是在 Windows 上写几个批处理脚本来做一些事情,比如在设备上启动一个私有服务器,将文件复制到正确的目录。如果您正在进行任何数量的编码和测试,您会希望身边有一些这样的脚本。

四、将 Eclipse 用于开发

本章将带您完成 Eclipse 的安装、配置和开发。

images 注意虽然 Eclipse 可以在 Linux、Mac OS X 和 Windows 上工作,但本章将使用 Windows 作为主要的开发环境。

好吧。我们开始吧。以下是我将在本章中介绍的内容:

  • 在开发机器上安装 Eclipse
  • 使用 Eclipse 基础知识
  • 了解 Android 开发工具包
  • 使用 Pydev

通过使用这里给出的说明,您将能够在几分钟内安装好 Eclipse,并为生产工作做好准备。我将带您了解 Eclipse 本身以及许多其他插件的基本安装步骤,以帮助我们开发本书中的所有示例。

在这一点上需要指出的是,Eclipse 从一开始就是作为一个用 Java 语言编写的可扩展的集成开发环境(IDE)来开发的。因为它是用 Java 编写的,所以它自动地成为了一个跨平台的应用,事实上,这是从一开始就有的想法。Eclipse 已经发展成为一个庞大的项目,这一点经常会吓跑一些人。

在开发机器上安装 Eclipse

Eclipse 有多种风格。选择一个适合你的,在很大程度上是一个品味问题。如果你打算用它来完成本书中的练习,你可以下载经典版。在撰写本书时,当前版本是 3.6.1,代号为 Galileo。对于 Linux 和 Windows,您可以选择 32 位或 64 位下载。对于苹果电脑,你可以选择苹果 OS X(碳纤维)、苹果 OS X(可可 32)或苹果 OS X(可可 64)。选择与您的操作系统相匹配的一个。Eclipse 需要安装 Java 才能工作,所以如果你跳过了第二章中关于下载和安装 Java 的部分,你需要现在就回去做。Eclipse 发布在一个单独的.zip文件中,这意味着安装程序所要做的就是解压文件内容。在 Windows Vista 和 Windows 7 上,您可以双击.zip文件,Windows 资源管理器将打开,让您选择提取所有文件,如图 4-1 中的所示。

images

图 4-1。 Eclipse SDK 安装

一旦有了一个解压了所有 Eclipse 文件的目录,您应该能够通过双击Eclipse.exe文件在 Windows 中启动程序。你可能想在桌面上创建一个快捷方式,因为这将是一个经常使用的应用。记住使用右键单击 Create Shortcut Here 方法来确保 Eclipse 能够正常启动。第一次运行 Eclipse 时,你会看到一个类似于图 4-2 中的对话框屏幕,提示你选择存储项目文件的位置。这在 Eclipse 中称为工作区。您可以选择默认目录,如果您选中“将此目录用作默认目录”旁边的框并且不再询问,它将成为永久默认目录。如果不选择这个框,那么每次启动 Eclipse 时,都会提示您选择一个工作区。

images

图 4-2。选择工作区目录对话框

通过选择 Install New Software 选项,在 Eclipse 的 Help 菜单中安装插件模块。这将打开图 4-3 中所示的对话框。每个附加组件通常是从互联网上的默认存储库中安装的。这使得保持最新更新和安装最新版本变得更加容易。要添加新的存储库,请单击标记为“使用”的文本框旁边的“添加”按钮。您也可以在同一个文本框中输入 URL,Eclipse 安装程序将会到那个地址寻找附加包。图 4-3 显示了为 Eclipse 的 Android 开发工具(ADT)插件输入 URL 的结果。如果您使用 Add 按钮,您将有机会命名存储库。ADT 的 URL 是

[dl-ssl.google.com/android/eclipse](https://dl-ssl.google.com/android/eclipse)

您必须选择开发人员工具行下面的单个选项,或者只需选中开发人员工具旁边的复选框即可全选。一旦下载完成,您将被指示重启 Eclipse 以使更改生效。如果您不记得您安装了什么,只需选中“隐藏已安装的项目”旁边的框,您将看到所有新项目。如果您想查看已安装内容的列表,可以单击“已安装”链接。还有一个名为 Available Software Sites 的链接,它会将您带到一个预定义的附加站点列表。

images

图 4-3。 ADT 安装对话框

您想要安装的下一个附加组件是 Pydev。使用 Eclipse 和 Pydev 编写和调试 Python 代码是一种真正的集成开发体验。稍后我将带您了解如何使用 Pydev。现在,再次从帮助菜单打开软件更新屏幕。这次我们将为 Pydev 更新站点输入另一个 URL。它应该如下所示:

[Pydev.org/updates](http://Pydev.org/updates)

图 4-4 显示了进入pydev.org网址后你会看到的对话框。

images

图 4-4。 Pydev 安装对话框

本书练习所需的最后一个 Eclipse 插件是 Aptana Studio。它在编辑 HTML、CSS 和 JavaScript 方面大放异彩。使用与 ADT 和 Pydev 相同的过程,在标签 Work with 旁边的文本框中输入以下行:

[download.aptana.com/tools/studio/plugin/install/studio](http://download.aptana.com/tools/studio/plugin/install/studio)

这将在可用站点列表中创建一个新条目,并显示另一个对话框,您可以在其中选择并下载加载项。

定期检查 Eclipse 和附加组件的更新是一个好主意。您可以通过从“帮助”菜单中选择“检查更新”来完成此操作。如果发现任何更新,您将会看到一个类似于图 4-5 中的对话框。这个特别的更新是针对 Ubuntu 系统上的 Pydev 的。

images

图 4-5。可用更新对话框

Eclipse 基础知识

Eclipse 是一个巨大的应用,具有许多功能和选项。整本书都是关于使用 Eclipse 开发复杂应用的。第一次启动 Eclipse 时,你会看到一个欢迎屏幕,如图 4-6 所示。教程和示例项目是专门针对 Java 开发人员的。尽管阅读它们不会伤害你,但你可能想暂时跳过它们,因为它们并不真正适用于手头的主题。

images

图 4-6。月食欢迎画面

如果单击 Overview 项,将会打开另一个屏幕,其中有四个主题:工作台基础、Java 开发、团队支持和 Eclipse 插件开发。对本书来说,唯一真正重要的是工作台基础知识主题。点击此项将启动 Eclipse 帮助系统,如图图 4-7 所示。如果您展开入门部分,您会发现一个详细的教程,其中涵盖了浏览 Eclipse 需要知道的所有基本操作。如果您以前没有使用过 Eclipse,那么花时间研究一下教程材料是非常值得的。

Eclipse 使用许多基本概念和术语来处理程序的不同功能和操作。工作台是用来标识 Eclipse 应用的整个窗口的术语。工作台窗口通常包含多个带有编辑器和视图的子窗口,例如 Project Explorer。当您打开其他窗口时,它们将包含编辑器或视图。Eclipse 还支持窗口中的多个选项卡,因此您可以在同一个编辑器窗口中打开多个文件。使用帮助菜单可以随时导航回欢迎页面。

images

图 4-7。月食帮助屏幕

视角

从程序使用者的角度来看,视角的概念更容易理解。每个人都会把自己的观点或偏好带到任何活动中。透视图实际上只不过是菜单和窗口在任何时候都打开的个人偏好。Eclipse 对透视图的定义是“一组视图和编辑器(部件)的可视容器”([www.eclipse.org/articles/using-perspectives/PerspectiveArticle.html](http://www.eclipse.org/articles/using-perspectives/PerspectiveArticle.html))。Eclipse 附带了许多已经为典型用途配置的透视图,比如编写代码。当您的活动发生变化时,您会切换到不同的视角,例如当您从编码切换到调试时。图 4-8 显示了一个典型的 Pydev 透视图。

images 注意高分辨率的大屏幕显示器提供了大量的空间来运行多窗口 Eclipse。您还可以在多个监视器上运行 Eclipse,并使用分离特性将单个窗口移出工作区容器,移到不同的监视器上。

images

图 4-8。 Pydev 透视图工作区

视角可以最大程度地定制。如果您不喜欢某个工具栏,可以将其关闭。每个透视图将特定的窗口加载到屏幕上的默认位置。您可以随意移动这些窗口,根据自己的喜好进行排列。要定制当前透视图,右键单击工作区右上角的名称/图标,并选择 customize。您将看到如图图 4-9 所示的屏幕。几个选项已经展开,以显示选择之间的差异。

此对话框使您可以完全控制当前透视图的每个方面。如果该屏幕中某个项目旁边的复选框有复选标记,则意味着该项目下的每个选项也被选中。填充复选框的蓝色方框表示该项下的某些选项已被选中,而某些选项未被选中。如果您展开每一项,您会很快看到 Eclipse 有很多选项。在这一点上,最好的方法是调整那些你认为会帮助你更有效率的事情,剩下的就不要管了。

附加选项卡允许您自定义工具栏可见性、菜单可见性(或显示在主工作区窗口顶部的项目)、命令组可用性以及将显示为子菜单项的快捷方式。“按命令组过滤”复选框将打开或关闭对话框左侧的另一个项目列表,显示出现在不同命令组中的选项。

images

图 4-9。自定义透视屏幕

您可以通过单击右上角带有小加号的看起来像窗口的小图标来快速打开一个新的透视图。它位于屏幕顶部的当前视角指示器旁边。(参见图 4-8 。)这将弹出如图图 4-10 所示的打开透视图对话框,其中列出了所有可用的透视图。单击任何项目都会打开该透视图及其所有窗口和视图。

images

图 4-10。 Eclipse 打开透视图对话框

您可以随时改变主意,通过单击 Cancel 按钮返回到当前打开的透视图。

项目

项目是收集一个特定应用的所有活动部分的地方。一个项目可以仅仅是一个单独的源文件或者任意数量的不同文件和文件类型。使用文件images新建项目或 Alt+Shift+N 组合键创建一个新项目。在 Mac 上,你可以使用 Command+Shift+N。这两种方法都会启动新项目向导(参见图 4-11 )。

选择 General Project 将启动另一个对话框,提示您命名项目,然后在默认工作区目录下创建一个同名的空文件夹。稍后我们将介绍如何创建 Pydev 项目。

images

图 4-11。 Eclipse 新项目向导

Android 开发工具包

Google 已经尽一切努力来简化为 Android 平台开发应用的过程,包括为 Eclipse 提供 Android 开发工具包(ADT)扩展。创建新项目时的一个选项是 Android Project。这是探索在 Android 平台上使用 Java 开发原生应用的好方法。图 4-12 显示了新的 Android 项目向导创建了一个名为 MyTestApp 的新项目,该项目基于样本记事本应用。ADT 附带了许多示例应用,以展示如何利用 Android 平台的不同特性。

images

图 4-12。新 Android 项目向导

当您单击 Finish 按钮时,向导将创建新项目,并将所有示例源代码复制到工作目录中。图 4-13 显示了这个项目在编辑器中的样子。

images

图 4-13。用新的 Android 项目向导创建的 MyTestApp】

此时,您可以单击 Run 图标,Eclipse 将完成编译和部署过程。如果您已经在开发机器上配置了模拟器,它将启动模拟器并将应用推送到设备上。图 4-14 显示了在通用仿真器上运行的应用。这个过程的美妙之处在于使用 Eclipse 和 ADT 可以快速编译、部署和测试序列。目标是让同样的体验成为可能,但是使用 Python 和 SL4A。

images 提示在 Eclipse 中,有多种方法可以完成相同的任务。如果你更喜欢点击鼠标,你会发现你需要执行的任何操作都有图标。如果你更喜欢键盘,你也会发现这些。您可以使用本章前面提到的“自定义透视图”对话框来探索不同的选项。

images

图 4-14。运行在通用仿真器上的 MyTestApp】

其中一个可用的视角被标记为 DDMS。图 4-15 显示了 MyTestApp 在模拟器中运行时的透视图。这个透视图给你提供了在第三章中讨论的 DDMS 工具的完整功能。此透视图打开的窗口包含设备;仿真器控制;Logcat 以及一个选项卡式窗口,可以快速访问线程、堆、分配跟踪器和文件资源管理器。

要查看与 MyTestApp 相关的信息,您必须向下滚动到 Devices 窗口的底部并找到该应用。在这种情况下,它被命名为com.example.android.notepad。当你选择这一行时,你将能够看到你曾经想知道的关于一个 Android 应用的一切――以及更多。

images

图 4-15。【MyTestApp 运行时的 DDMS 视角

在讨论多监视器时,前面提到的一个特性是能够“撕下”任何窗口,并将其拖出主 Eclipse 窗口。图 4-16 显示了仿真器控制窗口被自己移出后的样子。为此,在窗口标题下左键单击并按住;然后将鼠标拖离 Eclipse 主窗口。如果主窗口最大化,这将不起作用。要关闭窗口,只需单击窗口右上角的 X。要在 Eclipse 中重新打开窗口,请打开窗口菜单。然后从显示视图中,选择要打开的窗口。如果您之前将它从 Eclipse 中分离出来,它将恢复脱离主窗口。您可以使用与取消停靠时相同的方法将它拖回主窗口。如果你只是想回到任何视角的默认设置,在窗口菜单上有一个重置视角选项可以让你回到最初的起点。

images

图 4-16。与主 Eclipse 窗口分离的仿真器控制窗口

使用 Pydev

Pydev 是一个 Eclipse 插件,它的创建是为了让 Python 开发人员的生活更加轻松。它有许多专门针对 Python 语言的特性,还有快捷方式、模板和它自己的默认透视图。Pydev 最重要的部分可能是它是由 Python 开发者为 Python 开发者创建的。如果你在任何平台上开发 Python 代码,这是你不想离开家不带的工具之一。

Pydev 工作之前必须做的一件事是配置 Python 安装的位置。图 4-17 显示了从首选项窗口展开的 Pydev 菜单项。您可以在工作区窗口顶部列表的窗口菜单下找到首选项选项。选择解释器- Python 菜单项最初会显示一个空白窗口。这需要配置为指向您的 Python 2.6 解释器安装目录。在 Windows 上,这通常位于路径C:\Python26中。在 Ubuntu 10.10 和大多数其他基于 Debian 的发行版上,你会在/usr/bin/python中找到 Python 可执行文件。

images

图 4-17。配置 Python 解释器的首选项对话框

安装 Pydev 后,此时您有两个选择:新建 Pydev 项目和新建项目。图 4-18 显示了新的 Pydev 项目屏幕的样子。此对话框中有许多必须设置的项目。在对话框的顶部,您必须输入新项目的名称,该名称将用作默认工作区下的子目录的名称,以及将在项目浏览器窗口中显示的标题。接下来,您必须使用下拉框更改语法版本,并选择 2.6。最后,您应该将解释器设置为指向 Python26 目录,以防您碰巧安装了多个版本的 Python。

images

图 4-18。新 Pydev 项目对话框

一旦创建了新项目,您就可以开始输入代码了。Pydev 使用模板来帮助您使用预定义的源代码快速构建代码。您可以使用任何提供的模板或创建自己的模板。您可以在这里放置一些标准的标题注释,包括版权信息、您的姓名以及其他任何可能相关的内容。

如果你想查看所有默认的快捷键,你可以按 Ctrl+Shift+L。这将弹出一个当前定义的所有快捷键的可滚动窗口,如图图 4-19 所示。就像 Eclipse 中的其他东西一样,您可以从 Preferences 对话框中定义自己的快捷键。如果屏幕上有快捷方式列表,您可以通过再次按 Ctrl+Shift+L 直接跳转到该页面。

images

图 4-19。快捷显示窗口示例

Pydev 附带了许多预定义的模板,可以在创建新的源文件时使用,或者快速插入到代码窗口中。当您使用 File images New images Pydev 模块过程创建新的源文件时,您将有机会使用模板文件。这将打开如图图 4-20 所示的对话框。如果打开了编辑器窗口,可以使用 Ctrl+space 组合键访问模板。这将打开一个窗口,其中列出了可供您插入的潜在代码片段。当任何项目高亮显示时,按回车键将在该点插入代码。

如果您第二次按 Ctrl+space,您将看到此时适用的可用模板列表。这将切换当前列表以显示可用的模板,并在第二个窗口中显示如果您点击 Return 将插入的语句的视图。可通过创建新 Python 模块对话框中显示的配置链接创建新模板。单击此链接将打开首选项对话框,并选择模板选项。

images

图 4-20。新 Pydev 模块对话框

要定义一个新模板,点击新建按钮并完成图 4-21 所示的对话框。这将允许您在每个 Python 模块的开头创建相同的基本代码时节省击键次数。“插入变量”按钮允许您根据某些变量(如文件路径、文件或模块名称、日期、时间或当前选定的单词)插入文本。

选择模板时,将插入图案窗口中的文本。在这种情况下,您将看到几乎每个 SL4A 应用开头都会出现的代码。Android 模块包括使用远程过程调用(RPC)机制在 SL4A 应用和底层本机 API 之间进行通信的代码。在这种情况下,RPC 调用与伪代理一起使用,在底层 Android 操作系统之间传递信息。这提供了一个额外的安全层,以防止流氓应用在您的设备上做一些邪恶的事情。

images

图 4-21。 Pydev 新模板对话框

您必须使用 ADB 工具来将 SL4A 应用部署到仿真器或真实设备上。这可以从命令行完成,或者您可以使用少量代码自动完成该过程:

`#!/usr/bin/env python

import subprocess

ADB = r'C:\Program Files (x86)\Android\android-sdk-windows\platform-tools\adb.exe'
APPLICATION = 'hello_world.py'
TARGET = '/sdcard/sl4a/scripts/'

def main():

Upload the application.

subprocess.call([ADB, '-e', 'push', APPLICATION, TARGET + APPLICATION])

Launch the application.

subprocess.call(*'"%s" -e shell am start *
*-a com.googlecode.android_scripting.action.LAUNCH_BACKGROUND_SCRIPT *
*-n *
*com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher *
*-e com.googlecode.android_scripting.extra.SCRIPT_PATH *
"%s%s"' % (ADB, TARGET, APPLICATION))

if name == 'main':
main()`

如果您用这段代码创建一个文件,将它包含在您的 Pydev 项目中,并在准备好测试时运行它,它将为您完成工作。一个好的描述性名字,比如Launch_app.py,会在需要使用的时候帮助你识别它。将它保存到工作区的一个目录中,这样您就可以将它复制到每个项目中。您唯一需要更改的是下面这一行:

APPLICATION = 'hello_world.py'

这显然会因每个应用而异,并且应该与存储在磁盘上的文件名相匹配。Pydev 提供了一个完整的调试工具,允许你设置断点,检查变量,单步调试代码行,如图 4-22 所示。在编辑器窗口中查看代码时,可以使用鼠标右键单击“添加断点”来设置断点。一旦设置了断点,就可以修改其属性并添加条件。当你有一个很长的循环并且你想在一些迭代之后中断时,这是非常宝贵的。现在,当您运行应用时,一旦遇到断点并满足条件,就会提示您。

images

图 4-22。 Pydev 应用调试

在真实或模拟设备上执行 Eclipse 中的代码的另一种方法是使用 ADB 工具来建立设备的代理。这在第三章中已经介绍过了,但是我将在这里再次介绍这些步骤,向您展示如何在 Eclipse 中完成。要在真实设备上实现这一点,您需要使用 USB 电缆连接它,并在设备上配置适当的设置,如启用 USB 调试和启用除仅充电之外的其他设置。接下来,您必须启动 SL4A 并从解释器菜单启动一个服务器。最后,您必须注意从设备上的通知页面分配给服务器的 TCP 端口。最后,但同样重要的是,您必须以管理员权限在命令窗口中键入以下内容:

adb forward tcp:9999 tcp:50681

这指示 ADB 将所有 TCP 流量从本地端口 9999 转发到远程端口 50681。完成所有这些后,您现在应该能够从 Eclipse 运行您的 Python 代码了,所有对 Android API 的调用都将通过代理发送到设备。从 Eclipse 内部运行的好处是,您可以看到 DDMS 中的所有调试信息和所有控制台输出,包括从设备返回的任何错误消息。

我发现一个非常有价值的工具是文件比较工具。要使用它,首先在工作区窗口左侧的 navigator 窗格中选择两个文件,方法是左键单击第一个文件,然后按住 Shift 左键单击第二个文件。接下来,使用鼠标右键操作并选择与images相互比较。这将产生一个双窗格窗口,突出显示任何差异,如图 4-23 所示。

images

图 4-23。文件比较工具

在 Eclipse 中使用多种文件类型

Eclipse 真正擅长的事情之一是管理具有多种文件类型的复杂应用。第九章将详细讨论 SMS Sender 程序,但现在它将作为一个例子,展示如何创建一个包含许多不同文件的项目。这可以从头开始或从现有文件中完成。在这种情况下,我们将使用现有的文件并将它们添加到一个新的 Pydev 项目中。

您必须做的第一件事是创建一个新的 Pydev 项目。这将在您的工作区下创建一个新文件夹和一个单独的src目录。要将文件添加到项目中,您可以使用文件菜单中的导入工具,也可以将文件从文件管理器(如 Windows 上的 Windows 资源管理器或 Linux 上的 Nautilus)拖放到项目中。图 4-24 显示了选择导入方法时将看到的对话框。

images

图 4-24。导入文件工具

当你点击下一步按钮时,你会看到另一个对话框,允许你浏览你想要导入的目录(见图 4-25 )。这应该是顶层或根目录,其中包含您希望导入到该目录或子目录中的所有文件。

images

图 4-25。导入目录选择器对话框

如果您选择拖放方法,您需要在文件管理器中选择文件和目录,然后将它们拖到 Eclipse 窗口上,并在指向项目时释放鼠标。这将弹出一个新的对话框,允许你选择如何处理这些文件(见图 4-26 )。

images

图 4-26。文件和文件夹操作对话框

一旦导入完成,您将拥有一个新的 Pydev 项目。图 4-27 显示了 SMSSender 项目,其中的文件是使用拖放方法包含的。左侧窗格显示了一个目录树,其中包含与此项目相关的所有文件。您可以并且应该在项目中包含所有文件,甚至是那些您不会用 Eclipse 编辑的文件,比如图像,以简化以后的部署过程。构建一个用于分发的包需要所有文件都包含在您的项目中,所以您最好现在就学习如何做。这还包括您的项目所依赖的任何库文件,如 JavaScript 库。

images

图 4-27。使用拖放方法创建的多文件示例

如果您双击项目中的一个非文本文件,Eclipse 将尝试使用默认的查看器应用打开该文件。如果你右击一个文件,你会看到包括打开方式在内的选项。图 4-28 显示了打开的选项菜单。

images

图 4-28。打开文件选项菜单

另一个选项将打开另一个对话框(见图 4-29 ),其中有一长串默认编辑器。如果您选择外部程序单选按钮,您将获得系统上所有已注册 mime 类型的列表。

images

图 4-29。编辑选择菜单

Eclipse 在为文件类型提供正确的编辑器方面做得很好,你可以从图 4-27 中的 HTML 标签看到这一点。

总结

Eclipse 是一个强大的 IDE。它也是一个复杂的软件应用,对于新手来说可能有些令人生畏。本章重点介绍了配置 Eclipse 并使用它快速有效地构建和测试您的应用。希望您已经看到了 Eclipse、Pydev 和 ADT 的组合可以做些什么。

以下是关于 Eclipse 需要记住的一些事情:

  • 不要害怕 Eclipse :许多人因为听说过或读到过 Eclipse 有多么庞大和笨重而回避使用它。虽然它可能在内存有限的旧计算机上运行缓慢,但它在典型的现代工作站上确实运行良好。
  • 阅读文档:如果你以前没有使用过 Eclipse,浏览一下文档也是个不错的主意。至少看看最近的 Eclipse 教程,帮助您快速入门。
  • 找到你的插件:除了本章提到的插件,还有很多插件。如果你认为还有其他需要的东西,浏览一下网站,在谷歌上搜索一下。Aptana Studio 并不是唯一的 HTML 编辑器,所以您可以随意查看其他一些选项。
  • 学习快捷键:学习几个键盘快捷键可以节省你很多时间,有助于让你的手保持在键盘上。每次你不得不移动你的手到鼠标上,不仅花费时间,而且潜在地增加了你的疲劳系数。

五、探索 Android API

本章将深入研究 Android 应用编程接口(API ),并展示如何使用 Python 调用不同的函数。

images 注意 Python 无法通过 SL4A 的 r3 版本访问每一个 Android API。其他语言如 BeanShell 确实有这种能力,并且将用于查看一些缺失的功能。

本章将在很大程度上依赖于对第一章中介绍的概念的基本理解。主题包括 Android 活动和意图、JSON、Python 类型和 RPC。我将带您浏览每个 API facade,并在适当的地方包括如何使用它们的例子。它们都有自己的描述性名称,比如cameraCapturePicturerecognizeSpeech

所有与 Android API 的双向通信都使用 JSON 作为传递数据的底层结构。如果你跳过了 JSON 的部分,你可能想回到第一章并阅读这一部分。JSON 并不复杂,但是如果您不知道自己在看什么,它可能会有些混乱。Python 很好地处理了 JSON,甚至有一个内置的过程pprint,可以很好地打印 JSON 结构。图 5-1 显示了不使用 pretty print 例程时 API 调用getLaunchableApplications的返回结果。

images

图 5-1。getLaunchableApplications API 调用的 JSON 返回示例

图 5-2 显示了相同的结果,但是使用了pprint模块,以一种更加易读的形式。

images

图 5-2。使用 pprint 格式化的 getLaunchableApplications JSON 示例

另一个概念,我假设你在这一点上理解是一个 Android 活动。SL4A 提供了一个启动并忘记(或启动并等待)Android 活动完成的接口。

探索 Android APIs

在 Python 中,所有 SL4A API 调用都返回一个包含三个字段的对象:

  • id:与 API 调用相关的严格递增的数字 ID
  • result:API 调用的返回值,如果没有返回值则为null
  • error:对发生的任何错误的描述,如果没有发生错误,则为null

android.py文件用三种方法定义了一个 Android 类。通过研究_rpc方法,可以了解 API 请求是如何使用 RPC 调用和 JSON 传递给底层操作系统的:

`def _rpc(self, method, *args):
data = {'id': self.id,
'method': method,
'params': args}
request = json.dumps(data)
self.client.write(request+'\n')
self.client.flush()
response = self.client.readline()
self.id += 1
result = json.loads(response)
if result['error'] is not None:
print result['error']

namedtuple doesn't work with unicode keys.

return Result(id=result['id'], result=result['result'],
error=result['error'], )`

同样的基本概念也适用于其他语言。在 BeanShell 中,代码如下所示:

call(String method, JSONArray params) { JSONObject request = new JSONObject(); request.put("id", id); request.put("method", method); request.put("params", params); out.write(request.toString() + "\n"); out.flush(); String data = in.readLine(); if (data == null) { return null; } return new JSONObject(data); }

安卓外观

第一章讨论了 SL4A 用来向底层 Android API 传递信息的 RPC 机制的基础知识。每个支持的 API 函数在每种 SL4A 语言中都有一个对应的接口,称为 facade ,带有 API 所需的适当参数。这些参数中有些是强制性的,有些是可选的。表 5-1 显示了顶层立面以及它们提供的访问功能。附录 A 包含所有 SL4A API 调用的完整列表。

表 5-1。安卓 API 外观

| `ActivityResultFacade` | 设置活动的返回值 | | `AndroidFacade` | 常见的 Android 功能 | | `ApplicationManagerFacade` | 获取有关已安装应用的信息 | | `BatteryManagerFacade` | 公开电池管理器 API | | `BluetoothFacade` | 允许访问蓝牙功能 | | `CameraFacade` | 所有与摄像机相关的操作 | | `CommonIntentsFacade` | 通用 Android 意图 | | `ContactsFacade` | 提供对联系人相关功能的访问 | | `EventFacade` | 公开作为 RPC 从事件队列读取的功能和作为纯 Java 函数写入事件队列的功能 | | `EyesFreeFacade` | 为 API 3 或更低版本提供文本到语音(TTS)服务 | | `LocationFacade` | 暴露与`LocationManager`相关的功能 | | `MediaPlayerFacade` | 展示基本的`mediaPlayer`功能 | | `MediaRecorderFacade` | 记录媒体 | | `PhoneFacade` | 暴露`TelephonyManager`功能 | | `PreferencesFacade` | 允许访问`Preferences`界面 | | `SensorManagerFacade` | 暴露与`SensorManager`相关的功能 | | `SettingsFacade` | 公开电话设置功能 | | `SignalStrengthFacade` | 显示信号强度功能 | | `SmsFacade` | 提供对 SMS 相关功能的访问 | | `SpeechRecognitionFacade` | 包含与 Android 语音转文本功能相关的 RPC 实现 | | `TextToSpeechFacade` | 为 API 4 或更高版本提供 TTS 服务 | | `ToneGeneratorFacade` | 产生双音多频音 | | `UiFacade` | 创建和处理对话框中的信息 | | `WakeLockFacade` | 展示了`PowerManager`的一些功能(特别是唤醒锁) | | `WebCamFacade` | 从前置摄像头捕捉视频 | | `WifiFacade` | 管理 WiFi 无线电的所有方面 |

活动结果面

这个 facade 提供了一种机制来显式地设置脚本如何将信息作为活动返回。每当使用 Android API 调用startActivityForResult()启动脚本 APK 时都会用到它。使用这种方法意味着您的脚本将返回某种结果,设置结果的类型(resultValue)和RESULT_CANCELED (0)或RESULT_OK (-1)是很重要的。

雄激素

这个外观有点包罗万象,提供了 Android 操作系统(OS)的许多可用功能。有检查当前执行包版本号(getPackageVersiongetPackageVersionCode)和 SL4A 版本(requiredVersion)的功能。第二个提供了一个很好的机制,在您的代码需要一些特定于版本的特性时,可以检查 SL4A 的最低版本。

这个 facade 里有几个弃用的调用,包括getInputgetPassword。两者都被更新的 Android API 调用所取代,但是仍然支持旧的脚本。图 5-3 显示了如果你使用一个废弃的 API 调用,你会看到什么。

images

图 5-3。不推荐使用的 API 调用的通知消息

每当您使用不推荐使用的函数时,SL4A 都会在您的通知窗口中添加一条消息。您将在这里找到启动 Android 活动并等待结果的函数,或者只是启动并返回。像 Windows 和 Linux 一样,Android 支持剪贴板的概念,用于在应用之间复制和粘贴信息。您可以使用函数getClipboardsetClipboard functionsfunction 从脚本中完成此操作。

lognotify功能提供了显示(notify)或保存(log)信息的方法,以便使用 logcat 应用查看。还有一个常用的makeToast功能,它只是在设备屏幕上短暂闪烁一条信息,然后删除它。如果您希望通过振动设备引起用户的注意,您可以使用vibrate功能。sendEmail功能将启动一个发送电子邮件的活动(该活动将取决于您在设备上加载的能够发送电子邮件的应用)并填充recipientsubjectbodyattachment字段。您将不得不使用该应用来实际发送消息。在下一章中,我将向您展示另一种不需要外部活动就能发送电子邮件的方法。

在引言中,我解释了 Android 架构,以及活动如何适应不同应用的执行。SL4A 的早期版本包括启动活动(startActivity)和启动活动并等待结果(startActivityForResult)的能力。SL4A r4 引入了两个额外的函数,允许您使用意图(startActivityIntent)启动活动,以及使用结果意图(startActivityForResultIntent)启动活动。SLA r4 中的另一个新函数调用是makeIntent。需要此函数来创建一个 intent,供需要 intent 的任何一个startActivity调用使用。这个函数的返回是一个表示意图的对象。

SL4A r4 还在 Android facade 中引入了getConstants函数,帮助您确定特定 Android 类中有哪些常量可用。当您想要查询内容供应器,但是不知道有什么可用时,这个函数非常方便。下面的一行代码演示了如何使用该调用来显示联系人提供程序中可用的常量:

res=droid.getConstants("android.provider.ContactsContract$CommonDataKinds$Phone").result

Android 2.2 将从联系人提供程序返回总共 99 个常量。以下是一些常量的简短列表:

{u'AGGREGATION_MODE': u'aggregation_mode', u'AVAILABLE': 5, u'AWAY': 2, u'CONTACT_ID': u'contact_id', u'CONTACT_PRESENCE': u'contact_presence', u'CONTACT_STATUS': u'contact_status', u'CONTACT_STATUS_ICON': u'contact_status_icon', u'CONTACT_STATUS_LABEL': u'contact_status_label', u'CONTACT_STATUS_RES_PACKAGE': u'contact_status_res_package', u'CONTACT_STATUS_TIMESTAMP': u'contact_status_ts', u'CONTENT_FILTER_URI': u'content://com.android.contacts/data/phones/filter', u'CONTENT_ITEM_TYPE': u'vnd.android.cursor.item/phone_v2', u'CONTENT_TYPE': u'vnd.android.cursor.dir/phone_v2', u'CONTENT_URI': u'content://com.android.contacts/data/phones', u'CUSTOM_RINGTONE': u'custom_ringtone', u'DATA': u'data1', u'DATA1': u'data1', u'DATA2': u'data2', u'DATA_VERSION': u'data_version', u'DELETED': u'deleted', u'DISPLAY_NAME': u'display_name', u'DISPLAY_NAME_ALTERNATIVE': u'display_name_alt', u'DISPLAY_NAME_PRIMARY': u'display_name', u'DISPLAY_NAME_SOURCE': u'display_name_source', u'DO_NOT_DISTURB': 4, u'HAS_PHONE_NUMBER': u'has_phone_number', u'IDLE': 3, u'INVISIBLE': 1, u'IN_VISIBLE_GROUP': u'in_visible_group', u'IS_PRIMARY': u'is_primary', u'LAST_TIME_CONTACTED': u'last_time_contacted', u'LOOKUP_KEY': u'lookup', u'MIMETYPE': u'mimetype', u'NAME_RAW_CONTACT_ID': u'name_raw_contact_id', u'NAME_VERIFIED': u'name_verified', u'NUMBER': u'data1', u'_COUNT': u'_count', u'_ID': u'_id'}

同样的getConstants函数可以用来获得android.content.Intent中所有可用常数的列表。这将包括所有标准的 Android 意图。下面是将列表打印到控制台的一小段代码:

import android droid = android.Android() myconst = droid.getConstants("android.content.Intent").result for c in myconst: print c,"=",myconst[c]

运行此代码的结果将产生一个格式良好的列表,如下所示:

ACTION_AIRPLANE_MODE_CHANGED = android.intent.action.AIRPLANE_MODE ACTION_ALARM_CHANGED = android.intent.action.ALARM_CHANGED ACTION_ALL_APPS = android.intent.action.ALL_APPS ACTION_ANSWER = android.intent.action.ANSWER ACTION_APP_ERROR = android.intent.action.APP_ERROR ACTION_ATTACH_DATA = android.intent.action.ATTACH_DATA ACTION_BATTERY_CHANGED = android.intent.action.BATTERY_CHANGED ACTION_BATTERY_LOW = android.intent.action.BATTERY_LOW ACTION_BATTERY_OKAY = android.intent.action.BATTERY_OKAY ACTION_BOOT_COMPLETED = android.intent.action.BOOT_COMPLETED ACTION_BROADCAST_KEYEVENT = android.intent.action.BROADCAST_KEYEVENT ACTION_BROADCAST_MOTIONEVENT = android.intent.action.BROADCAST_MOTIONEVENT ACTION_BROADCAST_TRACKBALLEVENT = android.intent.action.BROADCAST_TRACKBALLEVENT ACTION_BUG_REPORT = android.intent.action.BUG_REPORT ACTION_CALL = android.intent.action.CALL ACTION_CALL_BUTTON = android.intent.action.CALL_BUTTON ACTION_CALL_EMERGENCY = android.intent.action.CALL_EMERGENCY ACTION_CALL_PRIVILEGED = android.intent.action.CALL_PRIVILEGED ACTION_CAMERA_BUTTON = android.intent.action.CAMERA_BUTTON ACTION_CHECK_CONTACT_DB_CORRUPT = android.intent.action.ACTION_CHECK_CONTACT_DB_CORRUPT ACTION_CHOOSER = android.intent.action.CHOOSER ACTION_CLOSE_SYSTEM_DIALOGS = android.intent.action.CLOSE_SYSTEM_DIALOGS ACTION_CONFIGURATION_CHANGED = android.intent.action.CONFIGURATION_CHANGED ACTION_CONTACTS_CHANGE = anddroid.intent.action.CONTACTS_CHANGE ACTION_CONTACTS_DB_READY = android.intent.action.CONTACTS_DB_READY ACTION_CONTACT_DATABASE_CORRUPT = android.intent.action.CONTACT_DB_CORRUPT ACTION_CREATE_SHORTCUT = android.intent.action.CREATE_SHORTCUT ACTION_DATE_CHANGED = android.intent.action.DATE_CHANGED ACTION_DEFAULT = android.intent.action.VIEW ACTION_DELETE = android.intent.action.DELETE ACTION_DELETE_THREAD_MSG = android.intent.action.DELETE_THREAD_MSG ACTION_DEVICE_STORAGE_LOW = android.intent.action.DEVICE_STORAGE_LOW ACTION_DEVICE_STORAGE_OK = android.intent.action.DEVICE_STORAGE_OK ACTION_DIAL = android.intent.action.DIAL ACTION_DIALER_NEED_CHANGE = android.intent.action.DIALER_NEED_CHANGE ACTION_DOCK_EVENT = android.intent.action.DOCK_EVENT ACTION_EDIT = android.intent.action.EDIT

有了这些信息,你就可以使用makeIntent功能和startActivityForResultIntent来访问隐藏在 Android 操作系统深处的几乎任何功能。这里有一小段使用这种技术来显示您的通话记录:

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

注意,这段代码首先使用getConstants函数来确定CONTENT_URI的值,然后使用queryContent(ContactsFacade的一部分)调用来实际返回结果。

应用管理学院

这个 facade 中的四个函数可以列出所有可用的和正在运行的包,启动一个活动,或者强制停止一个包。您可以使用这些调用来编写自己的任务管理器或终止一组特定的包。请注意,getLaunchableApplications调用可能需要一段时间返回结果,这取决于您在设备上加载的应用数量。图 5-1 显示了原始 JSON 格式的部分应用列表,而图 5-2 显示了使用pprint函数格式化的相同列表。

电池组管理学院

任何与你的设备电池有关的东西都在这里。这个门面是一个谈论监控概念的好地方。在许多其他情况下,为了收集有意义的数据,您必须启动和停止对某种类型信息的监控。图 5-4 展示了一个使用这些 API 调用的交互式会话的例子。

这也是指出每个 API 调用返回的信息中的一些差异的好地方。Python IDLE 工具使得从工作站键盘舒适地探索不同的调用变得非常容易。这是假设您已经在设备上启动了 SL4A,启动了一个服务器,并使用 ADB 连接到它(如果这些都没有意义,请参见第二章)。在接下来的例子中,你会看到三个箭头,如>>>所示,表示来自 IDLE 的提示。如果您想自己尝试代码,请不要键入这些内容。

正如第一章中提到的,Python 中的一切都是对象。API 调用的每个返回都是一个结果对象。如果您检查来自_rpc方法的最后一行,您将看到以下内容:

return Result(id=result['id'], result=result['result'], error=result['error'], )

要访问 Python 调用的结果,可以将它赋给一个变量,然后计算结果,如下所示:

`>>> apps = droid.getLaunchableApplications()

pprint.pprint(apps.result)`

要确定 Python 中对象的类型,可以使用type()函数,如下所示:

`>>> type(apps)
<class 'android.Result'>

type(apps.result)
<type 'dict'>`

这表示apps是从类android.Result派生的对象。下面一行显示了apps.result的类型是dict,这在 Python 中实质上是一个键/值对。在 Java 中,这将被表示为一个Map对象。图 5-4 显示了检查不同电池管理 API 调用返回的结果。

images

图 5-4。电池管理 API 调用示例

蓝星脸

Android 设备拥有广泛的蓝牙功能,这可能是你在移动设备上想不到的。BluetoothFacade提供对所有这些功能的访问,从基本的连接特性到发送和接收 ASCII 和二进制数据。最简单的层次是用于控制连接的bluetoothAcceptbluetoothConnectbluetoothMakeDiscoverablebluetoothStop。您还可以使用checkBluetoothStatetoggleBluetoothState来简单地打开和关闭蓝牙无线电,或者只是检查它处于什么状态。虽然toggleBluetoothState功能听起来像是简单地翻转蓝牙无线电的当前状态,但它实际上会用一个可选参数将其设置为您想要的状态。默认情况下,这将在设备上弹出一个请求许可的屏幕,如图图 5-5 所示。

images

图 5-5。蓝牙 API 提示权限

BluetoothFacade还支持与设备之间的数据传输。这里的选项包括发送/接收 ASCII 字符的bluetoothReadbluetoothWrite。还有一个bluetoothReadLine可以读取整行文本。发送和接收二进制数据有bluetoothWriteBinarybluetoothReadBinary。这两个功能使得使用蓝牙向/从您的设备传输二进制文件成为可能。

照相机

当你从一个剧本中拍摄一张照片时,你基本上有两种选择。你可以抓拍相机当前正在拍摄的任何东西(cameraCapturePicture),或者启动图像捕捉应用(cameraInteractiveCapturePicture)。这是严格用于使用设备背面的镜头。对于带有前置摄像头的设备,有WebCamFacade。应该注意,这两个 API 调用都要求您在设备上传递一个路径来存储图像。如果您不熟悉如何访问不同的目录,您应该花些时间浏览您的设备。在大多数设备上,通常会有一个名为sdcard的可移动设备。安卓相机应用在/sdcard/DCIM/100Media存储图片。

顺便说一下,你应该知道 Android 有一个媒体扫描仪应用,它可以查找特定的文件类型,例如图片的.jpg,并将这些图像添加到默认应用的浏览列表中,例如 Gallery。如果您不希望这种情况发生,您可以使用带有前导句点的隐藏目录,例如/sdcard/.donotscan。你也可以添加一个名为.nomedia,的文件,Android 应该会忽略该目录中的媒体文件。

普通意向下降

Android 操作系统的 2.x 版本通过CommonIntentsFacade提供了一组通用意图。对于扫描条形码,有scanBarcode功能。底层代码将试图解释您正在扫描的内容,然后将其作为结果呈现出来。为了测试这个功能,我使用了几行 Python 代码来启动条形码扫描仪,然后用它的二维码指向 SL4A 主页,以下载.apk文件。这是我得到的:

`>>> import android

droid = android.Android()
res = droid.scanBarcode()
res.result
{u'extras': {u'SCAN_RESULT': u'http://android-scripting.googlecode.com/files/sl4a_r3.apk',images
u'SCAN_RESULT_FORMAT': u'QR_CODE'}}`

接下来是search API 函数。您可以使用通用字符串调用此 API 函数,如下所示:

>>> search('pizza')

接下来会发生什么取决于您的设备上有多少不同的应用能够执行搜索。图 5-6 显示了典型安卓手机上的几个选项。你可以选择一个应用并将其设为默认,但它可能不会给你想要的结果。调用pick函数显示基于作为参数传递的统一资源标识符(URI)选择的内容。您可以使用它来显示具有以下代码的联系人列表:

`>>> import android

droid = android.Android()
droid.pick('u'content://contacts/people')`

view API 函数基于作为参数传递的 URI 启动一个查看动作。这个函数还接受两个可选参数:type是一个表示 URI MIME 类型的字符串,extras是一个 JSON 对象,包含意图所需的任何额外信息的映射。理解如何使用 API 调用需要了解一些意图和 URIs。基础知识包含在第二章中,尽管重温一下谷歌 Android 开发者网站也无妨。

如果你只是想启动联系人应用,那么使用viewContacts。这个函数使用启动活动调用来简单地启动应用,然后返回给调用者。如果您的设备上碰巧存储了任何 HTML 内容,您可以使用viewHtml功能来显示它。它需要一个完整的文件路径作为单个参数。要在地图上搜索某样东西,请使用带有字符串参数的viewMap函数,该参数包含您要查找的内容。这将启动地图应用,搜索栏包含您的搜索字符串。

images

图 5-6。搜索 API 函数的结果

联系

这个外观让您可以访问任何与联系人有关的东西。如果你只是想得到一个包含你设备上所有联系人的大列表,使用contactsGet。您可能希望首先使用contactsGetAttributes呼叫来找出每个联系人的可用信息。随着谷歌改进他们的产品,这个列表可能会随着时间的推移而改变。在检索整个列表之前,您可能想使用的另一个调用是contactsGetCount。这只是返回存储在设备上的联系人总数。

如果您需要选择一个特定的联系人用于其他操作,请使用pickContact功能。这将启动 People 应用,并显示搜索框和键盘。请注意,它返回指向所选联系人的意图。如果你只需要一个联系人的电话号码,你可以使用pickPhone功能。这将像以前一样显示联系人列表,但只在您选择姓名后给出相关的电话号码。该函数返回作为结果一部分的电话号码。这里最后两个函数是contactsGetByIdcontactsGetIds。这两者结合在一起,允许您只使用 ID 来选择特定的联系人。图 5-7 显示了这些功能中的一部分。

images

图 5-7。不同联系人 API 函数的结果

SL4A r4 为 contacts facade 引入了一个新的queryContent函数。这个函数总共有五个参数,为了完全定义您希望查询返回的内容,您需要传递它。第一个参数是您希望查询的内容供应器的 URI。对于联系人数据库,这将是content://com.android.contacts/data/phones。其余的参数是可选的,但是如果你想使用默认值,你必须传递关键字'None'。第二个参数是数据库中您希望返回的列的列表。第三个参数是一个选择过滤器,用于选择要从数据库返回的特定行。最后两个参数是selectionArgsorder

下面的一小段代码展示了如何使用这个函数:

import android droid = android.Android() contacts = droid.queryContent('content://com.android.contacts/data/phones',\ ['display_name','data1'],None,None,None).result for c in contacts: print c

您还需要安装至少PythonForAndroid_r6.apk来运行这个例子。也就是说,您应该会看到该代码片段的输出,如下所示:

[{u'data1': u'321-555-1212', u'display_name': u'John Doe'}, {u'data1': u'321-555-1212', u'display_name': u'Jane Doe'}, {u'data1': u'321-555-1212', u'display_name': u'Jed Doe'}, {u'data1': u'321-555-1234', u'display_name': u'John Smith'}, {u'data1': u'321-555-1234', u'display_name': u'Jane Smith'}, {u'data1': u'321-555-1234', u'display_name': u'Jill Smith'}, {u'data1': u'800-555-1212', u'display_name': u'Toll Free'}]

活动能力

Android 操作系统保留了一个事件队列,用于在应用之间异步传递信息。这个外观让您可以访问操作 Android 事件的函数。如果你只是想清除事件缓冲区,只需调用eventClearBuffer。要在队列中添加或删除事件,应该使用eventPosteventPoll。为了等待一个事件,使用带有参数eventNameeventWaitFor,但是要注意这将阻止进一步的执行,直到指定的事件发生。

使用 SL4A API wiki 中的示例代码可以看到一个事件示例。我更新了代码以使用 SL4A r4 函数,如下所示:

import android, time droid = android.Android() droid.startSensingTimed(1,1000) e = droid.eventPoll(1).result event_entry_number = 0 x = e[event_entry_number]['data']['xforce']

函数是实现模态对话框的一种方式,我将在后面的章节中演示。如果您正在构建一个多线程的应用,它也可以作为一个跨线程的通信媒介。

裸眼立面

这个 facade 为 API 3 或更低版本提供 TTS 服务。这里唯一的函数是ttsSpeak,它使用 TTS 功能输出传递的字符串。

定位面

通过 GPS 或使用有关你当前使用的手机信号塔的信息,功能可以让你随时知道你在哪里。如果您使用getLastKnownLocation功能,您将获得的信息可能是最新的,也可能不是。为了确保获得相关信息,您必须调用startLocating调用。随后调用readLocation,您应该会看到如下结果:

Result(id=6, result={u'network': {u'altitude': 0, u'provider': u'network', u'longitude':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) -84.480000000000004, u'time': 1296595452577L, u'latitude': 31.392499999999998, u'speed':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) 0, u'accuracy': 1000}}, error=None)

startLocating将仅使用当前启用的位置资源。这意味着除非您已经启用了 GPS,否则您将无法获得 GPS 定位。使用stopLocating呼叫停止收集位置数据。这个 facade 中最后一个可用的函数是geocode,您可以将它与readLocationgetLastKnownLocation结合使用,以获得给定纬度和经度的地址列表。

将先前的位置输入到geocode会返回以下内容:

Result(id=7, result={u'locality': u'Milford', u'sub_admin_area': u'Baker', u'admin_area':![imagesu'Georgia', u'feature_name': u'Milford', u'country_code': u'US', u'country_name':![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) u'United States'}], error=None)

请记住,此功能需要活跃的互联网连接来进行实际查找。

中型师学院

如果您想播放音乐或视频内容,可以使用这个外观。SL4A r4 使这个函数成为 API 的正式部分。可用的功能提供了打开、关闭、播放、暂停和查找媒体文件中某个位置的方法。功能mediaIsPlayingmediaPlayInfomediaPlayList提供关于媒体播放器当前状态的信息。请记住,这些功能实际上并不操作媒体播放器应用;他们推出了媒体播放器服务。如果您想启动媒体播放器,可以使用startActivity来启动。

函数名很明显:mediaPlaymediaPlayClosemediaPlayPausemediaPlaySeekMediaPlayStart。您将需要使用mediaPlay来实际加载由 URL 指定的媒体资源。如果你正在创建一个类似背景噪声播放器的东西,函数mediaPlaySetLooping是最后一个你可能会觉得方便的函数。

中间商唱片公司

MediaRecorder facade 让您可以访问音频和视频录制功能。您必须为输出文件提供有效的路径,否则调用将失败。如果您只是想启动视频捕捉应用,请使用startInteractiveVideoRecording功能。要开始录音,使用recorderStartMicrophone功能。对于录像,使用recorderCaptureVideo功能。如果您使用其中任何一个,您必须显式调用recorderStop来结束先前开始的记录。

PhoneFacade

每部 Android 手机都可以通过编程实现基本的手机操作。这个外观还包括许多特定于网络的功能。如果设备上没有可用的功能,有些功能会直接返回,没有任何信息。这种行为的一个例子是getCellLocation调用。如果你在 CDMA 手机上打这个电话,结果将是一无所获。如果要监控手机状态,必须先调用startTrackingPhoneState功能。readPhoneState函数返回当前状态以及任何来电的电话号码。

打电话有两种基本方式。首先是phoneCallNumberphoneDialNumber函数。这些函数将字符串形式的电话号码作为唯一的参数。两者的区别在于,phoneCallNumber函数将实际发出呼叫;phoneDialNumber将使用您传递的号码打开电话拨号器,就像在键盘上输入一样。

您也可以使用phoneCallphoneDial功能用 URI 弦拨号。您可以传递由pickContact函数返回的意图,它将调用该联系人的主要号码。要在 Python 中做到这一点,您需要提取由pickContact函数返回的意图。在 Python 中,应该是这样的:

cont = droid.pickContact() droid.phoneDial(cont[1]['data'])

偏好

如果您希望构建一个有自己偏好设置的应用,您将需要这个 facade。SL4A r4 版本支持三个功能:prefGetAllprefGetValueprefPutValue。默认情况下,这三者都在共享首选项存储上运行,其中包含使用情况跟踪首选项。以下是您将从 IDLE 中看到的内容:

`>>> import android

droid = android.Android()
pref = droid.prefGetAll()
pref
Result(id=0, result={u'usagetracking': False, u'present_usagetracking': False}, error=None)`

要创建自己的首选项文件,您需要添加一个文件名作为参数传递给GetPut例程,如下所示:

`>>> droid.prefPutValue('GPSTracking', True, 'myprefs')
Result(id=7, result=None, error=None)

droid.prefGetValue('GPSTracking','myprefs')
Result(id=9, result=True, error=None)`

传感器管理人员的能力

每个 Android 设备都有一个或多个传感器可供应用使用。至少,有一个加速度计来确定屏幕的方向。SensorManager facade 提供了对 Android 当前支持的所有传感器的访问。这也是另一种需要你开始和停止感知过程的门面类型,因为这发生在后台。要开始和停止感应,使用startSensingstopSensing功能调用。一旦您开始感测并等待一段时间以允许收集传感器数据,数据将可用。

最高层是readSensors函数调用。以下示例显示了此函数返回的数据:

`>>> res = droid.readSensors()

import pprint
pprint.pprint(res.result)
{u'accuracy': 3,
u'azimuth': -2.734636402130127,
u'pitch': -1.0204463958740235,
u'roll': 0.034272377938032152,
u'time': 1296683466.802,
u'xforce': -0.14982382999999999,
u'xmag': 13.75,
u'yforce': 8.6625409999999992,
u'ymag': -38.4375,
u'zforce': 5.3664170000000002,
u'zmag': 15.375}`

有单独的函数调用返回特定的信息:sensorsGetAccuracysensorsGetLightsensorsReadAccelerometersensorsReadMagnetometersensorsReadOrientation。调用这些函数的结果如下所示:

`>>> droid.sensorsGetAccuracy()
Result(id=7, result=3, error=None)

droid.sensorsGetLight()
Result(id=8, result=None, error=None)
droid.sensorsReadAccelerometer()
Result(id=9, result=[-0.14982382999999999, 8.7306430000000006, 5.4345189999999999],images
error=None)
droid.sensorsReadMagnetometer() Result(id=10, result=[11.25, -37.6875, 13.3125], error=None)
droid.sensorsReadOrientation()
Result(id=11, result=[-2.7596172332763671, -1.0129913330078124, 0.035179258137941358], images
error=None)`

加速度计和磁力计函数返回 X、Y 和 z 值列表。方向返回方位角、俯仰角和滚动角列表。在 SL4A r4 中,startSensing功能已被弃用,代之以startSensingThresholdstartSensingTimed。在许多情况下,当您需要使用传感器时,您要么希望根据时间来检测运动,要么希望设备跨越某个运动阈值。startSensingThreshold功能允许您在方位、运动(加速度计)、方向(磁力计)或光线超过特定阈值时,将传感器事件记录到事件队列中。如果您希望使用多个传感器,您必须多次调用startSensingThreshold来启用每个传感器的特定阈值。startSensingTimed函数采用两个参数来确定要记录哪个传感器(1 = all,2 =加速度计,3 =磁力计,4 =光),以及一个delayTime(以毫秒为单位)参数来指定读数之间的时间间隔。

集合完毕

这种外观让您可以访问手机上的所有不同设置:铃声音量、屏幕亮度等。当你考虑为你的设备编写脚本时,这可能是更有用的外观之一。后面的章节将使用这些函数调用来演示 SL4A 的强大功能。现在,让我们来看看有什么可用的。

有三个函数调用简单地检查某个东西的状态。这些包括checkAirplaneModecheckRingerSilentModecheckScreenOn。这三个函数都返回一个布尔值,表明模式是开(True)还是关(False)。要知道checkScreenOn至少要求 API 等级 7 以上(安卓 2.1)。要更改AirplaneModeRingerSilentMode,您可以使用toggleAirplaneModetoggleRingerSilentMode。这些函数类似于其他切换函数,因为您可以通过传递可选参数来显式设置模式。返回的结果将反映设备的当前状态。还有一个toggleVibrateMode来设置设备只有在铃声启用时才振动,否则在收到新通知时振动。

其余的函数要么获取特定的设置,要么为某个设置设置一个值。为了得到一个值,你使用getMaxMediaVolumegetMaxRingerVolumegetMediaVolumegetRingerVolumegetScreenBrightnessgetScreenTimeoutgetVibrateMode。要设置数值,应该使用setMediaVolumesetRingerVolumesetScreenBrightnesssetScreenTimeout

信号强度外观

如果你想知道或显示你的信号有多好,你应该使用这个门面。首先,您必须调用startTrackingSignalStrengths函数来开始收集数据。接下来,您应该调用 readSignalStrengths 来实际读取数据。它将返回如下内容:

>>> droid.readSignalStrengths().result {u'cdma_ecio': -70, u'evdo_dbm': -98, u'cdma_dbm': -97, u'evdo_ecio': -1515,![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) u'gsm_signal_strength': 99, u'gsm_bit_error_rate': -1}

一旦你完成了,你应该发出stopTrackingSignalStrengths来关闭这个进程。

smsf acad

这个外观允许您操作手机上存储的 SMS 消息。它有许多删除、阅读、标记和发送短信的功能。短信是谷歌决定让属性更灵活的另一个领域。smsGetAttributes函数返回当前定义的可用属性列表。使用 Python 和pprint函数将显示以下内容:

>>> pprint.pprint(droid.smsGetAttributes().result) [u'_id', u'thread_id', u'toa', u'address', u'person', u'date', u'protocol', u'read', u'status', u'type', u'reply_path_present', u'subject', u'body', u'sc_toa', u'report_date', u'service_center', u'locked', u'index_on_sim', u'callback_number', u'priority', u'htc_category', u'cs_timestamp', u'cs_id', u'cs_synced', u'error_code', u'seen']

如果您想知道设备上当前存储了多少条短信,请使用smsGetMessageCount。该函数有一个必需的布尔参数,用于指示您是希望统计未读邮件还是所有邮件。如果你没有给它传递一个参数,你会得到这样一个错误信息:

>>> droid.smsGetMessageCount() com.googlecode.android_scripting.rpc.RpcError: Argument 1 is not present Result(id=24, result=None, error=u'com.googlecode.android_scripting.rpc.RpcError: Argument 1![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/U002.jpg) is not present')

TrueFalse作为参数调用它将返回一个整数计数,如下所示:

`>>> droid.smsGetMessageCount(True).result
0

droid.smsGetMessageCount(False).result
228`

操纵单个消息是通过 ID 完成的。调用smsGetMessageIds将返回所有消息 id 的列表或未读消息,这取决于传递的布尔参数。这只是返回一个数字列表,所以如果你真的想用它们做什么,你必须得到所有的消息。有两种方法可以做到这一点。要么调用smsGetMessages获取所有信息,要么遍历由smsGetMessageIds返回的消息 id 列表,然后使用smsGetMessageById分别获取每个消息 id。

如果您只想处理未读邮件,您可以设置传递给任何 GetMessage 调用的布尔值。然后,您可以使用smsMarkMessageReadsmsDeleteMessage来处理每条消息。最后,还有真正发送短信的smsSend。这个函数有两个参数:目的地址(通常是电话号码)和消息的实际文本。

演讲认知面

您可以使用这个外观将语音识别添加到您的脚本中。它只有一个名为recognizeSpeech的函数调用。有三个可选参数,包括提示字符串、通知识别器预期使用不同于默认语言的语音的语言字符串,以及告诉识别器首选哪个语音模型的语言模型字符串。它返回一个字符串,表示尽最大努力将语音转换为文本。如果它不能解释它,你将得到一个空字符串。下面是我调用这个函数,说“西班牙的雨主要落在平原上”时得到的结果:

>>> droid.recognizeSpeech() Result(id=2, result=u'the rain in spain falls mainly on the plane', error=None)

TextToSpeechFacade

这些函数为 API4 和更高版本提供 TTS 服务。要让设备“说出”一个短语,您可以使用ttsSpeak函数,向它传递一个包含该短语的字符串。发出此调用后,控制权会立即传递回调用脚本。您必须使用功能ttsIsSpeaking来确定语音功能是否已经完成。

音发生器面

如果您需要为特定功能(如与交互式语音应答应用交互)生成 DTMF 音调,这款电话正适合您。要使用它,您必须调用generateDtmfTones,传入一个表示您希望生成的数字的字符串。可选的整数参数允许您改变每个音的持续时间,默认值为 100 毫秒。

简易性

这个 facade 提供了创建用户界面元素所需的所有函数,比如文本框、复选框、日期选择器等等。其中一些函数是单动作,这意味着你只需要调用它们一次就能得到响应。这些功能还会阻止或等待用户完成操作并关闭对话框。两个单动作、与输入相关的对话框是dialogGetInputdialogGetPassword。两者都有可选参数来设置标题、提示消息和默认输入。图 5-8 显示了以下代码的结果:

>>> droid.dialogGetInput(u'My Title', u'My Message')images

图 5-8。对话输入示例

这个外观中有许多函数需要两次调用来实际显示对话框,第三次调用来获得响应。这个过程包括用一个调用来设置对话框,然后用一个对dialogShow的调用来呈现它。图 5-9 显示了dialogCreateAlert的一个例子。

images

图 5-9。对话示例创建警报

这个特殊的对话框旨在向用户呈现某种类型的警告信息,这需要在设备上执行任何操作之前进行确认。它不会返回任何信息,也不会阻止任何进一步的程序执行。您可以通过按下一个硬件按钮或调用dialogDismiss以编程方式关闭对话框。

对于其他要返回信息的 UI 元素,您需要调用dialogGetResponse来实际获取数据。这里的顺序很重要,因为dialogGetResponse实际上会阻塞,直到用户关闭对话框。您应该检查结果变量,以确定用户是实际输入了数据还是按下了 Cancel 按钮。要提示并实际获得与dialogCreateTimePicker,的时间,请执行以下操作:

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

droid.dialogShow()
Result(id=23, result=None, error=None)
droid.dialogGetResponse()
Result(id=24, result={u'hour': 15, u'minute': 53, u'which': u'positive'}, error=None)`

您可以在使用 Python 和 IDLE 时看到调用dialogGetResponse的结果,因为提示会消失,直到您关闭对话框。如果用户单击 Cancel 按钮,您将在'which'参数中得到一个正的返回,如下所示:

>>> droid.dialogGetResponse() Result(id=26, result={u'hour': 0, u'minute': 0, u'which': u'negative'}, error=None)

有三个函数调用允许您设置警告框中显示的按钮文本。这里有一个简短的 Python 例程来演示dialogSetPositiveButtonTextdialogSetNegativeButtonTextdialogSetNeutralButtonText的用法。图 5-10 显示了实际的对话框。

`import android

droid = android.Android()

title = 'Alert'
message = ('This alert box has 3 buttons '
'and waits for you to press one.')
droid.dialogCreateAlert(title, message)
droid.dialogSetPositiveButtonText('Yes')
droid.dialogSetNegativeButtonText('No')
droid.dialogSetNeutralButtonText('Cancel')
droid.dialogShow()
response = droid.dialogGetResponse().result

print ['which'] in ('positive', 'negative', 'neutral')`images

图 5-10。多按钮警告对话框示例

下一组对话框功能包含多个元素,必须在显示它们之前进行设置。这些元素包括以单项或多项选择方式进行选择的项目列表。图 5-11 显示了使用dialogSetItems创建列表。下面是设置项目列表的一小段 Python 代码:

droid.dialogCreateAlert(title) droid.dialogSetItems(['one', 'two', 'three']) droid.dialogShow() response = droid.dialogGetResponse().resultimages

图 5-11。多选项警告对话框示例

这个主题的一个小变化是使用dialogSetSingleChoiceItemsdialogSetMultiChoiceItems来创建一个项目列表,用单选按钮或复选框来选择项目。使用dialogSetItemsdialogSetSingleChoiceItems的唯一真正区别是带有单选按钮和确认选择和返回按钮的可视显示。下面是使用dialogSetSingleChoiceItems的代码:

droid.dialogCreateAlert(title) droid.dialogSetSingleChoiceItems(['One', 'Two', 'Three']) droid.dialogSetPositiveButtonText('Done') droid.dialogShow()

对于多选选项,还需要一次函数调用来获取选中的项目:dialogGetSelectedItems。在这个例子中,操作的顺序也很重要。您必须等待用户实际选择项目并关闭对话框,然后才能尝试读取它们。因此,您必须在调用dialogGetResponse之后插入对dialogGetSelectedItems的调用。图 5-12 显示了对话框的样子。下面的代码片段展示了如何使用这个调用来创建对话框并获得响应:

droid.dialogCreateAlert(title) droid.dialogSetMultiChoiceItems(['One', 'Two', 'Three']) droid.dialogSetPositiveButtonText('Done') droid.dialogShow() droid.dialogGetResponse() ans = droid.dialogGetSelectedItems()images

图 5-12。多选警告对话框示例

选择选项一和三的结果如下:

Result(id=5, result=[0, 2], error=None)

在 Python 中,结果实际上是代表所选选项的值列表(从零开始)。如果您有任何需要一些时间才能完成的脚本代码,您应该使用进度对话框来通知用户。可用的两个选项是dialogCreateHorizontalProgressdialogCreateSpinnerProgress。要更新进度设置,您必须呼叫dialogSetCurrentProgress。还有dialogSetMaxProgress来定义终点。图 5-13 显示了用以下代码生成的水平进度条:

`import android
import time

droid = android.Android()

title = 'Horizontal'
message = 'This is simple horizontal progress.'
droid.dialogCreateHorizontalProgress(title, message, 100)
droid.dialogShow()
for x in range(0, 99):
time.sleep(0.1)
droid.dialogSetCurrentProgress(x)
droid.dialogDismiss()`images

图 5-13。水平进度对话框示例

请注意,您必须调用dialogDismiss才能真正让进度对话框消失。微调器进度对话框是用dialogCreateSpinnerProgress创建的,用于显示移动的东西,让用户知道正在进行加工。和水平进度对话框一样,你必须调用dialogDismiss来关闭微调对话框。

Android 为应用提供了两种类型的菜单:上下文和选项。一个上下文菜单类似于你在桌面操作系统中右击鼠标时看到的内容。选项菜单是您在脚本运行时按下设备菜单按钮时看到的内容。然后,用户可以设置偏好,甚至以相对标准的 Android 方式退出脚本。您可以使用addContextMenuItemaddOptionsMenuItem向这些菜单添加项目。要清除任一菜单,使用clearContextMenuclearOptionsMenu

最后的 UI 对话框元素是webViewShow。这个对话框向 SL4A 脚本打开了 HTML 表单的世界,并将在后面的章节中用来构建一个全功能的应用。现在,让我们假设它将使用传递给它的 URL 显示一个 WebView。如果设置为True,可选的wait布尔参数将导致脚本阻塞,直到用户退出 WebView。第八章使用这个外观来构建一些基于对话框的用户界面例子。

醒脑脸

在移动设备应用领域,有一个概念是将设备锁定在唤醒状态,以允许一些关键过程完成。这对你的电池寿命有潜在的危险,应该只在短时间内使用。它也可以用于像视频播放器这样的应用,以防止正常的屏幕关闭。为创建唤醒锁而提供的函数调用包括wakeLockAcquireBrightwakeLockAcquireDimwakeLockAcquireFullwakeLockAcquirePartial。每一个都影响屏幕亮度和 CPU 状态。当你的应用不再需要唤醒锁时,它调用wakeLockRelease函数来关闭它。

网络广播学院

Android 设备上的网络摄像头是前置摄像头。使用webcamStartwebcamStop来启动或停止网络摄像头。当您启动网络摄像头时,您可以使用分辨率、质量和端口号的默认设置,或者将它们作为选项传入。还有一个单独的功能,webcamAdjustQuality,用于在播放视频时调整质量。

wif academy

通过WifiFacade,你可以完全控制你设备上的 WiFi 收音机。基本操作是checkWifiStatetoggleWifiState。这些函数的操作方式与其他类似命名的函数非常相似,这意味着您可以向toggleWifiState传递一个布尔值来隐式启用或禁用 WiFi 无线电。对wifiDisconnectwifiReconnectwifiReassociate的调用顾名思义。要检索当前活动接入点的信息,使用wifiGetConnectionInfo

您可以使用剩余的函数调用来构建一个 WiFi 扫描应用。可用的函数调用有wifiStartScanwifiGetScanResultswifiLockAcquireFullwifiLockAcquireScanOnlywifiLockRelease。如果您想独享 WiFi 广播,您应该拨打wifiLockAcquireFullwifiLockAcquireScanOnly。确保完成后调用wifiLockRelease,否则其他应用将无法连接到 WiFi。

总结

本章的重点是让你熟悉 SL4A 提供的 Android APIs。例子给出了所有使用的 Python 和运行在 Windows 上的空闲应用。通过使用相同的基本方法,您应该能够在 Linux 或 Mac OS X 上重复这些示例。在下一章中,我将开始实际创建您可以立即投入使用的真实脚本。

以下是你想从这一章中记住的一些事情。

  • 物是人非:SL4A 项目是一个动态的项目,新的版本经常会给 API 带来变化。如果某个特定的功能被替换了,你会收到一个通知。
  • 了解你的门面 : SL4A 使用门面的概念来模仿原生 Android API 调用。这将有助于您了解本地调用是如何工作的,尤其是对于像startActivitymakeIntent这样的东西。
  • 不要害怕尝试:模拟器是测试许多 API 调用的好地方。不幸的是,并非所有的功能都可以在模拟器中工作。传感器、摄像头、WiFi 和网络摄像头只能在真实设备上工作。在一台设备上测试这些功能不会对你造成任何伤害。所以去尝试一下吧。
  • 阅读文档:我知道有时候阅读文档有多难。在使用 Android 和 SL4A 的情况下,如果你只是做一点阅读,它可以节省你的时间和挫折。谷歌搜索也可以成为你的朋友。

六、使用 Python 编写后台脚本

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

本章的主要主题如下:

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

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

后台任务

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

images

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

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

images

图 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 显示了在查看脚本列表时,如果按下菜单按钮,然后选择触发器,您将看到的菜单。

images

图 6-3。触发菜单

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

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

images

图 6-4。移除触发按钮

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

images

图 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 中的窗口。

images

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

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

images

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

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

images

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

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

images

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

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

images

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

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

images

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

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

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

images

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

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

images

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

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

images

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

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

images

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

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

images

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

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

images

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

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

images

图 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 所示。

images

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

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

images

图 6-20。 Flickr 授权屏幕

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

images

图 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 显示了我们的文档在谷歌文档中的样子。

images

图 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 的不同工具任务。在典型的个人电脑上,这些属于命令行工具类。

images 撰写本章时使用的 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 所示。

images

图 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 的文件和目录列表。

images

图 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\nAUTHimages
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 消息所需的命令。

images

图 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'] + ',' +images
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 网络界面中的样子。

images

图 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']images

图 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 显示了注册页面的截图。

images

图 7-6。推特应用注册

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

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

images

图 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 中的。

images

图 7-8。推特应用授权

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

images

图 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看时间线会是什么样子。

images

图 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 位置)。

images

图 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命令的帮助。

images

图 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 )。

images

图 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 或您所连接的接入点。

images

图 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 将在实际退出应用之前提示您,如图所示。

images

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

images

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

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

images

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

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

查杀正在运行的 App

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

images

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

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

images

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

images

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

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

URL 文件检索器

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

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

images

图 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 显示了下载文件过程中进度条的样子。

images

图 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 )以方便主机(左边)和客户端(右边)之间的拖放文件操作。

images

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

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

images

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

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

images

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

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

images

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

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

images

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

总结

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

以下是本章的要点:

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

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

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

images 本章将讨论使用 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

images

图 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 的弹出信息显示一小段时间。

images

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

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

droid.dialogCreateAlert('Settings Dialog','Chose any number of items and then press OK') droid.dialogShow()images

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

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

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

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

images

图 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").resultimages

图 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 显示了结果对话框。

images

图 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,然后用返回的标题列表填充警告对话框。

images

图 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 中所示的对话框。

images

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

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

便捷对话框

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

droid.dialogCreateDatePicker(2011) droid.dialogShow()

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

images

图 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 或午夜。

images

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

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

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

images

图 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()images

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

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

droid.dialogSetCurrentProgress(2048)

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

images

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

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

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

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

images

图 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 中所示的对话框。

images

图 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 显示了如果你按下硬件菜单按钮,运行前一个脚本的结果应该是什么样子。

images

图 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])`images

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

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

images

图 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"`images

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

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

Chosen item= 1 = White Doneimages

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

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

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

images 提示如果您远程运行任何需要引用文件系统的应用,您需要知道它将在您的本地文件系统上寻找,而不是在设备或模拟器上。通过在主驱动器上创建一个名为/sdcard的目录,然后添加一个名为sl4a的子目录,然后在 sl4a 下添加另一个名为scripts的子目录,可以镜像设备或仿真器上的相同结构。

images

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

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

Result= {u'which': u'positive'} Selected= [0, 1, 2] Doneimages

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

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

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

??" 播客应用

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

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

images

图 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):

images 注意递归是创建受控用户界面的一种很好的方式——这意味着相同的代码会被执行多次,直到用户以你想要的方式退出。

images

图 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 显示了我们最终设置对话框的样子。

images

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

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

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

images

图 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函数调用递归输出一个列表框。

九、使用 HTML 的 Python GUIs

本章将介绍使用 SL4A 构建基于 CSS、HTML、JavaScript 和 Python 的图形用户界面(GUI)的可用选项。

images 本章将讨论如何使用 CSS、HTML 和 JavaScript 来构建呈现真实世界用户界面的应用。如果你有这些领域的背景知识会很有帮助,但这不是必须的。

本章的主要主题如下:

  • HTML 图形用户界面基础
  • 使用级联样式表(CSS)向 HTML 添加一些格式
  • 用 CSS、HTML、JavaScript 和 Python 创建商业质量的用户界面

这里的基本方法是使用 HTML 和 JavaScript 来构建用户界面(UI ),然后在幕后使用 Python 来处理任何额外的处理。CSS 可以用来使 HTML 字段和字体在外观和一致性方面更加整洁。Python 也可以用来构建 HTML 文件,在没有任何用户界面的情况下显示信息。

HTML 和基本信息显示

在构建应用时,需要简单地向用户显示大量信息的情况并不少见。这可能是一个列表的形式,甚至只是一个连续的文本框。使用 HTML 作为显示机制,两者都很容易得到支持。HTML 文件可以通过编程生成,或者使用任何文本编辑器创建,然后使用webViewShow API 调用启动。

我们先看第一个选项。在这个示例代码中,我们将查询电池的状态,并在一个简单的 HTML 文件中显示您想知道的一切。然后,我们将通过调用webViewShow来启动该文件,我们就完成了。下面是实现它的代码:

`import time

import android

Simple HTML template using python's format string syntax.

template = '''

Battery Status

  • Status: %(status)s
  • Temperature: %(temperature)s
  • Level: %(level)s
  • Plugged In: %(plugged)s
'''

if name == 'main':
droid = android.Android()

Wait until we have readings from the battery.

droid.batteryStartMonitoring()
result = None
while result is None:
result = droid.readBatteryData().result
time.sleep(0.5)

Write out the HTML with the values from our battery reading.

f = open('/sdcard/sl4a/scripts/battstats.html', 'w')
f.write(template % result)
f.close()

Show the resulting HTML page.

droid.webViewShow('file:///sdcard/sl4a/scripts/battstats.html')`

这将在scripts目录中创建一个名为battstats.html的文件。如果您想保存这些文件的集合,您只需将当前时间添加到文件名中,每次都会生成一个唯一的文件。图 9-1 显示了代码显示文件时应该看到的内容:

images

图 9-1。使用简单的 HTML 文件显示电池状态

这个调用的第二个例子是从第七章中获取我们的 WiFi 扫描仪示例,并使用 HTML 文件方法显示信息。在这种情况下,您可能希望在文件中添加诸如时间和日期戳之类的东西,然后每次都追加到末尾。这样,您将有一个您的设备已经看到的 WiFi 接入点的运行日志。下面是生成该文件的代码:

`import time
import android

if name == 'main':
droid = android.Android()

Show the HTML page immediately.

droid.webViewShow('file:///sdcard/sl4a/scripts/wifi.html')

Mainloop

while True:

Wait until the scan finishes.

while not droid.wifiStartScan().result: time.sleep(0.25)

Send results to HTML page.

droid.postEvent('show_networks', droid.wifiGetScanResults().result)

time.sleep(1)`

虽然这段代码只是显示当前范围内的 WiFi 接入点,但是您可以创建一个日志文件并将您的结果附加到其中。该文件会随着时间的推移而增长,直到您删除它。将它保存到文件中,然后显示为 HTML 文件的好处是,您可以使用与浏览网页相同的手指动作来滚动文件。图 9-2 显示了结果。

images

图 9-2。WiFi 扫描结果

HTML 和 JavaScript

除了基本的信息显示,下一步是添加某种类型的交互性。这就是我们必须将 JavaScript 引入讨论的地方。SL4A 为网页和 Python 之间的通信提供了一种机制。这是通过在网页中使用事件和一些 JavaScript 代码来实现的。JavaScript 代码的唯一真正要求是,在进行任何 API 调用之前,必须用代码var droid = new Android()实例化 Android 对象。完成后,您就可以像从 Python 中一样访问相同的 API facades 了。

下面是一个使用 JavaScript 获取联系人列表并根据数据动态构建网页的示例。这种技术可以用于任何返回您想要显示的数据的 API 调用。HTML 文件看起来是这样的:

`

<head> </head> <body> <h1>Contacts</h1> <ul id="contacts"></ul> <script type="text/javascript"> var droid = new Android(); var contacts = droid.contactsGet(['display_name']); var container = document.getElementById('contacts'); for (var i=0;i<=contacts.result.length;i++){ var data = contacts.result[i]; contact = '<li>'; contact = contact + data[0]; contact = contact + '</li>'; container.innerHTML = container.innerHTML + contact; } </script> </body> </html>`

注意,我在这里做的只是调用contactsGet routine并传入display_name限定符。下面是 Python 代码实际显示 HTML 文件的样子(这段代码唯一做的事情就是加载 HTML 文件,然后退出):

`import android

droid = android.Android()
droid.webViewShow('file:///sdcard/sl4a/scripts/contacts.html')`

图 9-3 显示了我们努力的结果。

images

图 9-3。联系人列表的基本 HTML 显示

这个版本很适合简单地显示信息,但是如果你想让用户能够用你展示的东西做一些事情呢?我们可以对 HTML 文件做一点小小的修改,并使用一个基本的表格和一个超链接添加一些交互。下面是 HTML 和 JavaScript 代码:

`

<head> </head> <body> <h1>Contacts</h1> <table id="contacts"></table> <script type="text/javascript"> var droid = new Android(); function call(number){ droid.phoneDialNumber(number); } var contacts = droid.contactsGet(['display_name', 'primary_phone']); var container = document.getElementById('contacts'); for (var i=0;i<=contacts.result.length;i++){ var data = contacts.result[i]; contact = '<tr>'; contact += '<th>' + data[0] + '</th>'; contact += '<td><a href="#" onclick="call(' + data[1] + ');return false;">' + data[1] + '</a></td>'; contact += '</tr>'; container.innerHTML = container.innerHTML + contact; } </script> </body> </html>`

JavaScript 代码有两处轻微的修改。首先,我们添加以下内容来创建一个超链接,以打开呼叫对话框:

function call(number){ droid.phoneDialNumber(number); }

另一个变化是使用 HTML <tr><td>标记代替简单的列表元素标记来创建一个表格。虽然这种改变非常简单,但它无需编写大量代码就能创建良好的用户交互。图 9-4 显示了结果。

images

图 9-4。以表格形式显示联系人的 HTML 格式

HTML 图形用户界面表单基础

现在我们将看看使用 CSS、HTML 和 JavaScript 构建 SL4A GUI 的基础知识。一般来说,这个想法是创建一个 HTML 表单,使用 Python 来处理表单生成的事件。例如,你可以在窗体上有一个按钮,当你按下它的时候会发生一些事情。SL4A wiki 给出了一个简单的例子,我把它包含在清单 9-1 : 中

清单 9-1。 text_to_speech.html

`

<head> <title>Text to Speech</title> <script> var droid = new Android(); var speak = function() { droid.postEvent("say", document.getElementById("say").value); } </script> </head> <body> <form onsubmit="speak(); return false;"> <label for="say">What would you like to say?</label> <input type="text" id="say" /> <input type="submit" value="Speak" /> </form> </body> </html>`

清单 9-2。 speakit.py

`import android

droid = android.Android() droid.webViewShow('file:///sdcard/sl4a/scripts/text_to_speech.html')
while True:
result = droid.waitForEvent('say').result
droid.ttsSpeak(result['data'])`

组成这个程序需要两个文件:名为text_to_speech.html的 HTML 文件和我们称之为speakit.py的 Python 启动器(见清单 9-2 )。两者都必须位于设备上的/sdcard/sl4a/scripts目录中。要启动程序,运行 SL4A 文件列表中的speakit.py文件。Python 代码首先使用webViewShow API 调用启动text_to_speech.html文件,然后等待 HTML 页面触发一个事件。该事件在用户触摸“speak”按钮时生成。

图 9-5 显示了屏幕的样子。

images

图 9-5。文本到语音转换演示的简单 HTML 页面

JavaScript 代码包含在<script> </script>标记中,并使用postEvent API 调用提供到调用 Python 脚本的连接。要启动这个 HTML 表单,需要调用如下的webViewShow API:

`import android

droid = android.Android() droid.webViewShow('file:///sdcard/sl4a/scripts/text_to_speech.html')
while True:
result = droid.waitForEvent('say').result
droid.ttsSpeak(result['data'])`

一旦显示了表单,Python 代码将阻塞并等待触发'say'事件。该事件将返回文本以传递给结果对象数据字段中的ttsSpeak API函数。当用户单击 speak 按钮时,web 页面将实际关闭,一旦控制从ttsSpeak函数返回,Python 代码将退出。

简单的 HTML 表单

现在我们准备处理一个稍微复杂一点的有多个输入框和输入类型的问题。该脚本将显示一个屏幕,允许用户设置许多设备首选项设置,包括屏幕亮度和超时、媒体音量、铃声音量和 WiFi 模式。创建这种类型表单的 HTML 非常简单。这是你需要的一切:

`

<div id="body"> <h1>My Settings</h1> <form> <div class="container"> <div> <label for="brightness">Brightness Level</label> <input size="5" id="brightness" type="text" /> </div> <div> <label for="timeout">Timeout Secs</label> <select> <option value="0">0</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> </div> <div> <label for="screen">Screen Off</label> <input id="screen" type="checkbox" /> </div> </div> <hr /> <div class="container"> <div> <label for="media_vol">Media Volume</label> <input size="5" id="media_vol" type="text" /> </div> <div> <label for="ringer_vol">Ringer Volume</label> <input size="5" id="ringer_vol" type="text" /> </div> </div> <hr /> <div class="container"> <div> <label for="airplane_mode">Airplane Mode</label> <input id="airplane_mode" name="radio" type="radio" /> </div> <div> <label for="wifi_on">Wifi On</label> <input id="wifi_on" name="radio" type="radio" /> </div> </div> <div class="container buttons"> <div> <input size="5" id="save" name="save" type="button" value="Save Settings" /> <input size="5" id="cancel" name="cancel" type="button" value="Cancel" /> </div> </div> </form> </div> </body> `

这将产生一个类似于图 9-6 的页面。正如你所看到的,这在小屏幕上呈现有一些问题。标题被砍掉了,按钮没有完全放在页面上,水平基准线似乎脱离了页面。虽然您可以对 HTML 进行一些调整以使其看起来更漂亮,但更好的方法是使用 CSS。

images

图 9-6。没有 CSS 的基本 HTML 表单

层叠样式表

用级联样式表(CSS)格式化 HTML 对创建和呈现一个干净的用户界面大有帮助。使用 CSS,你可以决定页面上所有 HTML 元素的对齐方式、字体、文本流和大小。这对于小屏幕来说非常方便,因为在小屏幕上,您想要精确地指示页面上的每个元素是如何出现的。

下面是一小段 CSS,我将使用它来帮助美化我们的用户设置页面:

`

<head> <style> #body {width:100%;} .container {text-align:center;margin:auto;} .container div {text-align:left;width:75%;} h1 {text-align:center;margin:auto;} hr {width:75%;margin:0px auto;} label {display:block;float:left;width:60%;} .buttons div {text-align:center;margin:auto;} </style> </head>`

web 表单的主体部分包含许多标准元素,如<div>标签、使用<label><input>标签的输入框,以及使用<label><select>标签的下拉框中的选项列表。前面的 CSS 代码控制标签的宽度和外观以及文本的对齐方式。它还控制按钮、h1hr HTML 标签的格式。你可以用 CSS 做更多的事情,但是我们将在这个例子中停止。剩下的 HTML 看起来和以前一样。

图 9-7 显示了添加了 CSS 的 HTML 页面的样子。虽然差别不大,但请注意文本框的宽度和元素的整体间距。随意搭配你喜欢的造型。

images

图 9-7。添加了 CSS 的 HTML 表单

speakit.py的例子中,我们看到了使用droid.postEvent() JavaScript 代码通过事件将数据发送回 Python 应用。这将发送一个表示要朗读的短语的字符串值。该表单包含许多元素,这些元素包含必须提取并发送回 Python 代码的信息。有许多方法可以实现这一点,但是我们将简单地使用一个键/值字符串对。

这可以通过在我们的 JavaScript 中增加几行代码来完成,以便从 HTML 表单中传递更多的信息。它看起来是这样的:

``

下面是这段代码的 HTML 代码:

`

My Settings

`

从特定的 HTML 表单元素中提取值的关键是document.getElementById()行。在 Python 端,这些值随后被用来设置手机上的特定设置。Python 代码如下所示:

`import android
import urlparse

droid = android.Android()
droid.webViewShow('file:///sdcard/sl4a/scripts/settings.html')
while True:
result = droid.waitForEvent('save').result
data = urlparse.parse_qs(result['data'][1:])

droid.toggleAirplaneMode('airplane' in data)
droid.toggleWifiState('wifi' in data)
droid.setScreenBrightness('screen' in data and 255 or 0)`

这个例子介绍了另一个名为urlparse的 Python 标准库工具。这个函数将把返回的元素解析成一个数据项列表,作为键/值对。此时剩下要做的就是调用适当的 API 函数来设置值。

短信合并

SL4A 主页包括许多示例程序的链接,包括一些演示如何使用webViewShow API 函数的链接。SMS 合并绝对是使用 Python、JavaScript、HTML 和 CSS 的组合所能完成的最完整的例子。这个示例还让我们有机会使用 Eclipse 及其文件管理特性来演示如何构建一个复杂的应用,并最终将其作为 Android 包(.apk文件)分发。那部分实际上会在第十章中涉及。

为了理解这个示例程序,将它分解成不同的组件以了解每个函数的作用是很重要的。请记住,这个程序是一个样本,而不是一个真正的充分测试和工作的应用。它确实有一些怪癖,甚至暴露了 SL4A 早期版本中的一些 bug。这里的目的是检查代码,看看每个函数做什么,并让您知道使用相同的技术可以构建什么。如果您选择自己运行代码,我将在这一节的最后总结一下需要注意的事项。如果你打开SMSMerge.zip文件,你应该看到类似于图 9-8 的东西。

images

图 9-8。SMS merge . zip 文件的内容

每个目录都包含基于名称的信息。/etc目录包含一个名为SMSSender.conf的文件,它存储了应用的所有配置信息。如果您用文本编辑器打开该文件,您将看到如下内容:

`[locale]
prefix = +60

[merger] informeveryratio = 10
informevery = 0

[application]
showonlycsvfiles = 0
showonlytextfiles = 1
showhiddendirectories = 0

[package]
version = 1.01`

这是一个使用标准 Python 编码实践的很好的例子,您可以在为桌面编写的典型开源应用中找到。它基于 Python 标准库ConfigParser模块。要使用它,只需简单地使用import ConfigParser,然后用parser = ConfigParser()实例化它,就可以访问不同的方法。节名完全由程序员决定,应该反映一个有意义的标题。在这种情况下,有四个命名的部分。解析时,它们会变成与节名相关联的键/值对的 Python 字典。下面是一段一段加载config文件的代码:

`def load( self ):

Go through all the sections

sections = {}

Some sections are meant to be ignored

for section in self.sections():
if section not in self.ignore:
items = self.items(section)
options = []
for item in items:
options.append( {"name":item[0],
"value":item[1],
"description": self.descriptions[section][item[0]]})
sections[section] = options
return sections`

在图 9-9 中,您可以看到设置页面上的可用选项,并了解它们如何与config文件中的值相关联。

images

图 9-9。设置配置页面

如果您单击任何选项的标签,您将会看到一个弹出对话框,其中描述了该选项的功能以及可接受的值。图 9-10 显示了你点击showonlycsvfiles线时会看到的内容。它使用一个弹出警告对话框向用户提供关于设置这个特定选项的后果的反馈。

images

图 9-10。弹出对话框为 showonlycsvfiles 选项

让我们回到这一点,谈谈 CSS 文件。在前面的 HTML 示例中,我使用了一个非常简单的 CSS 文件来定义表单在小屏幕上的显示方式。这个应用将 CSS 文件提升到了一个全新的水平。如果在文本编辑器中打开zest.css文件,您将看到不同的部分和用于定义 HTML 元素格式的技术。在文件的顶部,你会看到两个标有bodybutton的部分。代码如下所示:

body { width:100%; padding: 0; font-size: 14px; background: black; color:white; font-family: Arial; } button{ color:white; background: transparent; border: solid 1px #2986a5; }

body部分定义了整个 HTML 页面的默认值,而button部分定义了按钮的默认值。对于这个应用,页面顶部有一个由四个图标组成的菜单。这些变化取决于用户交互。每个按钮都有一个白色的底色,一个透明的背景,一个像素的边框,边框上有十六进制的颜色代码#2986a5

代替简单的按钮,SMS 合并应用使用了一种相当常见的方法,即使用图像文件。每个按钮使用两种图像版本,分别表示选中和未选中。当用户按下按钮时,图像被交换,产生高亮效果。图 9-11 显示了包含不同按钮图像的目录视图。

images

图 9-11。用于 UI 元素的图标

下面是一段 CSS 代码,它定义了菜单在页面顶部的外观:

div#menu { background-image: url("img/tab-bg.png"); background-repeat: repeat-x; color: white; font-weight: bold; height: 96px; } div#menu div.current { background-image: url("img/tab-bg-current.png"); background-repeat: repeat-x; } div.icon { height: 67px; width: 100%; background-repeat: no-repeat; background-position: top center; }

这是相应的 HTML 块:

`

`

图 9-12 显示了菜单在模拟器中的样子。

images

图 9-12。由 CSS、HTML、图片和 JavaScript 构建的菜单

当您触摸其中一个按钮时,如文件按钮,您将看到一个新的显示,如图图 9-13 所示。该页面的 HTML 代码如下所示:

`

File -

Fields (select phone number column):

File dialect:

End of line character:

Quote character:

Field delimiter:

File preview:

`![images](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/pro-andr-sc-sl4a/img/0913.jpg)

图 9-13。文件加载屏幕

文件预览部分由加载的 CSV 文件构建,并使用 Python 代码提供的数据。下面是实际读取文件并将其返回给 JavaScript 的代码:

`def loadfile(self, data):
self.log("Loading file")
merger = self.merger
filename = data["path"]
if filename != "":
self.log("Selected filename %s " % filename)
try:
reader = CSVReader( filename )
except csv.Error, e:
return { "error": "Unable to open CSV: %s" % e }
fields = reader.getFields()
self.log("Found fields: %s" % ''.join(fields))
merger.setFields(fields)
rows = reader.getRows()
merger.setItems(rows)

Rows are now dicts, for preview, want them as list of values only

values = []
for row in rows:
values.append( row.values() )
else:
self.log("No file name")
return {"filename":"","fields":[], "error": ""}

Success and new file, return all info

return {"filename":filename, "fields":fields,
"delimiter":reader.dialect.delimiter,
"quotechar":reader.dialect.quotechar,
"lineterminator": reader.dialect.lineterminator,
"error": "", "rows":values }`

“合并”选项卡是真正的操作发生的地方。它获取包含电话号码和消息文本的 CSV 文件,并将其与您手动输入或从文件加载的消息合并,最终广播 SMS 消息。图 9-14 显示了这个屏幕的样子。

images

图 9-14。合并发送短信画面

执行合并的 Python 代码一点也不难阅读。看起来是这样的:

`def merge(self, data):
droid = self.droid
merger = self.merger
merger.prefix = parser.get( "locale", "prefix" )
merger.setNumberColumn(int(data["phone"]))
merger.setTemplate(data["text"])
ret = {"success":False, "error":"", "messages":[]}

Valid template returns a list of merge fields that are not used by the given template

missing = merger.validTemplate()
if missing.len() == 0:
ret["messages"] = merger.merge()
ret["success"] = True
else:
droid.dialogCreateAlert("Incomplete text",
"The following merge fields are not being used by the template: %s.\r\n Would you like
to edit the template text?" % ",".join(missing))
droid.dialogSetPositiveButtonText("Yes")
droid.dialogSetNegativeButtonText("No")
droid.dialogShow()
resp = droid.dialogGetResponse()

User wishes to load now

if resp.result["which"] == "positive" :
return {"task":"edittext"}
else:
ret["messages"] = merger.merge()
ret["success"] = True
return ret`

该页面背后还有一些 JavaScript 代码:

`/*

  • Merge tab button event
  • On Click, checks that CSV is loaded, checks that template text is loaded.
  • Then fires an event to request Python to merge all SMS
  • Receives sms as object {number,message} and displays them in a table
    */
    buttons.merge.addEvent("click",function(){
    if(!csvLoaded()){
    if(loadCsv()){
    buttons.file.fireEvent("click");
    buttons.importCSV.fireEvent("click");
    }
    } else {
    var text=dta.getValue();
    if(text == ""){
    textNeeded();
    buttons.textTab.fireEvent("click");
    } else {
    handler.startLoad("Processing", "Merging")
    showOne(tabs,divs.mergeTab);
    phone = getMergeFields().phone;
    var resp = handler.postAndWait( {"task":"merge", "text":text.replace
    ("\n","\u000A"), "phone":phone });
    if(resp.task=="edittext"){
    buttons.textTab.fireEvent("click");
    dta.fireEvent("click");
    }else{
    clearMergedSamples();
    var table = divs.mergeTab.getElement("table");
    resp.messages.each(function(m){
    var clone = templateRow.clone();
    clone.getElement("td.phone").setText(m.number);
    clone.getElement("td.message").setText(m.message);
    table.adopt(clone);
    });
    }
    handler.stopLoad();
    }
    }
    });`
依赖关系

每个软件项目都有某种依赖关系。当你选择了一种编码语言,你就做了一个依赖选择。如果您的应用将在特定的操作系统上运行,那么您已经做出了操作系统依赖性的决定。外部库通常提供额外的功能,否则很难编写代码。您付出的代价是打包库和管理任何可能破坏代码的更新所带来的痛苦。本书中的所有示例脚本都依赖于 SL4A 和 Python。

SMS Merger 的第一个版本使用了一个外部依赖项,以 Open Intents (OI)文件管理器的形式浏览和选择文件。下面是该版本的一段代码,它使用startActivityForResult API 调用启动 OI 文件管理器,然后从返回的映射值中提取文件名:

`def requestTemplateFromFile( self ):
droid = self.droid
map = droid.startActivityForResult( "org.openintents.action.PICK_FILE",
None, None,
{"org.openintents.extra.TITLE":"Choose file containing message",
"org.openintents.extra.BUTTON_TEXT":"Choose"})
if map.result is None:
self.requestMessage()
else:
filename = map.result["data"].replace( "file://", "" )
text = open( filename, "r" )
smscontent = text.readline().replace( "\n", "" )
if self.validTemplate( smscontent ) is True:
return smscontent
else:
self.warnInvalidTemplate( smscontent )

Loop

return self.requestTemplateFromFile()`

OI 文件管理器是一个界面简洁的好工具。它提供了一种选择文件并将其返回给调用者的简单方法。图 9-15 显示了它的样子。

images

图 9-15。打开意向文件管理器

使用像 OI 文件管理器这样的外部应用的一个缺点是用户必须完成额外的安装。虽然这对程序员来说没什么大不了的,但这肯定不是您希望一个典型用户做的事情。更好的解决方案是使用 HTML、JavaScript 和 Python。下面是一个创建文件列表的 Python 函数:

`def listdir(self, data):
""" Creates two lists of files and folders in the path found in data

data -- dict containing path and type (for filtering)

"""
self.log("Loading directory content")
base = data["path"]
type = data["type"]

Check in the config whether we want to show only a certain type of content

showHiddenDirectories = self.parser.getboolean( "application",
"showhiddendirectories" )

if type == "txt":
if self.parser.getboolean( "application", "showonlytextfiles" ) is True:
filter = ".{0}".format( type )
else:
filter = None
elif type == "csv":
if self.parser.getboolean( "application", "showonlycsvfiles" ) is True:
filter = ".{0}".format( type )
else:
filter = None
else:
filter = None

List all directories and files, then filter

all = os.listdir(base)
files = []
folders = []
for file in all:

Separate files and folders

abs = "{0}/{1}".format( base, file )
if os.path.isdir( abs ):

Are we filtering hidden directories?

if showHiddenDirectories is True or file[0] != ".":
folders.append( str( file ) )
elif os.path.isfile( abs ):

Are we filtering by type?

if filter is None or os.path.splitext( file )[1] == filter:
files.append( str( file ) )

Sort alphabetically

files.sort( key=str.lower )
folders.sort( key=str.lower )
return {"files":files,"folders":folders}`

图 9-16 显示了一个独立于任何外部应用的 HTML 和 JavaScript 版本。

images

图 9-16。 HTML 和 JavaScript 文件浏览器

创建这个窗口的 JavaScript 代码很长,但是可读性很好。它基本上为importCSV按钮添加了一个事件处理程序,首先调用 Python 代码来实际加载 CSV 文件,然后构建一个表来显示结果。它将 CSV 文件的路径传递给 Python 代码,作为从函数filebrowser返回的结果。用户通过滚动filebrowser窗口并触摸文件来选择要读取的 CSV 文件,这将关闭filebrowser窗口。

buttons.importCSV.addEvent("click",function(){ // Override the onClose function to use the path of the CSV file filebrowser.onClose = function(a) { if(a){ handler.startLoad("Loading","Loading CSV file"); var resp = handler.postAndWait({"task":"loadfile","path":a}); if(resp.error==""){ // resp.filename will definitely be same as a? if(resp.filename!=""){ clearMergedSamples(); divs.csvFilename.setText( resp.filename ); divs.fields.removeClass("nodisplay").getElement("div").remove(); var newdiv = new Element("div").addClass("col").addClass("width-100"); resp.fields.each(function(r, k){ newdiv.adopt(new Element("div").addClass("col") .adopt(new Element("input",{"type":"radio","name":"iField"}) .addEvent("click",function(){hideAll(valid);})) .adopt(new Element("span").setText(r)) ); }); divs.fields.adopt(newdiv); // Select the first item divs.fields.getElement("input").setProperty("checked",true); // More information about the loaded file $("dialectQuotechar").setText(resp.quotechar); $("dialectDelimiter").setText(resp.delimiter); $("dialectLineterminator").setText(resp.lineterminator); // Preview var t=new Element("table", {"cellpadding":"0","border":"0"}),th=new Element("tr"); resp.fields.each(function(v){ th.adopt(new Element("th").setText(v)); }); t.adopt(th); resp.rows.each(function(v){ var tr=new Element("tr"); v.each(function(w){ tr.adopt(new Element("td").setText(w)); }); t.adopt(tr); }); divs.preview.empty().adopt(t); } }else{ handler.alert("CSV import error",resp.error); } handler.stopLoad(); } filebrowser.close(); } filebrowser.setType("csv").setTitle("Load CSV file" ).show(); });

让应用的用户了解正在发生的事情总是一个好主意。当 SMS 合并应用第一次启动时,它需要加载配置文件(如果存在的话)。微调对话框完美地告诉用户,应用实际上是在加载一个配置文件,而不仅仅是留下一个可见的空白屏幕。图 9-17 显示了手机短信程序如何使用微调按钮让用户知道正在发生的事情。

images

图 9-17。设置配置页面

当您使用webViewShow API 函数在 HTML/JavaScript 和 Python 代码之间传递数据时,您必须在任一端编写一个事件处理程序来接收数据。SMS Sender 示例利用 JavaScript 和 Python 事件处理程序来完成工作。下面是在 JavaScript 端设置不同事件处理程序的一段代码:

handler = new UIHandler(); window.addEvent("domready",function(){ var buttons = {"saveconfig":$("bSaveConfig"),"file":$("bFile"), "setup": $("bSetup"),"importText":$("bChooseText"), "textTab":$("bText"),"merge":$("bMerge"), "validate":$("bValidate"),"process":$("bProcess"), "importCSV":$("bCSV"),"closebrowser":$("closeButton")}, divs = {"preview":$("dPreview"),"filebrowser":$("filebrowser"), "browsercontent":$("browserContent"),"fileTab":$("dFile"), "fields":$("dFields"),"csvFilename":$("csvfile"), "setupTab":$("dSetup"),"textTab":$("dText"),"mergeTab":$("dCSVMerged")}, dta=divs.textTab.getElement("textarea"),browserTitle=$("browserTitle"), tabs=[divs.setupTab,divs.mergeTab,divs.textTab,divs.fileTab], tabButtons=[buttons.setup,buttons.merge,buttons.textTab,buttons.file], validSpan=$("wValid"),invalidSpan=$("wInvalid"),valid=[validSpan,invalidSpan], templateRow=$("templateTable").getElement("tr"); tabButtons.each(function(button,k){ var current = "current"; button.addEvent("click",function(){ if(!button.hasClass(current)){ removeClassFromAll(tabButtons,current); button.addClass(current); showOne(tabs,tabs[k]); } }); });

在 Python 方面,必须有相应的事件处理程序。以下是手机短信程序的处理方式:

`class SMSSenderHandler(UIHandler):
""" Handler class for this particular application. Extends UIHandler """
def init(self):
UIHandler.init(self)

Create the dispatch dictionnary which maps tasks to methods

self.dispatch = {
"loadfile": self.loadfile,
"validate": self.validate,
"loadfilecontent": self.loadfilecontent,
"loadconfig": self.loadconfig,
"send": self.send,
"merge": self.merge,
"listdir":self.listdir,
"saveconfig":self.saveconfig
}`

手机短信程序的怪癖和陷阱

请注意,根据您安装的 SL4A 版本,SMS Sender 示例可能无法运行。我遇到了一些 SLA4 r3 和模拟器不能正确处理事件传递的问题。这在当时是一个已知的错误,并被如此报告。HTML 文件选择器也有一个问题,它似乎不允许您在打开文本或 CSV 文件后打开子目录。也就是说,它展示了一些可以在 Python 和 HTML 代码之间双向通信的方法。

总结

本章试图向您展示编写使用 HTML 显示信息并通过webViewShow API 调用与用户交互的脚本的基础。

以下是本章的要点:

  • HTML 基础知识:你所学到的关于优秀 HTML 的一切都适用于此。您可以使用 HTML 文件和几行代码构建简单的输出。
  • 学习一些 JavaScript :这本书的主题是用 Python 编程,但是要在 HTML 页面中实现交互,你必须写一些 JavaScript。学习这门语言并不困难,尤其是如果你对 C++或 Java 很熟悉的话。网上有很多资源可以帮助你开始。
  • 别忘了设计:对于程序员创建的网页,最大的抱怨之一就是它们看起来不太吸引人。SMS Sender 示例使用了许多良好的设计原则来分隔操作,并将相似的功能组合在一起。因为webViewShow API 函数使用 HTML 来创建用户界面,所以学习一点好的 HTML 页面设计是个好主意。
  • CSS 可以帮助:使用 CSS 实际上也是一个很好的编程实践。它有助于将编码中的一些设计方面分离到一个文件中。CSS 有助于给 HTML 带来一致的外观和感觉,并且非常适合小屏幕。

十、打包和分发

本章将介绍使用 Eclipse 和 QR 码打包和分发脚本的方法。

本章将涵盖以下主题:

  • 使用二维码分发脚本
  • 构建可分发的应用
  • 使用 Eclipse 创建一个.apk文件

虽然这本书的大部分内容都是关于创建供个人使用的脚本,但是使用 SL4A 构建一个商业 Android 应用还是很有可能的。一旦完成,你需要一种方法来分发你的应用,让其他人也能享受它。这一章将会介绍几种你可以做到的方法。

二维码

如果你有一个相对较短的脚本想要分享,快速响应(QR)码是发布你的作品的一个很好的方式。大多数 Android 设备都包含一个原生条形码扫描仪应用(ZXing),SL4A 甚至支持直接将二维码导入编辑器。它也可以从 Android 市场获得。当您启动 SL4A 时,您应该会看到设备上的scripts目录中的文件列表。如果你按下硬件菜单按钮,你会在左上角看到一个添加按钮(见图 10-1 )。

images

图 10-1。菜单按钮弹出对话框

如果您按下添加,您将得到一个菜单,其中包含任何已安装的解释器、Shell 和扫描条形码的文件(参见图 10-2 )。

images

图 10-2。添加菜单

如果你在谷歌上快速搜索 SL4A 二维码,你会发现很多条目,人们在博客或个人网站上使用二维码分享他们的脚本。一个二维码只能编码 4296 个字符的内容,所以你的脚本必须简短。有几个网站可以粘贴文本,并为您创建一个二维码。SL4A 维基参考资料http://zxing.appspot.com/generator。以下是附带的说明:

  1. 打开内容下拉列表,然后选择文本。
  2. 在文本内容的第一行,输入脚本的名称(例如,hello_world.py)。
  3. 在那下面,粘贴脚本内容。
  4. 打开大小下拉列表并选择 l。
  5. 单击生成。
  6. 嵌入生成的条形码图像或与朋友分享。

图 10-3 显示了使用此处显示的makeToast.py代码从[zxing.appspot.com/generator](http://zxing.appspot.com/generator)生成二维码的结果:

`import android

droid = android.Android()
name = droid.getInput("Hello!", "What is your name?")
droid.makeToast("Hello, %s" % name.result)`images

图 10-3。二维码生成使用http://zxing.appspot.com

如果你有一个简短的脚本要分享,并且有一个分享的地方,比如博客或网站,二维码给了你一个很好的选择。

应用包

Android 应用通常以扩展名为.apk的单个文件或包分发。Android 包本质上是一个类似于.jar.zip文件的存档文件。每个.apk包含一些必须存在的强制文件,否则应用不会安装。最重要的文件是AndroidManifest.xml.这个文件描述了应用在上下文中所需的资源和权限。根据 Android 文档,除了声明应用的组件之外,清单还做了许多事情:

  • 标识应用需要的任何用户权限,例如对用户联系人的 Internet 访问或读取权限
  • 根据应用使用的 API,声明应用所需的最低 API 级别
  • 声明应用使用或需要的硬件和软件功能,如摄像头、蓝牙服务或多点触摸屏
  • 指定应用需要链接的 API 库(除了 Android 框架 API),例如 Google Maps 库

创建 Android 项目有很多选择。一种方法是从命令行手动创建一个新项目。它包括使用android命令和一些参数。图 10-4 显示了在 Windows 命令提示符下运行该命令的结果。

images

图 10-4。命令行项目创建

当您使用命令行工具android来构建您的项目时,它会在AndroidManifest.xml文件中正确设置。下面是从的命令行得到的那个文件的样子图 10-4 :

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myfirstapp" android:versionCode="1" android:versionName="1.0"> <application android:label="@string/app_name" android:icon="@drawable/icon"> <activity android:name="MyFirstApp" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

命令行方法创建了一个基本的项目框架,需要做一些调整才能使它像 SL4A 项目一样工作。幸运的是,SL4A 人员已经完成了大部分工作。你需要做的第一件事就是从 SL4A 项目网站(http://android-scripting.googlecode.com/hg/android/script_for_android_template.zip)下载脚本模板文件。图 10-5 显示了script_for_android_template.zip文件中的内容。

images

图 10-5。script _ for _ Android _ template . zip 文件内容

提供的AndroidManifest.xml文件包含您明确授予访问权限的项目或属性列表。SL4A 站点提供的模板文件包含了一个完整的列表,但是大部分条目都被注释掉了。它将如下所示:

<!-- <uses-permission android:name="android.permission.VIBRATE" /> -->

每个有效的权限行应该如下所示:

<uses-permission android:name="android.permission.VIBRATE"></uses-permission> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>

一旦你下载了模板,你就可以开始构建你的可发布项目,也就是所谓的.apk文件。最简单的方法是使用 Eclipse。我将使用虚拟脚本模板带您完成这些步骤。第一步是将模板导入 Eclipse。图 10-6 和 10-7 显示了您需要浏览的两个对话框。

images

图 10-6。 Eclipse 项目导入对话框

当你点击下一步按钮时,你应该会看到一个类似于图 10-7 中的对话框。如果您在选择归档文件选项的同一行单击浏览按钮,您将能够导航到该目录并选择script_for_android_template.zip文件。

images

图 10-7。 Eclipse 项目导入对话框:归档文件选择

在构建项目之前,必须在属性页上进行一项更改。为此,请从“窗口”菜单中打开“首选项”对话框。展开 Java 菜单项,后跟构建路径。此时,你的对话框应该类似于图 10-8 中的对话框。选择“类路径变量”项,然后单击“新建”按钮。这将弹出另一个类似于图 10-9 中的对话框。

images 注意您可能需要在ScriptForAndroidTemplate下添加一个名为gen的目录。我第一次尝试构建项目时遇到了一个错误,因为这个目录丢失了。script_for_android_template.zip文件的后续版本可能会对此进行更正。

images

图 10-8。 Eclipse 项目首选项对话框

ANDROID_SDK 变量必须指向您的 Android SDK 的安装路径。在我的情况下,这是在我的下载目录下。如果您在 Windows 上使用 SDK 的安装程序可执行文件,您的路径可能类似于C:/Program Files/Android/android-sdk/。最好的办法是单击文件夹按钮并导航到目录。

images

图 10-9。新建类路径变量条目对话框

如果您展开新导入的ScriptForAndroidTemplate,您应该在 Eclipse Pydev Package Explorer 窗口中看到类似于图 10-10 的内容。

images

图 10-10。导入模板项目的浏览器视图

此时,您应该已经准备好构建项目了。首先从项目菜单中运行清理工具来确保旧项目或以前的构建没有任何问题,这是一个不错的主意。我养成了每次都这样做的习惯,只是为了更好地衡量。如果项目构建成功,您应该不会在 Problems 选项卡上看到任何条目(参见图 10-11 )。

images

图 10-11。问题和控制台标签应该是空的

至此,我们已经有了一个准备打包的 Android 应用。这是 Eclipse 真正闪光的地方。在“文件”菜单上,选择“导出”。你应该会看到一个类似图 10-12 的对话框。

images

图 10-12。 Eclipse Android 包导出对话框

点击下一步按钮将弹出一个类似于图 10-13 中的对话框。此对话框让您知道您将要导出当前设置为可调试的项目。在开发过程中,这不是问题,但是在将应用发布给其他人使用之前,您会希望对其进行更改。

images

图 10-13。 Eclipse 导出项目检查

接下来的三个对话框处理应用的签名。每个 Android 应用在安装之前都必须进行数字签名。如果这是您第一次经历这个过程,那么您必须生成一个新的密钥库和一个要使用的密钥。点击图 10-13 中对话框的下一步按钮,将出现图 10-14 中所示的对话框。

images

图 10-14。项目密钥库选择对话框

在这里,您可以选择一个文件来保存您的密钥库和一个密码来保护它。密码必须至少包含六个字符,并且应该是您容易记住的内容。单击“下一步”按钮会将您带到另一个对话框,您将在其中输入生成新密钥的信息。图 10-15 显示了密钥创建对话框。

images

图 10-15。密钥创建对话框

请注意有效性字段。您可以创建一个有效期为任意年数的密钥,从 1 到 99 这样的大数字。最后一个对话框允许你指定.apk文件的位置。

images

图 10-16。文件.apk的目的目录

现在我们已经生成了一个.apk文件,我们可以在模拟器中测试它。有两种方法可以做到这一点:直接从 Eclipse 或者从命令行使用 ADB 工具。我个人更喜欢命令行,但我是相当老派的。要使用 ADB 进行安装,请打开终端窗口,将当前目录更改为您选择作为.apk文件目标的目录,并键入以下内容:

adb install ScriptForAndroidTemplate.apk

如果安装成功完成,您应该会在模拟器中看到一个名为虚拟脚本的条目,如图 10-17 中的所示。

images

图 10-17。安装了虚拟脚本的模拟器屏幕

如果你将ScriptForAndroidTemplate.apk文件安装到没有安装 SL4A 的设备上,你会看到一个弹出对话框,如图 10-18 中的所示。

images

图 10-18。缺少 Python 解释器提示

单击 Yes 按钮将引导您完成为 SL4A 安装 Python 解释器的过程。一旦该过程完成,您应该能够通过单击它来运行虚拟脚本应用。如果您碰巧没有正确设置AndroidManifest.xml文件中的所有权限,您会得到类似于图 10-19 中的通知。

images

图 10-19。权限通知缺失

要解决这个问题,您必须手动编辑AndroidManifest.xml文件,或者在 Eclipse 中打开该文件并在那里进行更改。Eclipse 方法要安全和快速得多,所以我们将在这里讨论它。要打开该文件,只需在 Package Explorer 窗口中双击AndroidManifest.xml。你应该会看到一个类似于图 10-20 中的对话框。

images

图 10-20。 Eclipse Android 清单权限标签

从图 10-20 中可以看到,这个AndroidManifest.xml文件中唯一的权限是允许访问互联网。如果你点击添加按钮,你会看到一个类似图 10-21 的对话框。

images

图 10-21。创建一个新的 Android 清单权限元素

我们需要选择使用权限来添加新元素。选择使用权限,然后单击确定按钮。接下来,您需要使用下拉框选择一个权限名称,该下拉框包含所有允许的值供您选择。我们需要标有android.permission.VIBRATE的那个。图 10-22 显示了选择的数值。

images

图 10-22。选择 android.permission.VIBRATE

完成后,您可以单击 Eclipse 主菜单下的小磁盘图标来保存您的更新。现在,您需要返回项目清理和导出过程,以创建一个新的.apk文件。

打包自己的应用

既然您已经知道如何使用模板打包应用,我们将使用相同的基本方法来打包我们自己的应用。对于单个 Python 脚本文件来说,这个过程非常简单。首先,在 Eclipse 中右键单击项目,然后从菜单中选择 copy,制作模板的副本。接下来,右键单击 Package Explorer 窗口的空白区域,并从菜单中选择 Paste。这将呈现一个类似图 10-23 的弹出窗口。为您的新项目命名,然后单击 OK。

images

图 10-23。 Eclipse 复制项目对话框

现在到了插入脚本的部分。复制您的脚本并将其粘贴到res/raw目录中。这里最简单的事情就是删除现有的script.py文件,并将你的脚本重命名为script.py。这样,您就不必更改引用script.py的任何其他位置。你还需要重命名默认的包com.dummy.fooforandroid / your_package_name 。您可以使用 Eclipse 重构/重命名工具来完成这项工作。然后需要更新AndroidManifest.xml中的package属性来引用 your_package_name

此时,您应该能够完成构建和导出过程,为您的脚本创建一个.apk文件。

蚂蚁建筑

对于真正的铁杆命令行迷来说,还有 Ant。如果你想走这条路,你需要一个 Mac OS X 或者 Linux 盒子。配置脚本是.sh文件,因此它们必须在这些操作系统的终端上运行。首先,您需要下载并解压缩上一节中使用的相同模板文件。您还需要将ANDROID_SDK变量设置为指向 Android SDK 的根目录。这看起来是这样的:

unzip -d <path/project_directory> script_for_android_template.zip export ANDROID_SDK=<SDK_root>

接下来,您需要如下执行configure_package.sh脚本:

sh configure_package.sh <your_fully_qualified_package_name>

如果您在模板中配置实际的虚拟包,命令应该是这样的:

sh configure_package.sh com.dummy.fooforandroid

此时,您需要将您的 Python 脚本复制到res/raw目录中,并替换现有的script.py文件。同样,如果你把你的剧本重新命名为script.py,会更容易。您将需要手动编辑AndroidManifest.xml来取消注释您的脚本需要的所有权限。实际的构建和运行过程使用了run-tests.sh脚本。要构建您的包,您需要打开一个终端窗口并导航到您的项目目录的根目录。命令ant debug将在项目/bin目录下创建一个名为<*your_project_name*>-debug.apk.apk文件。该文件将使用调试密钥签名,并使用zipalign工具对齐。

构建发布版本稍微复杂一些。首先,您必须用合适的证书签署您的应用。如果您计划在 Android market 上发布您的应用,您的有效期必须在 2033 年 10 月 22 日之后结束。调试证书使用以下默认值:

  • 密钥库名称:"debug.keystore"
  • 密钥库密码:"android"
  • 按键别名:"androiddebugkey"
  • 密钥密码:"android"
  • CN: "CN=Android Debug,O=Android,C=US"

您的私有释放密钥必须为所有这些值使用不同的字段。对于私钥,您有两种选择:从证书发行商那里购买一个或者自己创建一个。Java 开发工具包(JDK)附带了一个keytool工具,它将为您生成一个自签名密钥。你还需要 JDK 的jarsigner工具。下面是生成私钥的命令行示例:

keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA –keysize 2048 -validity 10000

使用有效的密钥,您可以使用命令ant release构建应用的发布版本。默认情况下,Ant 构建脚本编译应用.apk而不对其进行签名。您必须使用jarsigner工具来实际签署.apk文件。您可以使用以下命令来完成它:

jarsigner -verbose -keystore my-release-key.keystore my_application.apk alias_name

验证您的.apk文件是否正确签名是个好主意。您也可以使用jarsigner命令:

jarsigner -verify my_signed.apk

如果您想了解更多信息,可以添加-verbose-certs。此时,剩下的就是运行zipalign工具来确保所有未压缩的数据都正确对齐。这个工具实际上是调整最终的包,使所有文件都在 4 字节边界上对齐。这极大地提高了应用加载性能,并减少了正在运行的应用消耗的内存量。下面是运行zipalign的命令行:

zipalign -v 4 *your_project_name*-unaligned.apk *your_project_name*.apk

这应该是创建一个完全可发布的 Android 应用所需的最后一步。最后,您可能希望考虑更新您的模板项目,以包含核心 SL4A 可执行文件的最新版本,因为它们会不断更新。为此,您需要下载最新版本的script_for_android_teplate.zip并解压以下文件:

libs/script.jar libs/armeabi/libcom_googlecode_android_scripting_Exec.so

将这些文件复制到项目中的相同位置,然后使用 Eclipse 进行刷新清理构建或使用 Ant 进行重建。

编译 SL4A

如果您想确保您拥有 SL4A 的绝对最新和最棒的版本,您必须从源代码编译它。如果您正在寻找一个稳定的版本,这可能有点冒险,但是它也可能修复您的应用需要的一个问题。不管怎样,如果你想编译 SL4A,这就是你需要做的。您需要做的第一件事是获得 SL4A 源代码树的副本。你要知道 SL4A 是用 Mercurial 做源代码管理工具的。你可以从它的下载页面([mercurial.selenic.com/downloads](http://mercurial.selenic.com/downloads))获得一份适用于各种 Linux 发行版、Mac OS X 和 Windows 的 Mercurial 客户端。

出于本章的目的,我将在 Windows 7 64 位机器上使用 TortoiseHg。下载页面提供了许多选项,包括一些不需要管理员权限的选项。我选择了 TortoiseHg 2.0.4 和 Mercurial 1 . 8 . 3–x64 Windows 选项。此选项提供了与 Windows 资源管理器的集成,使得将任何存储库克隆到本地驱动器上的特定位置变得非常简单。一旦您安装了客户端,您将需要克隆源代码树。在 Windows 中,你可以在文件资源管理器中右键单击你想要创建克隆的目录,然后选择 TortoiseHg 和 clone,如图图 10-24 所示。

images

图 10-24。创建 SL4A 源树的克隆

选择克隆选项将启动另一个对话框,您必须在其中指定存储库的源 URL 和本地计算机上的目标位置。我使用的网址如下:

https://rjmatthews62-android-scripting.googlecode.com/hg/

实际的官方网址是:

https://android-scripting.googlecode.com/hg/

在撰写本文时,这似乎是包含所有补丁和更新的最新位置。图 10-25 显示了您必须输入该 URL 的对话框。

images

图 10-25。选择 SL4A 源树位置

一旦下载了整个树,就需要将其导入 Eclipse。为此,打开 Eclipse 并从 File 菜单中选择 Import。因为文件已经存在于本地磁盘上,所以您必须使用选择根目录选项。单击浏览按钮导航到执行克隆操作的位置。图 10-26 显示了选择克隆目录后的对话框。

images

图 10-26。从本地目录导入 Eclipse】

此时,您不需要克隆的源代码树中的所有项目。您可以通过右键单击每个项目并选择“关闭项目”来删除以下内容:

  • BeanShellForAndroid
  • DocumentationGenerator
  • InterpreterForAndroidTemplate
  • JRubyForAndroid
  • LuaForAndroid
  • PerlForAndroid
  • RhinoForAndroid
  • TclForAndroid

现在,您应该准备好执行项目Image构建,然后执行项目Image清理Image全部清理。我不得不再次将gen目录添加到许多项目中。一旦这样做了,你应该做一个干净的构建,一切都应该是好的。此时你应该会看到一个类似图 10-27 的月食窗口。

images

图 10-27。建造 SL4A 后的月食窗口

现在我们需要添加我们的模板项目来创建我们的最终应用。为此,右键单击ScriptForAndroidTemplate文件夹并制作副本。然后通过右键单击包资源管理器区域并选择粘贴来粘贴新副本。这将是我们的目标应用。要将这个副本连接到 SL4A 克隆,您需要展开项目并右键单击build.xml文件。选择运行方式,然后选择 Ant 构建。如果需要,您可以在此时重命名您的项目。又一个干净的构建,你应该有一个工作的.apk准备好测试了。

要测试该应用,要么将一个真实的设备连接到您的工作站,要么直接使用模拟器。在 Eclipse 中,你只需右击模板的副本,选择运行方式,然后选择 Android 应用(见图 10-28 )。

images

图 10-28。建造 SL4A 后的月食窗口

此时,您的应用有了一个.apk文件,SL4A 有了一个.apk文件。如果你分发你的应用.apk文件,会提示用户必须先安装 Python 2.6.2(参见图 10-18 )。

点睛之笔

如果你打算将你的剧本公之于众,你需要做一些调整。默认模板包括一个名为res的资源目录。在这个目录中有许多子目录,包含应用使用的各种文件——包括当您浏览设备上的应用时会看到的代表图标的图像,以及将出现在该图标下的名称。要更改名称,您需要编辑values子目录中的strings.xml文件。下面是该文件在默认模板中的样子:

<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World!</string> <string name="app_name">Dummy Script</string> </resources>

要更改名称,只需更改"app_name">Dummy Script行来反映应用的名称。您可能想要更改的另一件事是应用图标。为此,您可以使用 Android SDK 提供的draw9patch工具。这可以通过简单地输入draw9patch从终端窗口启动。图 10-29 显示了加载了默认 SL4A 脚本标志的draw9patch app。

images 注意安卓图标默认使用.png格式。术语九补丁指的是一个标准的 PNG 图像,包括一个 1 像素宽的边框。它通常用于图像必须拉伸以适应不同长度的文本标签的按钮。

images

图 10-29。载入 SL4A 图标的 Draw9patch 应用

一旦程序运行,你可以拖拽一个图像并放到打开的窗口中,或者使用文件Image打开 9 补丁选项。当你完成后,在文件菜单上有一个保存 9 补丁选项来保存你的工作。

蜿蜒向下

SL4A 为希望开发市场现成应用的有抱负的程序员和希望自动化一些功能以使他们的移动生活更轻松的精明的智能手机用户提供了理想的解决方案。对于精通 Python 的人来说,这是利用他们的编程技能使用任何 Android 设备的绝佳机会,就像使用台式机或笔记本电脑一样。对一些人来说,甚至有可能用基于安卓系统的平板电脑取代笔记本电脑。随着移动设备处理和存储能力的提高,这种可能性只会越来越大。

SL4A 真正伟大的地方在于它的开源特性。随着该项目越来越广为人知,将有更多的用户转化为更广泛的受众和更大的参与。开发工作的新贡献者增加了重要的新特性,例如使用任何原生 Python 库的能力。对谷歌电视等其他 Android 平台的更新应该也允许 SL4A 在那里运行。Google groups 上有一个相当活跃的论坛,你可以在那里提问并获得帮助。

试用 SL4A 并没有你想象的那么难。你真的不能做任何对你的设备直接有害的事情,尽管它可能会增加你的数据账单,这取决于你的脚本做什么。最安全的入门方式是使用 Android 模拟器。通读本书中的章节将为您使用 SL4A 让您的 Android 设备做您从未想过可能的事情打下坚实的基础。

总结

本章非常详细地描述了如何为 SL4A 脚本构建可分发的包。

以下是本章的要点:

  • 创建二维码:二维码给你一个快速简单的方法来分发任何人都可以直接加载到 Android 设备上的简短脚本。
  • 构建 .apk 文件:如果你想使用 Android market 发布你的应用,你必须学会如何构建.apk文件。
  • 使用 Eclipse:它使得构建和测试可分发应用的过程变得更加容易。
  • 美化你的应用:如果你想让你的用户真正使用它,你真的需要花些时间为你的应用创建一个图标。
posted @   绝不原创的飞龙  阅读(160)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示