安卓应用与-Eclipse-教程-三-

122 阅读34分钟

安卓应用与 Eclipse 教程(三)

原文:Android Apps with Eclipse

协议:CC BY-NC-SA 4.0

十、项目:使用 HTML 和 JavaScript 的电影播放器

在这一章中,我们将使用 SL4A 框架用 HTML 和 JavaScript 重新实现我们在第五章中开发的电影播放器应用。由于 SL4A 框架没有提供电影播放器应用所需的所有功能,我们将更进一步,将一个新的自定义外观集成到 SL4A 框架中。我们将把生成的应用打包成一个独立的 Android 脚本应用,可以像普通的 Android 应用一样进行分发和部署。

获取 SL4A 源代码

我们的电影播放器应用需要访问媒体商店内容提供商,以便查询设备上的电影文件列表。现有的 SL4A 版本 R5 外观都不提供对媒体商店的访问。作为这个例子的一部分,我们将开发一个新的外观,使脚本应用能够从平台获取必要的信息。SL4A 框架目前不支持新外观的动态发现,并且仅限于使用内置外观。

T 要添加新的外观,SL4A 框架源代码需要稍微修改一下。为了实现这一点,在本节中,我们将从 SL4A 源代码库中检查 SL4A 源代码。

准备工作空间

SL4A R5 源代码要求在主机上安装 Android API level 3、4、7 和 8 平台 SDK。如第五章所述,从顶部菜单栏选择窗口 Image ** Android SDK 管理器**启动 SDK 管理器,并将这些 SDK 下载到主机上。

SL4A 项目依赖于 Eclipse 中预定义的ANDROID_SDK classpath 变量来编译。要定义这个变量,打开 Eclipse Preferences 对话框,如前面章节所述。使用搜索框,过滤类路径变量的首选项列表。然后单击“新建”按钮启动“新变量输入”对话框。如下定义新的变量条目,如图 10-1 所示:

  • **名称:**设置名称为ANDROID_SDK
  • Path: 变量应该指向 Android SDK 在主机上的位置。使用右边的文件夹按钮,选择 Android SDK 目录。

Image

图 10-1。 为 SL4A 源代码添加 ANDROID_SDK 类路径变量

设置 Java 编译器合规级别

SL4A 源代码基于 Java 源码 1.6 版。如果您已经按照第五章中的建议安装了 JDK 版本 6,则不需要额外的配置。否则,为了编译 SL4A 源代码,需要将 workspace 编译器兼容级别更改为 1.6。要更改符合性级别,请打开 Eclipse Preferences 对话框,并使用搜索框过滤编译器的首选项列表。选择 Java 编译器首选项,将兼容级别改为 1.6,如图图 10-2 所示。

Image

图 10-2。 改变工作区 Java 编译器兼容级别

安装水星

SL4A 源代码通过 Google Code 网站作为 Mercurial 源代码库。要将 SL4A 源代码签出到主机,需要安装 Mercurial 和 Mercurial Eclipse 插件。在 Mac OS X 和 Linux 平台上,需要在下载 Mercurial Eclipse 插件之前安装 Mercurial 二进制文件。在本节中,我们将介绍在这些平台上安装 Mercurial 二进制文件的过程。

在 mac os x 上安装 mercurial

使用您的 web 浏览器,导航到位于[mercurial.selenic.com](http://mercurial.selenic.com)的 Mercurial 下载站点,下载 Mac OS X 的二进制文件。如图 10-3 所示,Mercurial 网站会自动检测您的操作系统,并为 Mercurial 安装程序提供一个下载按钮。

Image

**图 10-3。**Mac OS X 平台的 Mercurial 下载页面

单击下载按钮,将 Mercurial 安装程序 ZIP 存档文件下载到您的主机上。接下来,进入你的Downloads文件夹。根据 Mac OS X 的版本,ZIP 文件可能会自动解压缩,或者您可能需要手动解压缩。此 ZIP 文件包含 Mercurial 二进制文件的 Mac OS X 安装包。

双击可安装软件包文件以启动 Mercurial 安装程序,该程序将引导您完成安装过程。安装完成后,可以在/usr/local/bin/hg找到 Mercurial 二进制文件。要验证 Mercurial 安装,请打开一个终端窗口,并在命令提示符下输入hg。如果你能看到如图图 10-4 所示的 Mercurial 基本命令列表,你的 Mercurial 安装就成功了。现在可以继续安装 Mercurial Eclipse 插件了。

Image

图 10-4。 验证 on Mac 上的 Mercurial 安装

在 Linux 上安装 Mecurial

Mercurial 二进制文件可以通过大多数 Linux 发行版上的应用库获得。打开一个终端窗口,根据您的 Linux 发行版,执行相应的安装命令:

  • debian/Ubuntu:??]
  • 开口:??sudo zipper in mercurial
  • Fedora: sudo yum install mercurial
  • Gentoo:

根据您的 Linux 发行版,Mecurial 安装目录可能会有所不同。要找到 Mercurial 二进制文件的位置,请打开终端窗口,并在命令提示符下输入which hg。如果你能看到 Mercurial 的安装目录,如图图 10-5 所示,你的 Mercurial 安装成功了。

Image

图 10-5。 验证 Linux 上的 Mercurial 安装

安装水星 Eclipse 插件

要安装 Mecurial Eclipse 插件,在 Eclipse 中,从顶部菜单栏选择帮助 Image 安装新软件… 来启动安装向导。Mercurial 不是官方 Eclipse 软件站点的一部分。点击添加按钮,定义位置为[cbes.javaforge.com/update](http://cbes.javaforge.com/update)的 Mercurial Eclipse 软件站点,如图图 10-6 所示。

Image

图 10-6。 给 Eclipse 添加 Mercurial 软件站点

添加新的软件站点后,Eclipse 将获取 Mercurial 插件列表,并在安装向导中显示它们。此过程可能需要一些时间,具体取决于您的网络连接。从这个插件列表中,选择 Mercurial Eclipse。对于 Windows 平台,同样选择 Mercurial 的 Windows 二进制,如图图 10-7 所示。

Image

图 10-7。 选择 Mercurial 插件

单击“下一步”按钮继续安装。Eclipse 将列出将要安装的插件。单击“完成”按钮开始安装。

检查 SL4A 源代码

安装好 Mecurial 和 Mecurial Eclipse 插件后,我们就可以检查 SL4A 源代码了。从顶部菜单栏选择文件 Image 新建 Image 其他… ,展开 Mercurial 类别,选择克隆已有的 Mercurial 库,如图 10-8 所示。

Image

图 10-8。 选择克隆现有的 Mercurial 库

在向导的 URL 字段中,输入[code.google.com/p/android-scripting/](https://code.google.com/p/android-scripting/)作为存储库位置,如图图 10-9 所示,然后点击 Next 按钮。Mercurial 是一个分布式源代码控制系统,这意味着它会将整个存储库克隆到主机上。此过程可能需要几分钟,具体取决于您的网络连接。

Image

图 10-9。 设置用于克隆的存储库位置

SL4A 的最新官方版本是 R5。在撰写本文时,SL4A 版本 R5 在源代码库中还没有标记。为了将代码库签出到 R5 版本,切换到修订选项卡,输入 1214 作为修订号,如图图 10-10 所示。单击“下一步”按钮继续。

Image

图 10-10。 选择 SL4A R5 改版

Mercurial 克隆库向导将显示 SL4A 库中所有项目的列表,如图 10-11 所示。虽然示例项目不会使用所有的 SL4A 项目,但是选择将除了DocumentationGenerator之外的所有项目导入 Eclipse。

Image

图 10-11。?? 选择要导入的项目

Eclipse 将自动开始构建所有的 SL4A 项目。检查问题视图并解决任何报告的问题。

电影播放器脚本项目

正如上一章所讨论的,SL4A 提供了一个模板项目,用于将脚本打包成独立的 Android 包。模板项目的源代码称为ScriptForAndroidTemplate。示例项目将使用模板项目作为其基础。

克隆模板项目

我们将在不同的项目名称下克隆模板项目,而不是直接修改模板项目。选择ScriptForAndroidTemplate项目,右键单击,从上下文菜单中选择复制。再次右击并从上下文菜单中选择粘贴来启动复制项目向导。将新项目命名为MoviePlayerScript,如图图 10-12 所示。

Image

图 10-12。 将模板项目克隆为电影播放器脚本

Eclipse 克隆整个项目设置,包括 Mercurial 元数据。因为新项目不是 SL4A 源代码库的一部分,所以右键单击项目名称并选择TeamImageDisconnect将其从 Mercurial 中分离。

链接到 SL4A 框架代码

MoviePlayerScript项目是 SL4A 模板的一个完全相同的克隆。SL4A 模板项目是一个独立的项目,除了解释器之外,没有任何外部依赖性。

SL4A 框架代码被预编译,并作为一个 JAR 文件与 SL4A 模板项目一起提供。因为我们将在这个示例项目中修改 SL4A 框架,所以我们需要删除这个 JAR 文件,并使该项目直接依赖于 SL4A 框架项目。使用 Package Explorer 视图,展开MoviePlayerScript项目下的libs目录,并删除script.jar文件。在删除文件之前,Eclipse 会显示一个确认对话框。

接下来,右键单击项目并选择首选项来启动项目首选项对话框。从左窗格中选择 Java 构建路径,然后切换到库选项卡。使用移除按钮,将script.jar从项目构建路径中移除,如图图 10-13 所示。

Image

图 10-13。 从项目中移除预编译的 SL4A 库

为了使MoviePlayerScript项目直接依赖于 SL4A 框架,切换到 Projects 选项卡并单击 Add 按钮。如图 10-14 所示,选择添加BluetoothFacadeCommonInterpreferForAndroidScriptingLayerSignalStrengthFacadeTextToSpeechFacadeUtilsWebCamFacade。然后单击确定按钮保存选择。

Image

图 10-14。 为 SL4A 框架选择所需项目

除了依赖这些项目之外,这些项目的输出还应该与MoviePlayerScript项目打包在一起,以便在 Android 设备上执行。为此,切换到订单和导出选项卡,并选择相同的导出项目列表,如图图 10-15 所示。Eclipse 将重新构建项目。此时,尝试在您的 Android 设备上运行项目,以确保项目配置成功。

Image

图 10-15。 标注 SL4A 项目出口

重命名项目包

由于MoviePlayerScript项目是 SL4A 模板项目的克隆,它共享相同的 Android 包名。为了防止在部署MoviePlayerScript项目时出现任何可能的冲突,打开AndroidManifest.xml文件并将包名改为com.apress.movieplayerscript。要重命名 Java 包,右击com.dummy.fooforandroid包并选择重构 Image 重命名

添加电影外观

为了提供对媒体商店的访问,需要开发新的外观并将其添加到 SL4A 框架中。为了最小化实际 SL4A 框架代码的变化量,我们将为外观实现创建一个单独的项目。

从顶部菜单栏选择文件 Image 新建 Image ** Java 项目**,并将 Java 项目命名为MovieFacade,如图图 10-16 所示。点击Next按钮继续。

Image

图 10-16。 创建电影艺术学院项目

在下一个屏幕上,选择“项目”选项卡,并添加 Common 和 Utils 项目作为项目依赖项,如图 10-17 所示。MovieFacade将使用这些项目中的组件作为 SL4A 框架的一部分。

Image

图 10-17。 添加 MovieFacade 项目依赖

MovieFacade也将使用来自 Android 框架的组件;但是,它不是一个 Android 项目。我们需要将 Android 框架库添加到项目中。切换到库选项卡,并单击添加变量…按钮。在列表中选择 ANDROID_SDK,点击编辑按钮将其值改为ANDROID_SDK/platforms/android-7/android.jar,如图图 10-18 所示。根据您将在外观中使用的 Android 功能,您可以用所需的适当 API 级别替换android-7。点击 OK 按钮保存变量,然后点击 Finish 按钮将库更改应用到项目中。

Image

图 10-18。 将 Android 框架作为库添加到 movie cade

创建 MovieFacade 类

为了在 SL4A 框架中充当门面,MovieFacade项目需要扩展com.google.android_scripting.jsonrpc.RpcReceiver类。选择MovieFacade项目,从顶部菜单栏中选择文件 Image 新建 Image 。在com.apress.movieplayerscript包中定义MovieFacade类,如图图 10-19 所示。

Image

图 10-19。 定义电影学院类

MovieFacade将包含一个暴露给脚本的方法moviesGet。SL4A 希望使用必要的 RPC 属性对公开的方法进行注释。SL4A 通过com.googlecode.android_scripting.rpc包提供了以下 RPC 属性:

  • Rpc:该属性用于将方法标记为通过 RPC 公开。它还提供了该方法的简要文档,包括其返回值。
  • RpcParameter:该属性用于记录方法的参数。
  • RpcOptional:该属性用于将参数标记为可选。
  • RpcDefault:该属性用于标记有默认值的参数。
  • RpcMinSdk:该属性用于指定执行该方法所需的最低 Android SDK 级别。
  • RpcStartEvent:该属性用于标记启动事件生成的方法。
  • RpcStopEvent:该属性用于标记终止事件生成的方法。

MovieFacade类的源代码显示在清单 10-1 中。

清单 10-1。MovieFacade.java档案

`package com.apress.movieplayerscript;

import java.util.LinkedList; import java.util.List;

import org.json.JSONException; import org.json.JSONObject;

import android.app.Service; import android.content.ContentResolver; import android.database.Cursor; import android.provider.MediaStore; import android.util.Log;

import com.googlecode.android_scripting.facade.FacadeManager; import com.googlecode.android_scripting.jsonrpc.RpcReceiver; import com.googlecode.android_scripting.rpc.Rpc;

/**  * Movie facade.  *  * @author Onur Cinar  / public class MovieFacade extends RpcReceiver {     /* Log tag. */     private static final String LOG_TAG = "MovieFacade";

    /** Service instance. */     private final Service service;

    /**      * Constructor.      *      * @param manager      *            facade manager.      */     public MovieFacade(FacadeManager manager) {         super(manager);

        // Save the server instance for using it as a context         service = manager.getService();     }

    @Override     public void shutdown() {

    } /**      * Gets a list of all movies.      *      * @return movie list.      * @throws JSONException      */     @Rpc(description = "Returns a list of all movies.", returns = "a List of movies as Maps")     public List moviesGet() throws JSONException {         List movies = new LinkedList();

        // Media columns to query         String[] mediaColumns = { MediaStore.Video.Media._ID,                 MediaStore.Video.Media.TITLE, MediaStore.Video.Media.DURATION,                 MediaStore.Video.Media.DATA, MediaStore.Video.Media.MIME_TYPE };

        // Thumbnail columns to query         String[] thumbnailColumns = { MediaStore.Video.Thumbnails.DATA };

        // Content resolver         ContentResolver contentResolver = service.getContentResolver();        

        // Query external movie content for selected media columns         Cursor mediaCursor = contentResolver.query(                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediaColumns,                 null, null, null);

        // Loop through media results         if (mediaCursor.moveToFirst()) {             do {                 // Get the video id                 int id = mediaCursor.getInt(mediaCursor                         .getColumnIndex(MediaStore.Video.Media._ID));

                // Get the thumbnail associated with the video                 Cursor thumbnailCursor = contentResolver.query(                         MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI,                         thumbnailColumns, MediaStore.Video.Thumbnails.VIDEO_ID                                 + "=" + id, null, null);

                // New movie object from the data                 JSONObject movie = new JSONObject();

                movie.put("title", mediaCursor.getString(mediaCursor                         .getColumnIndexOrThrow(MediaStore.Video.Media.TITLE)));                 movie.put("moviePath", "file://" + mediaCursor.getString(mediaCursor                         .getColumnIndex(MediaStore.Video.Media.DATA)));                 movie.put("mimeType", mediaCursor.getString(mediaCursor                         .getColumnIndex(MediaStore.Video.Media.MIME_TYPE))); long duration = mediaCursor.getLong(mediaCursor                         .getColumnIndex(MediaStore.Video.Media.DURATION));                 movie.put("duration", getDurationAsString(duration));

                if (thumbnailCursor.moveToFirst()) {                     movie.put(                             "thumbnailPath",                             "file://" + thumbnailCursor.getString(thumbnailCursor

.getColumnIndex(MediaStore.Video.Thumbnails.DATA)));                 } else {                     movie.put("thumbnailPath", "");                 }

                Log.d(LOG_TAG, movie.toString());

                // Close cursor                 thumbnailCursor.close();

                // Add to movie list                 movies.add(movie);

            } while (mediaCursor.moveToNext());

            // Close cursor             mediaCursor.close();         }

        return movies;     }

    /**      * Gets the given duration as string.      *      * @param duration      *            duration value.      * @return duration string.      */     private static String getDurationAsString(long duration) {         // Calculate milliseconds         long milliseconds = duration % 1000;         long seconds = duration / 1000;

        // Calculate seconds         long minutes = seconds / 60;         seconds %= 60;

        // Calculate hours and minutes         long hours = minutes / 60;         minutes %= 60;

        // Build the duration string         String durationString = String.format("%102d:02d:%202d:%302d.02d.%403d",                 hours, minutes, seconds, milliseconds);

        return durationString;     } }`

MovieFacade在初始化时获取FacadeManager实例。FacadeManager允许访问 SL4A Android 服务实例。在与 Android 框架交互时,服务实例可以被外观用作 Android 上下文。moviesGet方法的实现部分借用了第五章示例项目,并修改为作为 RPC 方法运行。因为脚本不能直接使用 Java 类,所以moviesGet方法的返回类型被改为JSONObject列表。

注册外观

虽然现在已经正确定义了外观,但是 SL4A 框架还不知道它。MovieFacade需要向FacadeConfiguration类注册。

右键单击ScriptingLayer项目并选择 Properties。在属性对话框中,选择 Java Build Path,切换到 Projects 页签,添加MovieFacade作为依赖项,如图图 10-20 所示。

Image

图 10-20。 将 MovieFacade 作为依赖添加到脚本层

在同一个项目下,使用 Package Explorer 视图,展开 Sources 下的com.googlecode.android_scripting.facade包,并打开FacadeConfiguration类。

FacadeConfiguration类充当 SL4A 外观的注册表。SL4A 目前只允许在此手动注册外观。在类文件的顶部,在静态上下文中,外观被添加到sFacadeClassList集合中。如下面的代码所示,将标记在CHANGES BEGINCHANGES END注释之间的部分添加到FacadeConfiguration类中。

`    if (sSdkLevel >= 8) {       sFacadeClassList.add(WebCamFacade.class);     }

    // **** CHANGES BEGIN ****

    // Movie facade     sFacadeClassList.add(MovieFacade.class);

    // **** CHANGES END ****

    for (Class<? extends RpcReceiver> recieverClass : sFacadeClassList) {       for (MethodDescriptor rpcMethod : MethodDescriptor.collectFrom(recieverClass)) {         sRpcs.put(rpcMethod.getName(), rpcMethod);       }     }`

现在MovieFacade是 SL4A 框架的一部分,可以从脚本中使用。

导出电影外观

虽然MovieFacade已经在 SL4A 框架中正确注册,但是它仍然没有在MoviePlayerScript项目的导出列表中声明。右键单击MoviePlayerScript项目,选择 Java Build Path,将MovieFacade项目添加到项目列表中,并在 Order and Export 选项卡中将其标记为 Export。

添加脚本

项目的实际 UI 和应用逻辑将使用 HTML 和 JavaScript 实现。SL4A 框架依赖 Android 框架来呈现 HTML 和解释嵌入的 JavaScript 代码,它不需要下载解释器。

SL4A 模板项目附带了一个示例 Python 脚本。打开MoviePlayerScript项目,展开资源,从原始资源目录/res/raw中删除script.py Python 脚本文件。从顶部菜单栏选择文件 Image 新建 Image 文件,并添加一个script.html文件。您可以通过右击该脚本文件并从上下文菜单中选择打开 Image 文本编辑器来打开该脚本文件进行编辑。在运行时,SL4A 框架将使用其扩展名检测文件类型,并自动启动嵌入式 web 浏览器来执行脚本。

HTML 部分

脚本的 HTML 部分非常简单。如下面的代码所示,它只定义了一个 HTML div元素,用moviesid来保存电影列表。CSS 定义了浏览器将如何呈现电影项目。

`

             .movie {             border: 1px solid #000;             padding: 0.4em;         }         .thumbnail {             width: 4em;             height: 4em;             float: left;             margin-right: 0.4em;         }

        .title {             font-size: x-large;         }

        .clear {             clear: both;         }     

    

`

JavaScript 部分

该脚本将使用 JavaScript 通过 SL4A 框架与MovieFacade通信,以获取电影列表和相关信息。与所有其他脚本语言一样,脚本从初始化 Android 代理 RPC 客户端开始。

`    

        // Movie element         var moviesElement = document.getElementById("movies");`

populateMovies JavaScript 函数使用MovieFacade通过 SL4A 获得电影列表,遍历这些电影,并调用addMovie函数填充 UI。

`        /**          * Populate movies.           */         function populateMovies() {             // Get movies             var movies = droid.moviesGet().result;

            for (var i = 0, e = movies.length; i < e; i++) {                 var movie = movies[i];

                addMovie(                     movie["title"],                     movie["moviePath"],                     movie["mimeType"],                     movie["duration"],                     movie["thumbnailPath"]);                         }         }

        populateMovies();`

addMovie函数简单地定义了呈现电影项目所需的一组 HTML 元素,并将其添加到电影列表中。除了关于电影的可见信息之外,addMovie函数还将电影路径和 MIME 类型保存到电影元素中,以便在必要时能够检索它。

/**          * Add movie.          *          * @param title movie title.          */         function addMovie(title, moviePath, mimeType, duration, thumbnailPath) {             // Movie element             var movieElement = document.createElement("div");             movieElement.setAttribute("class", "movie"); `            movieElement.setAttribute("data-moviepath", moviePath);             movieElement.setAttribute("data-mimetype", mimeType);             movieElement.onclick = onMovieClick;

            // Thumbnail element             var thumbnailElement = document.createElement("img");             thumbnailElement.setAttribute("class", "thumbnail");             thumbnailElement.src = thumbnailPath;             movieElement.appendChild(thumbnailElement);

            // Title element             var titleElement = document.createElement("div");             titleElement.setAttribute("class", "title");             titleElement.innerHTML = title;             movieElement.appendChild(titleElement);

            // Duration element             var durationElement = document.createElement("div");             durationElement.setAttribute("class", "duration");             durationElement.innerHTML = duration;             movieElement.appendChild(durationElement);

            // Clear element             var clearElement = document.createElement("div");             clearElement.setAttribute("class", "clear");             movieElement.appendChild(clearElement);

            // Append movie to list             moviesElement.appendChild(movieElement);         }`

电影项目上的点击事件通过onMovieClick函数处理。onMovieClient函数提取由addMovie函数保存的电影路径和 MIME 类型,并依靠CommonIntentsFacade提供的view方法向 Android 平台发送一个启动所选电影项的默认播放器的意图。

`/**          * On movie click handler.          *          * @param e UI event.          */         function onMovieClick(e) {             // Get clicked movie item             var movieElement = e.currentTarget;

            // Movie path             var moviePath = movieElement.getAttribute("data-moviepath");

            // MIME type             var mimeType = movieElement.getAttribute("data-mimetype");

            // View movie             droid.view(moviePath, mimeType);         }     

`

运行应用

电影播放器脚本应用现在可以部署了,如第五章所述。由于 Android 模拟器中的一个已知错误,示例代码目前只能在 Android 设备上运行。因为示例应用将在外部存储中查找视频文件,所以请确保 Android 设备包含一个带有视频文件的 SD 卡,并断开 Android 设备与您的主机的连接以释放 SD 卡。当你运行应用时,你会看到电影列表,如图图 10-21 所示。

Image

图 10-21。 显示电影列表的电影播放器脚本应用

总结

在这一章中,我们深入到 SL4A 框架并探索了它的内部,包括外观注册和项目结构。SL4A 是一个开源项目,具有很强的可扩展性。您可以遵循本章示例中描述的相同步骤来扩展主 SL4A 应用ScriptingLayerForAndroid项目,以包含新的外观,并在以后通过任何脚本语言在本地或远程使用它们。

在本书中,我们探讨了 Android 平台的基础,以更好地理解其基础。我们研究了 Android 应用架构,并将这些新概念应用于我们的第一个 Android 应用,一个电影播放器。然后,我们通过集成本地代码库来扩展电影播放器应用,以支持其他视频格式。在开发的每个阶段,我们都采用了 Eclipse 提供的高级开发特性,例如快速导航、内容辅助、代码生成器以及调试和故障排除特性,以简化开发过程。

资源

以下资源可用于本章涵盖的主题:

  • Android 脚本层(SL4A),[code.google.com/p/android-scripting/](http://code.google.com/p/android-scripting/)
  • 水星月食,[javaforge.com/project/HGE](http://javaforge.com/project/HGE)

十一、附录一:测试 Android 应用

测试是应用开发周期中最重要的阶段之一。Android SDK 提供了一个强大的测试框架,用于定义和运行各种测试来验证 Android 应用的不同方面。Android 测试框架建立在流行的 JUnit 测试框架之上。Android 测试框架扩展了 JUnit,加入了 Android 特有的工具功能,允许测试控制 Android 应用周围的环境。这使得测试所有可能的用例变得容易。

JUnit 基础知识

JUnit 是 Java 编程语言的测试框架。JUnit 提供了一组类来定义、组织和运行测试用例。JUnit 提供的最重要的类是junit.framework.TestCase,它是所有测试用例的基类。Android 测试类也构建在这个类之上,它们遵循相同的代码结构和流程。在列出 A-1 的中显示了一个基本的测试用例类。

A-1 上市。 基本测试用例类

public class MyTest extends AndroidTestCase {     /**      * Sets up the text fixture before each test is executed.      */     protected void setUp() { `    }

    /**      * Test method.      */     protected void testSomething() {     }

    /**      * Tears down the text fixture after each test is executed.      */     protected void tearDown() {     } }`

以下是测试用例类的关键部分:

  • setUp:该方法在每次测试前设置测试夹具。开发人员应该重写该类,以正确初始化测试设备,从而确保新的测试运行与之前的测试运行相隔离。
  • tearDown:这个方法拆除文本 fixture 并释放所有为测试分配的资源。JUnit 测试框架在整个测试用例的执行过程中保留测试用例类。开发人员应该释放tearDown方法中的任何资源,以防止耗尽平台资源。
  • 测试用例类可以包含一个或多个测试。测试方法有前缀test。JUnit 框架在处理测试用例类时运行所有带有该前缀的方法。

断言

在计算机科学中,断言是一个谓词,用于表明开发者对给定阶段的应用状态的假设。断言用于测试应用的正确性。

JUnit 框架通过junit.framework.Assert类提供了一组常用的断言方法,用于测试用例。基类junit.framework.TestCase扩展了Assert类,并提供了对这些断言方法的直接访问。

JUnit 公开的断言方法是高度通用的。它们没有涵盖 Android 测试用例所涵盖的频繁断言操作。Android 测试框架在这里发挥了作用,它提供了额外的断言类,这些断言类具有专门为解决 Android 测试需求而设计的大量方法。尽管 JUnit 有Assert类,这些额外的类并不是测试用例的基类。开发人员需要将这些类导入到 Java 代码中,并使用它们的静态断言方法。

以下附加断言类是作为android.test Java 包的一部分提供的:

  • 这个类提供了一组通用的断言方法,JUnit 没有提供这些方法来测试 Java 类型、数组和值。
  • ViewAsserts:这个类为 Android 视图提供了一套断言方法。这些方法可用于断言用户界面(UI)组件的可见性,以及它们在显示器上的位置。

单元测试

单元测试允许开发者孤立地测试应用组件。Android 测试框架在android.test Java 包下提供了一组组件测试类,方便了特定于组件的测试需求——比如夹具设置、拆卸和生命周期控制。测试用例可以扩展这些类,并提供建立在所提供功能之上的实际测试方法。

下面是提供的一些测试框架类:

  • 这是一个通用的测试用例类,具有访问上下文和资源以及测试应用权限的方法。
  • ApplicationTestCase:这个类提供了一个在受控环境中测试Application类的环境。它允许测试代码控制应用的生命周期,以及注入依赖项,如独立的上下文。它延迟应用的启动,直到执行createApplication方法,以允许开发人员进行夹具设置。
  • ActivityUnitTestCase:这是对Activity类进行隔离测试的测试类。在测试中,一个活动在与 Android 平台最小连接的情况下启动。它允许在测试之前将模拟上下文和应用实例注入到活动中。为了提供真正的单元测试环境,它覆盖了一组 Android 方法,以防止该活动与其他活动和平台进行交互。
  • ServiceTestCase:这是一个测试类,用于在受控环境中测试Service类。它为服务生命周期管理提供了基本支持,也允许开发人员通过测试代码注入依赖项和控制环境。
  • ProviderTestCase2:这是一个单独的ContentProvider类的测试类,用于测试独立内容提供者的应用代码。它没有为提供者使用系统映射,而是维护其内部列表,并将这些内容提供者只暴露给测试用例。它反对使用ProviderTestCase类来打破对检测的依赖。
模拟物体

单元测试是一个具有已知输入和输出的可重复过程。组件的所有依赖都通过模拟对象来实现,以消除影响测试结果的外部依赖。

为了方便依赖注入,Android 框架在android.test.mock Java 包下为 Android 框架的核心部分提供了模拟对象。这些模拟类通过覆盖和存根它们的正常操作将测试与运行系统隔离开来。除了开发人员定义的部分之外,它们没有任何功能。所有没有被覆盖的方法抛出一个java.lang.UnsupportedOperationException来通知开发人员测试代码正在试图与环境通信。提供了以下模拟类:

  • MockApplication:这个类扩展了Application类,并保留了它的方法。开发人员可以扩展这个模拟类来实现依赖注入所必需的方法。所有其他方法都会提高UnsupportedOperationException
  • MockContext:这个类扩展了Context类,并保留了它的方法。开发人员可以使用模拟上下文将其他依赖项注入应用。
  • MockContentResolver:这个类扩展了ContentResolver类,覆盖了 Android 通过权限解析内容提供商的正常方式。模拟内容解析器保留了一个内部映射,而不是使用系统的内容提供者映射。开发人员应该在 fixture 设置期间将他们的模拟内容提供者注册到模拟内容解析器中。模拟内容解析器通过只解析直接注册的模拟内容提供者来隔离被测试的应用。
  • MockContentProvider:这个类扩展了ContentProvider类,并保留了它的方法。开发人员应该重写必要的内容提供者方法,以便向该内容提供者的使用者提供静态数据。稍后,通过模拟内容解析器,可以将模拟内容提供者注入到被测试的应用中。
  • MockCursor:这个类扩展了Cursor类,并保留了它的方法。它通常与模拟内容提供者一起使用,为被测试的应用提供静态数据。
  • MockDialogInterface:这个类用存根方法实现了DialogInterface。开发人员可以重写它的方法来验证对话框的 UI 输入。
  • MockPackageManager:这个类扩展了PackageManager类,并保留了它的方法。开发人员可以覆盖必要的方法来模拟正在测试的应用和 Android 系统之间的交互。
  • MockResources:这个类扩展了Resources类,并保留了它的方法。它使开发人员能够通过覆盖模拟方法在被测试的应用中进行资源注入。

功能测试

功能测试是一种黑盒测试。它根据软件组件的规格来测试它们。功能测试包括输入和检查输出;很少考虑内部程序结构。

Android 测试框架允许通过工具对 Android 应用进行功能测试。Android instrumentation 是一组控制方法和挂钩,用于将用户事件和请求注入到应用中,同时管理其生命周期。插装方法通过android.app.Instrumentation类提供。该类在任何应用代码运行之前被实例化。

与单元测试类不同,功能测试类使用实际的系统上下文加载应用,并使用应用的 UI 或向系统公开的 Android 服务将事件提供给应用。功能测试类扩展了InstrumentationTestCase类,并通过getInstrumentation方法提供对插装实例的访问。在android.test Java 包中提供了以下插装类:

  • 这个类提供了对单个活动进行功能测试的方法。被测试的活动是使用系统基础设施启动的,然后可以使用插装方法进行操作。它通过为被测试的活动提供更细粒度的配置选项而摒弃了ActivityInstrumentationTestCase类。
  • SingleLaunchActivityTestCase:这个类启动正在用其setUp方法测试的Activity类,并在其tearDown方法中终止它。与其他测试类不同,这个类在现有的活动实例上运行所有的测试方法,而不是为每个测试设置和拆除活动实例。
用户界面操作

Android 框架要求所有与 UI 组件的交互都发生在应用的主线程中,也称为 UI 线程InstrumentationTestCase类为在 UI 线程中运行测试代码提供了以下选项:

  • TouchUtils:该类提供了从仪器测试中生成触摸事件的方法,该测试被分类为模拟用户通过触摸屏与应用的交互。
  • UiThreadTest : Annotation 可以用来标记测试类中应该在应用的 UI 线程中执行的测试用例。在这种模式下,可能无法使用检测方法。
  • runTestOnUiThread:这个方法可以用来调度 UI 线程中的Runnable对象。这允许测试用例将测试的一部分注入到应用的 UI 线程中。

测试项目

测试项目与一般的 Android 项目没有什么不同。它们是作为独立于实际应用的项目生成的。尽管它们是一个独立的项目,但是最佳实践是将测试项目存储在主项目根目录下的tests目录中。

本书第五章中介绍的 Android 开发工具(ADT)为生成测试项目提供了两个选项。一种方法是在创建实际项目的同时创建测试项目。新的 Android 项目向导,在它的第三步,询问你是否也应该生成一个测试项目,如图图 A-1 所示。您可以先标记“创建测试项目”选项,然后配置测试项目。

Image

图 A-1。 用新的 Android 项目向导配置一个测试项目

在应用开发的最开始拥有一个测试项目是测试驱动编程的良好实践。然而,如果测试项目没有在开始时创建,那么创建一个也不迟。ADT 提供了一个新的项目向导,专门用于为工作区中现有的 Android 项目生成一个新的测试项目。

创建新的测试项目,从顶部菜单栏选择文件 Image 新建 Image 项目… ,展开 Android,选择 Android 测试项目,如图 A-2 所示。

Image

图一-2。 选择创建一个新的 Android 测试项目

ADT New Android Test Project 向导将首先询问您这个测试项目的名称。作为新项目的位置,建议使用项目内部的tests子目录进行测试。单击“下一步”按钮继续下一步。

每个测试项目都需要与现有的 Android 项目相关联。下一步,向导会要求您选择目标项目,如图图 A-3 所示。选择目标项目,然后单击“下一步”按钮继续。

Image

图 A-3。 为测试项目选择目标 Android 项目

作为最后一步,New Android Test Project 向导将询问新测试项目的目标 Android SDK 版本。选择适合目标 Android 项目的 SDK 目标,点击 Finish 按钮。ADT 将生成测试项目。

运行测试

要运行测试用例,请选择测试项目并单击 run 按钮。如图 A-4 所示,Eclipse 将询问项目应该如何执行。从列表中选择 Android JUnit Test,然后单击 OK 按钮继续。

Image

图 A-4。 测试项目第一次运行时的运行方式对话框

ADT 首先构建和部署实际的 Android 项目,然后对测试项目本身进行同样的操作。当测试在目标设备或模拟器上运行时,您可以使用 Eclipse 中的 JUnit 视图来监控它们,如图图 A-5 所示。

Image

图一-5。 JUnit 视图显示测试进度

JUnit 视图有两个窗格:

  • 顶部的窗格提供了正在执行的测试列表,以及关于通过和失败的测试用例数量的统计数据。
  • 如果一个测试用例失败,底部窗格提供显示错误位置的失败跟踪,如图图 A-6 所示。

Image

图 A-6。 显示测试失败的失败痕迹

在对大型项目中失败的测试用例进行故障排除时,最好只运行那个测试用例,而不是整个测试套件。若要仅运行一个测试,请使用包资源管理器选择测试用例类,然后单击“运行”按钮。Eclipse 将显示运行方式对话框,如图图 A-7 所示。选择 Android JUnit 测试,然后单击确定按钮继续。JUnit 将只运行所选测试用例类中的测试。

Image

图 A-7。 执行选中的测试用例类

测量测试代码覆盖率

多少测试就够了?这是测试中最常被问到的问题之一。测试用例的数量并不是测试覆盖率的良好衡量标准。Android SDK 附带了 EMMA,用于测量和报告测试用例的代码覆盖率。

**注意:**代码覆盖率目前仅在 Android 模拟器和根设备上受支持。

尽管 EMMA 是 Android 测试框架的一个重要组成部分,但它在 Android 应用中的使用并没有明确的文档记录。在这一节中,我们将经历从测试项目中生成代码覆盖报告的步骤。

设置艾玛权限

在撰写本文时,Eclipse 的 ADT 插件没有提供对 EMMA 的直接访问。EMMA 代码覆盖工具只能通过基于 Ant 的构建脚本来调用。创建 Android 项目时,Eclipse 不会生成 Ant 构建脚本。应该为应用项目和测试项目手动创建构建脚本。

要创建构建脚本,如果在 Windows 主机上运行,请打开命令提示符,如果使用基于 Mac OS X 或 Linux 的主机,请打开终端窗口,并调用以下命令:

`cd android update project --path .

cd android update test-project --main --path .`

这些命令将在应用和测试目录中生成 Ant 构建脚本、build.xml和其他必要的属性文件。

为测试运行启用 EMMA

要启用 EMMA,请使用包资源管理器,展开测试项目。如果build.xml文件不可见,按 F5 刷新项目目录。右键单击build.xml文件并从上下文菜单中选择运行为 Ant Build… 。将出现“编辑配置”对话框。切换到主选项卡,将参数设置为all clean emma debug install test,如图 A-8 所示。

Image

图一-8。 配置蚂蚁构建脚本

确保 Android 设备连接到主机,并单击 Run 按钮执行 Ant 构建脚本。构建脚本将应用和测试部署到目标设备或启用 EMMA 的仿真器,如图图 A-9 所示。

Image

图一-9。 控制台视图显示艾玛被启用

在测试用例完成后,脚本将提取 EMMA 结果文件,并在测试项目下的coverage目录中生成一个 HTML 格式的报告。使用包资源管理器选择测试项目后,按 F5 键刷新项目目录。展开coverage目录,打开coverage.html报表文件,如图 A-10 所示。

Image

图 A-10。 艾玛代码覆盖报告文件

EMMA 报告 HTML 文件提供了关于测试用例代码覆盖率的大量信息。通过点击包和类,您可以导航到源文件,并查看没有被任何测试用例执行的代码部分。使用这些信息,您可以扩展测试用例来覆盖更大部分的应用代码。

压力测试

压力测试是一种测试形式,通过引入超出应用运行能力的负载来确定应用的稳定性。Android SDK 提供了 Monkey 工具来向应用发送伪随机的击键、触摸和手势流。压力测试不是一个可重复的过程;然而,Monkey 工具允许重复事件流来重现错误情况。

要启动 Monkey 工具,首先将目标设备连接到主机或启动模拟器。如果您使用的是基于 Windows 的主机,请打开命令提示符,或者在基于 Mac OS X 和 Linux 的主机上打开终端窗口,并调用以下命令:

adb shell monkey -p <your application package> -v 500

这个命令启动 Monkey 工具,并用给定的包名向 Android 应用发送 500 个伪随机事件。关于 Monkey 工具命令行参数的更多信息,见[developer.android.com/guide/developing/tools/monkey.html](http://developer.android.com/guide/developing/tools/monkey.html)