Android开发权威指南(第二版)

978-7-115-32033-9
作者: 李宁
译者:
编辑: 张涛

图书目录:

详情

本书全面介绍了Android应用开发的各种技术,主要内容包括Android的四大应用程序组件、布局、菜单、控件、资源和国际化、可视化UI设计、对话框、Toast和通知栏、拖放技术、数据库技术、Android支持的各种组件技术、通讯技术(蓝牙、NFC、网络等)、OpenGL ES、桌面小部件、动态壁纸、传感器、GPS、谷歌地图、人机交互、输入法开发、Android NDK开发、测试驱动、性能优化等。

图书摘要

Android开发权威指南(第二版)

李宁 编著

人民邮电出版社

北京

随着Android的超速发展,为了使Android更加稳定,以及更好地处理版本碎片化,Google公司承诺每年只发布一次Android的新版本,不过从最近几年看,Google公司完全没有遵守这个承诺,Android的版本发布越来越快,越来越频繁。

由于Android版本更新太块,而且大的升级有可能出现版本之间的不兼容,或增加了新的技术,所以就造成了目前市面上的Android书籍的内容严重滞后,这就造成了书中的很多例子在新版本的Android系统中根本无法成功运行。为了使读者尽可能跟上Android升级的脚步,笔者经过近一年的写作,将《Android开发权威指南》第1版进行了全面改版,增加了很多只有Android 4.x才有的新功能。所以,读者可以通过本书的学习全面掌握Android的最新技术。掌握这些技术除了可以显著提高自己的技术水平外,还可以让自己的职业生涯达到一个新的高度。而且,Android程序员目前的市场需求越来越大,这必然会为我们带来更多的就业和创业机会,这些机会都值得广大程序员好好把握!

本书的特点

本书是一本全面介绍Android应用开发的专著,全面介绍了在Android 4.2.2平台上应用开发的各种知识。与市面上的同类Android编程的图书不同,本书从Android的各个版本新增功能入手,介绍了Android各个主要版本之间的差异,这对于读者为不同Android版本开发应用至关重要。

本书具有的特点介绍如下。

1.知识点全面,覆盖面广

本书全面介绍了Android的各种应用开发知识,主要包括四大应用程序组件、布局、菜单、控件、资源和本地化、可视化UI设计、对话框、Toast和通知栏(Notification)、拖放技术、数据库技术、Android支持的各种组件技术、通信技术(蓝牙、NFC、网络等)、异步装载、推送技术、多媒体、2D绘图技术、OpenGL ES、动态壁纸、传感器、GPS、谷歌地图、人机交互、Android NDK开发、测试驱动开发、性能优化等。这些技术几乎覆盖了Android应用开发的每一个方面,其中有很多技术讲解得非常深入,例如,Activity从创建到销毁的过程中到底发生了什么;Intent、Intent Filter与Activity到底是什么关系;NFC技术在不同场合、不同领域的应用。其中有很多技术在其他同类的图书中是绝无仅有的。

2.最新版本,实战性强

本书采用了最新的Android版本(4.2.2),所以,书中示例使用的都是最新的技术。而且每一个知识点都采用了“项目驱动”的方式来讲解,这样在理解知识点所涉及的理论后,就可以很容易地进行实战演练。随书光盘中提供的数万行源代码可以供读者随时参考。

3.讲解细腻,授人以渔

现在市面上很多同类书尽管提供了大量的示例,但只是机械地Step by Step,并没有介绍其中的实现原理,而本书的很多示例将会进入到更深层来挖掘其中的奥秘。例如,对于动画渲染器来说,自定义的渲染器不能在XML文件中定义,那是为什么呢?本书会详细分析相关的Android SDK源代码来了解其中的奥秘。读者将从这些示例中了解到从官方文档和其他图书中无法了解到的信息,本书不仅授人以鱼,而且还授人以渔。

光盘内容

1.光盘说明

src:所有的源代码都在src目录中,每一章的代码都放在chxx目录中,例如,第5章的源代码目录是src/ch05。src.7z:为了方便读者将源代码复制到个人计算机上,还提供了一个压缩版本的源代码文件。specification:该目录中是NFC规范。tools:该目录中包含了一些反编译工具。software:该目录中包含了用于读写NFC标签的工具。tomcat:该目录中包含了Tomcat 7,并在其中内置了第27章使用的Servlet,可直接在Windows、Mac OS或Linux上启动Tomcat,但要设置JAVA_HOME环境变量,并且将<JDK安装目录>/bin路径设置到PATH环境变量中。

2.运行环境

本书的示例运行在JDK 6下,需要在Eclipse 3.4及以上版本中导入并运行本例。建议读者除了使用JDK 6外(如果编译Android源代码,必须使用JDK 6,如果只想运行随书光盘中的示例,也可以使用JDK 7),其他的工具尽量使用最新的版本。

随书光盘中的示例在Android 4.2.2上测试通过,如果读者使用的Android版本与本书示例使用的Android版本不同,请在Eclipse中修改当前工程的Android版本。修改的方法是打开工程的属性对话框,单击左侧的“Android”项,然后在右侧的列表中选择相应的Android版本即可。

另外,有一些示例并不能在Android模拟器上测试,例如,蓝牙、NFC等。运行这些示例需要读者自己准备带有相应硬件模块的Android设备。对于NFC应用,读者可以使用第45章介绍的NFC侠开发和测试NFC应用,但第26章的示例仍然使用传统的方式编写,所以,仍然需要在支持NFC的Android设备上运行。

3.注意事项

(1)本书所有的Android项目都是Eclipse工程,建议读者将源代码从光盘复制到个人计算机上,然后使用Eclipse直接导入即可。

(2)本书的第27章涉及一些Web的技术,其中使用到了Tomcat 7和Servlet,读者只要将光盘中的tomcat目录直复制到个人计算机上,并设置好JAVA_HOME环境变量,然后直接运行<Tomcat安装目录>/bin目录中的startup.sh或startup.bat脚本文件就可以启动Tomcat了。第27章使用的Servlet已经集成到Tomcat中,只要修改一下示例中的IP即可访问Servlet。

(3)本书还有一些示例使用了笔者机器的IP地址,读者在运行这些示例时要将这些IP地址改成自己机器的IP地址。这些内容已经在相关的章节予以说明了,读者在运行这些示例时要仔细阅读相关的代码描述。

4.技术支持

如果您对光盘的内容有什么不清楚的,或任何与本书有关的问题都可以到http://blog.csdn.net/ nokiaguy或新浪微博http://weibo.com/androidguy与笔者联系。编辑联系邮箱为zhangtao@ptpress.com.cn。

尽管到现在为止没有专门讨论Intent,但在前面的章节已经多次使用了Intent对象。这些Intent对象都在startActivity或startActivityForResult方法之前创建,这两个方法会根据Intent对象封装的信息决定如何来操作窗口。其实Intent对象的功能还不止这些,而且Intent对象也不仅仅只用于窗口。那么Intent对象到底有什么其他的功能呢?Intent Filter又在使用Intent的过程中扮演什么角色呢?为了给出这些问题的答案,本章将结合大量的案例讨论Intent和Intent Filter的使用方法和应用场合,并且还会结合Android源代码和反编译技术进一步分析Intent Filter的实现原理。

Android系统从诞生之初就不允许直接访问4大应用程序组件(Activity、Service、Broadcast Receiver和Content Provider),这就需要提供一种机制可以访问这些组件,这也是Intent诞生的根本原因。

【注】①如果读者继续阅读本书后面的章节,会逐渐体会到为什么安迪-鲁宾(Andy Rubin,Android之父)要这么设计。

Intent对象被应用于4大应用程序组件中的3个(Activity、Service和Broadcast Receiver),而Content Provider会采用其他方式访问(在后面的章节会详细介绍)。例如,对于窗口(Activity)来说,尽管无法直接访问窗口实例,但可以调用startActivity方法显示窗口。为了可以更灵活地控制窗口,以及向窗口传递数据,就需要一种技术来完成这些工作,这就是Intent对象的核心功能,所以说Intent对象从本质上来说就相当于一个信使,用于保存和传输各种数据。

服务(Service)和广播(Broadcast)的使用方法和窗口(Activity)类似,Intent对象也用来完成类似的工作。

下面的代码用于开始服务和发送广播,其中都使用到了Intent对象。关于服务和广播的细节会在后面的章节讨论。

// MyService是服务类

Intent serviceIntent = new Intent(this, MyService.class);

// 开始服务

startService(serviceIntent);

 

// mybroadcast是广播字符串

Intent broadcastIntent = new Intent("mybroadcast");

// 向系统发送一条广播

sendBroadcast(broadcastIntent);

从上面的代码可以看出,开始服务、发送广播和显示窗口的代码风格是相同的,都需要创建一个Intent对象,并指定相关的信息(服务指定具体的服务类;广播指定一个字符串,被称为Broadcast Action)。

上一节介绍了Intent的主要作用就是封装和传递各种信息。那么Intent对象到底可以封装和传递哪些信息呢?本节将会揭开这些秘密。

Intent对象可以封装和传递如下6种信息。

组件名(Component name)。

动作(Action)。

种类(Category)。

数据(Data)。

附加信息(Extra)。

标志(Flags)。

在6.2.1至6.2.7小节会分别介绍这6种信息,并从6.4节开始提供大量的案例演示在Android应用中如何使用这些信息。

Intent对象可以直接指定要访问的组件,例如,有一个名为MyActivity的窗口类,可以使用下面的代码指定该窗口类。

// 下面的代码在窗口类中调用,this就是Context对象

Intent intent = new Intent(this, MyActivity.class);

直接指定窗口类的class只是其中最常用的引用方式,除此之外,还有如下3个方法可以指定具体的窗口。

setClass:指定Context对象和窗口类的class。与通过Intent类的构造方法传递信息的作用是一样的。

setClassName:指定Context对象、字符串形式的包名和窗口类名。

setComponent:与setClassName方法类似,只是要传递的数据都封装在了ComponentName对象中。

1.setClass方法

setClass方法的原型如下:

public Intent setClass(Context packageContext, Class<?> cls)

该原型与下面的Intent类构造方法的一个重载形式是完全一样的。

public Intent(Context packageContext, Class<?> cls)

从setClass方法及Intent类的构造方法原型可以看出,这两个方法是完全等效的,也就是不想在Intent类的构造方法传递信息,也可以使用Intent.setClass方法完成同样的工作。

Intent intent1 = new Intent(this, MyActivity.class);

//下面两行代码等价与上面的一行代码(intent1和intent2封装的信息是相同的)

Intent intent2 = new Intent();

intent2.setClass(this, MyActivity.class);

2.setClassName方法

setClassName方法允许直接指定具体的应用程序和组件类(窗口类、服务类等)的全名。该方法有如下两个重载形式。

public Intent setClassName(Context packageContext, String className)

public Intent setClassName(String packageName, String className)

由于Android应用不能像Windows程序一样直接执行可执行文件,所以需要使用Android应用的唯一索引PackageName定位某个具体的程序,这也是packageName参数的值。而packageContext允许以另外一种形式定位应用程序,这就是应用程序的Context对象(上下文对象),在6.4节中会学习如何来获得当前和其他应用程序的Context对象。className参数表示类的全名(记住,是PackageName+ClassName,不能只指定类名)。

【注】①就是AndroidManifest.xml文件中<manifest>标签的package属性值。

3.setComponent方法

setComponent方法通过ComponentName对象指定PackageName、ClassName等信息,该方法的原型如下:

public Intent setComponent(ComponentName component)

使用setComponent方法之前先要创建ComponentName对象,在6.4节中会详细介绍setComponent方法及ComponentName对象的使用方法。

一般会通过窗口类的class或窗口类的全名确定某个具体的窗口,不过这些信息都是窗口的固有属性。然而还有第3种确定窗口的方法,这就是窗口动作(Activity Action)。如果用一句话解释什么是Activity Action,那就是“与窗口绑定的一个或多个字符串”,也就是说Activity Action是一个字符串,而且一个窗口可以拥有多个这样的字符串。其实Activity Action在前面的章节虽然未专门讲解,但已经多次涉及了。每次建立Android工程时都会自动创建一个主窗口,该窗口通常会在AndroidManifest.xml文件中有如下的声明代码。其中<action>标签的android:name属性值android.intent.action.MAIN就是一个系统提供的Activity Action。一旦应用程序启动,Android系统就会寻找要运行的Android应用中定义了该Activity Action的窗口,一旦找到,就会显示该窗口。

【注】②不仅窗口、服务和广播接收器也可以与多个字符串进行绑定,由于本章主要介绍窗口的基础知识,所以本章的大多数地方只提到了窗口,但读者要清楚一点,除非特别提到,否则后面章节介绍的服务和广播接收器也拥有同样的特性。关于如何将字符串与窗口绑定,会在本章后面的部分详细介绍。

【注】③当然,除了要有android.intent.action.MAIN外,还需要定义一个叫android.intent.category.LAUNCHER的category。这部分内容会在本章后面的部分介绍。

<activityandroid:name=".MyActivity">

  <intent-filter>

     <action android:name="android.intent.action.MAIN" />

     <category android:name="android.intent.category.LAUNCHER" />

  </intent-filter>

</activity>

尽管一个窗口可以有多个Activity Action,但只能通过Intent.setAction方法设置一个Activity Action。如果指定的Activity Action并不存在,当显示窗口时(调用startActivity方法)就会抛出异常。例如,我们可以通过如下的代码指定一个Activity Action,并显示定义该Activity Action的窗口,如果系统中有多个窗口定义了该Activity Action,会显示一个选择列表,列表中显示了所有定义该Activity Action窗口,用户可以选择显示哪个窗口。

// 通过Intent对象指定了一个Activity Action

Intent intent = new Intent(“mobile.android.android.MYACTION”);

// 如果系统中没有定义mobile.android.android.MYACTION的窗口,执行下面的代码会抛出异常

startActivity(intent);

如果用户不想通过Intent类的构造方法指定Activity Action,也可以通过Intent.setAction方法指定Action,代码如下:

Intent intent = new Intent();

intent.setAction(“mobile.android.android.MYACTION”);

startActivity(intent);

系统应用也有很多窗口定义了Activity Action,这些Activity Action可以调用Android系统内置应用中的窗口。例如,照相机拍照界面、拨号界面等。虽然可以直接用字符串指定这些系统的Activity Action,但为了兼容(Android的后续版本又可以修改系统窗口的Activity Action),应尽量使用在Intent类中定义的Action常量(以ACTION_开头的都是Action常量)。这些常量包括Activity Action和Broadcast Action,系统未定义Service Action。直接在代码编辑器中输入“Intent.”就会看到所有的Action常量,如图6-1所示。

 

▲图6-1 Action常量

在这里先给大家提个问题,如果系统中有多个窗口(不同应用程序中的窗口)定义了同一个Action,而且自己编写的程序中也定义了这个Activity Action。这样如果通过Intent.setAction方法设置这个Action后显示窗口,就会首先显示一个选择列表,列出了所有定义该Activity Action的窗口。那么在调用自己编写的程序中定义了该Activity Action的窗口时不想显示这个选择列表,而只显示自己应用中的窗口又该如何做呢?

其实解决上述问题也很简单,就像文件和文件夹的关系一样,如果想在磁盘上存储多个同名的文件,又不发生冲突的方法就是将这些文件放到不同的文件夹下。那么Action也是一样。如果单凭Action无法定位唯一的窗口,那就要再加一个可以定位的标志,这就是种类(Category)。

Category实际上与Action一样,也是与窗口绑定的字符串。在声明主窗口时<category>标签的android:name属性值指定的就是Category。如果声明窗口时同时指定了Action和Category,那么在显示窗口之前不仅要使用Intent.setAction方法设置Action,还要使用Intent.addCategory方法添加Category。对于Intent对象来说,Action与Category的不同之处在于只能为Intent对象指定一个Action,而Category可以指定多个(从方法名setAction和addCategory就可以很容易猜到)。

Category也可以和Action一样任意指定字符串。在Intent类中同样定义了一些系统的Category常量,如图6-2所示。

同时设置Action和Category的Java代码如下:

Intent intent = new Intent();

// 设置Action

intent.setAction("mobile.android.action.MYACTION");

// 设置Category

intent.addCategory(“mobile.android.category.MYCATEGORY);

 

▲图6-2 Category 常量

通过指定系统的Action和Category可以实现很多有趣的东西,例如,在上一章介绍的调用系统窗口就是其中之一。读者可以在学习完这一章的内容后再回过头来复习上一章的内容,会有更深的体会。

public Intent setTypeAndNormalize(String type)

Intent intent = new Intent();

Data与Action、Category并称为Android组件的3大过滤机制(将在6.6节详细介绍)。但Data与后两者不同的是并不是简单地通过字符串对比的方式进行匹配,而是按着一定规则进行匹配。实际上,Data本身又分为两种方式进行匹配:MIME Type和URI。

MIME Type就是指要访问的组件处理的数据类型,例如video/mpeg4、video/mp4、video/avi等。MIME Type也可以用通配符(*)匹配某一类型的数据,例如“audio/*”表示所有的音频数据格式。

URI有些类似我们经常使用的Web地址,但要比Web地址范围更广,例如,下面的3行字符串都属于URI。

http://www.google.com

content://mobile.android.data/cities

ftp://192.168.17.168

设置MIME Type和URI可以由下面几个方法完成,通过这些方法可以单独设置这两个值,也可以同时设置这两个值。

1.setType方法

该方法的原型如下:

public Intent setType(String type)

setType方法用于设置MIME Type,并且在设置后会将Uri清除,这一点从setType方法的源代码很容易看出。

public Intent setType(String type)

{

  // mData字段用于保存Uri,直接将该字段设为null了,也就是说前面设置的Uri将被清空

  mData = null;

  mType = type;

  return this;

}

下面的代码使用setType方法设置了MIME Type。

Intent intent = new Intent();

intent.setType("audio/*");

2.setTypeAndNormalize(String type)方法

该方法的原型如下:

setTypeAndNormalize与setType的功能类似,也用于设置MIME Type,并且将Uri清空。但setTypeAndNormalize方法还可以规范MIME Type。例如,如果MIMI Type设为“text/plain; charset=utf-8”,就会将后面的非MIMI Type部分去掉,变成“text/plain”。该方法还可以使MIMI Type的写法看起来更标志,例如会将“text/x-vCard”变成“text/x-vcard”。

其实setTypeAndNormalize只是调用了Intent.normalizeMimeType方法对MIMI Type进行规范处理。setTypeAndNormalize方法的代码如下:

public Intent setTypeAndNormalize(String type)

{

  // normalizeMimeType方法用于规范Mime Type

  return setType(normalizeMimeType(type));

}

3.setData方法

该方法的原型如下:

public Intent setData(Uri data)

setData方法用于设置Uri,并且会清空MIMI Type。setData方法的代码如下:

public Intent setData(Uri data)

{

  mData = data;

  // mType保存MIME Type

  mType = null;

  return this;

}

下面的代码设置了一个Uri。

Intent intent = new Intent();

intent.setData(Uri.parse("http://192.168.17.168/process"));

4.setDataAndNormalize方法

该方法的原型如下:

public Intent setDataAndNormalize(Uri data)

setDataAndNormalize与setData的功能类似,只是会将scheme变成小写形式。scheme就是指Uri开始的部分(冒号以前的部分),例如,http://www.google.com中的http就是scheme。假设有一个Uri:HTTP://www.google.com,setDataAndNormalize方法会将该Uri变成http://www.google.com。

5.setDataAndType方法

该方法用于同时设置Uri和MIME Type,原型如下:

public Intent setDataAndType(Uri data, String type)

下面的代码同时设置了Uri和MIME Type。

intent.setDataAndType(Uri.parse("http://192.168.17.168/process"),"audio/*");

6.setDataAndTypeAndNormalize方法

该方法用于同时设置Uri和MIME Type,并且将Uri和MIME Type标准化,该方法的原型如下:

public Intent setDataAndTypeAndNormalize(Uri data, String type)

Extra就是一些Key-Value数据对。这些数据通过Intent类提供的具有多个重载形式的putExtra方法设置。例如,下面是putExtra方法的一些常用的重载形式。

// 设置字符串

public Intent putExtra(String name, String value)

// 设置整数

public Intent putExtra(String name, int value)

// 设置浮点数

public Intent putExtra(String name, float value)

// 设置实现Parcelable接口的对象

public Intent putExtra(String name, Parcelable value)

尽管通过Data可以向要访问的组件传递一些数据,但对于数据量较大或需要将数据进行划分时应使用Extra。有很多系统的Action需要提供附加信息,例如ACTION_TIMEZONE_CHANGED就需要一个名为time-zone的附加信息,所以在使用该ACTION时需要使用下面的代码设置附加信息。

intent.putExtra("time-zone",java.util.TimeZone.getDefault());

如果想知道哪一个系统Action需要传递附加信息,可以通过Content Assist(内容助手)来查询。例如,要查询ACTION_TIMEZONE_CHANGED需要哪些附加信息,首先要找到该Action,然后选中该Action,就会在下方显示该Action的详细信息,如图6-3所示的黑框中就是该Action需要的附加信息。

 

▲图6-3 显示Action 需要哪些附加信息

Flags和前几种Intent对象封装的信息都不同。Flags既不会定位窗口,也不会向窗口传递数据,Flags只用来控制窗口显示行为。这里涉及任务(Task)和回退栈(Back Stack)的概念。这一部分内容会在7.2节详细讨论。在本章读者只要知道通过Intent对象可以完成如下3项工作即可。

定位窗口:通过Component name、Action、Category和Data可以定位一个或多个窗口。

传递数据:通过Data和Extra。

控制访问组件的行为(窗口、服务和广播):通过Flags。

到现在为止读者已经见识过大量的Android应用程序,这些应用大多都有两个或两个以上的窗口,这就要求在某一个窗口中显示另外一个窗口。想必读者已经观察到不同显示窗口的方式。我们最开始接触到直接指定窗口类的方式称为显式访问窗口,而后来的一些程序,尤其是上一章调用系统窗口的例子又向我们展示了另外一种访问窗口的方式:隐式访问窗口。

显式访问窗口不会产生歧义,系统可以毫不犹豫地显示指定的窗口,下面是一段典型的显式访问窗口的代码。

Intent intent = new Intent(this, MyActivity.class);

startActivity(intent);

隐式访问窗口与显式访问窗口不同,前者并不需要指定窗口类的class,而只需要指定与窗口绑定的字符串(Action、Category和Data),至于如何设置Intent对象,要看访问的窗口在AndroidManifest.xml文件中是如何声明的,也就是说<intent-filter>标签里面有什么东西。下面给出一个最见到的隐式访问窗口的方式。在6.5节会详细讨论更复杂的隐式访问窗口的方式。

窗口类的声明代码。

<activity android:name=".MyActivity" >

  <intent-filter>

     <action android:name="android.intent.action.MYACTION" />

     <category android:name="android.intent.category.DEFAULT" />

  </intent-filter>

</activity>

隐式调用MyActivity窗口的代码。

Intent intent = new Intent("android.intent.action.MYACTION");

startActivity(intent);

源代码目录:src/ch06/WebBrowser、src/ch06/InvokeOtherActivity

在前面曾介绍过可以通过显式和隐式的方式访问窗口,显式方式只能访问应用程序内部的窗口,而隐式方式无论当前应用程序,还是其他应用程序中的窗口都可以访问。不过阅读了本节的内容后读者就会改变这种观念。因为Android足够强大,已经完全打通不同应用程序之间的界限了,也就是说不同应用程序之间的交互几乎和应用程序内部的交互完全一样。因此在一个应用程序中同样可以通过直接指定另外一个应用程序中窗口类的方式访问该窗口,也就是跨应用程序显式调用。所以从现在开始不要认为只有隐式方式才可以调用其他应用程序的窗口,显式方式一样可以。

要了解显式调用窗口精髓需要知道如下几点。

每一个Android应用的唯一标识就Package Name(包名),也就是AndroidManifest.xml文件中<manifest>标签的package属性值。只要Package Name相同,就可以认为是同一个Android应用。

如果用Intent对象指定一个窗口类,除了需要指定窗口类的class外,还需要指定窗口类所在的应用程序的Context对象(或应用程序的Package Name),所以通过Package Name和Activity Class Name(窗口类名)可以定位当前Android系统中的任意窗口。

了解了这两点后,就可以很容易想到如何通过直接指定窗口类的方式调用另一个应用程序的窗口。在6.2.2小节介绍了通过Intent.setClass、Intent.setClassName和Intent.setComponent方法可以指定Package Name和Activity Class Name。只要Package Name(或与其对应的Context对象)指向了另一个应用程序,那么系统就会认为窗口类是这个应用程序的(当然窗口类必须在该应用程序中存在,否则访问该窗口会抛出异常)。

定位某个应用程序可以有如下3种方式。

直接指定Package Name。

另一个Android应用的Context对象。

ComponentName对象。

直接指定Package Name比较简单,Package Name就是一个字符串,只要知道目标应用程序的Package Name,直接指定即可。而Context对象就需要使用Context.createPackageContext方法创建了,代码如下:

【注】①Package Name在AndroidManifest.xml文件中定义,如果没有AndroidManifest.xml文件源代码,可以将相应的APK文件解压,并反编译AndroidManifest.xml文件即可,或安装APK,查看/data/data目录中系统为该应用创建的目录名(就是Package Name)即可。

Context context = createPackageContext(  

           "mobile.android.web.browser", Context.CONTEXT_INCLUDE_CODE

                 |Context.CONTEXT_IGNORE_SECURITY);

创建Context对象时也需要指定Package Name,但最好还指定Context.CONTEXT_INCLUDE _CODE和Context.CONTEXT_IGNORE_SECURITY,其中Context.CONTEXT_INCLUDE_CODE允许代码被装载,即使这些代码可能不安全,而Context.CONTEXT_IGNORE_SECURITY则忽略任何安全限制。

至于ComponentName对象,实际上就是封装了Package Name(或Context对象)和Class Name(或窗口类的class)的对象,只要得到了Context和窗口类的class,就可以直接创建ComponentName对象,并通过ComponentName类的构造方法传入相应的数据即可。

本例演示了跨应用程序访问窗口的各种方法,程序的主界面如图6-4所示。读者可以通过不同的按钮测试相应的功能。

 

▲图6-4 跨应用程序访问窗口

为了测试本节的例子,需要编写一个辅助程序WebBrowser,该程序的功能是接收一个Uri,并在WebView控件中显示Uri指向的页面。

【注】①WebView就是Android SDK中的浏览器控件,用于显示Web页面。在后面的控件详解部分会详细介绍该控件。

WebBrowser的窗口类WebBrowserActivity(该程序中只有一个窗口类)的实现代码很简单,只是在onCreate中通过getData方法获取传入的Uri对象,然后用WebView控件显示Uri指向的页面,WebBrowserActivity类的实现代码如下:

源代码文件:src/ch06/WebBrowser/src/mobile/android/web/browser/WebBrowserActivity.java

public class WebBrowserActivity extends Activity

{  

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_web_browser);

    WebView webview = (WebView) findViewById(R.id.webview);

    // 获取Uri对象

    Uri uri = getIntent().getData();

    if (uri != null)

    {

      // 在WebView控件中显示Uri指向的页面

      webview.loadUrl(uri.toString());

      // 在窗口标题栏中显示Uri

      setTitle(uri.toString());

    }

  }

}

在声明WebBrowserActivity类时就有一定的说道了。既然是跨应用程序调用,通常认为指定一个Action即可。但为了说明显式和隐式跨应用程序调用的差别,WebBrowserActivity类需要指定与系统自带的浏览器相同的Action。这样在使用该Action显示窗口时就会显示一个如图6-5所示的选择列表,该列表中至少会显示两个候选的程序。其中右侧的WebBrowser就是WebBrowserActivity窗口,左侧的Browser是系统自带的浏览器应用中的窗口。

【注】②如果系统中没安装其他浏览器程序的话,就只有系统自带的浏览器和WebBrowser有相同的Action,并且符合浏览网页的过滤条件。

 

▲图6-5 选择一个可以显示网页的窗口

要想让某个窗口可以接收Uri,除了指定相应的Action外,还需要指定Data(用<data>标签设置)。对于浏览Web页面,Data只需要设置scheme即可,也就是Web协议,如http、https等。

WebBrowserActivity类的声明代码如下:

源代码文件:src/ch06/WebBrowser/AndroidManifest.xml

<activity

  android:name="mobile.android.web.browser.WebBrowserActivity"

  android:label="@string/app_name" >

  <intent-filter>

     <action android:name="android.intent.action.MAIN" />

     <category android:name="android.intent.category.LAUNCHER" />

  </intent-filter>

  <intent-filter>

  <!-- android.intent.action.VIEW是Web浏览器的标准Action -->

  <action android:name="android.intent.action.VIEW" />

  <category android:name="android.intent.category.DEFAULT" />

  <!-- 指定该窗口可以打开以“http://”开头的Uri -->

  <data android:scheme="http" />

  <!-- 指定该窗口可以打开以“http://”开头的Uri -->

  <data android:scheme="https" />

 </intent-filter>

</activity>

从6.2节的内容可知,窗口可以通过Action、Category和Data三种机制的组合来定位,而WebBrowserActivity在声明时这3种过滤机制都使用了,所以在通过Action显示WebBrowserActivity窗口时需要同时指定Action、Category和Data。不过由于Category是android.intent.category.DEFAULT,所以并不需要指定Category。

下面看一下本节的核心程序InvokeOtherActivity(该程序的窗口类也是InvokeOtherActivity)是如何使用各种方式调用WebBrowserActivity的。前面如图6-4所示的界面就是InvokeOtherActivity程序的主界面。除了显示WebBrowserActivity窗口外,最后一个按钮还可以使用显式方式调用系统的计算器程序。

InvokeOtherActivity窗口类的代码如下:

源代码文件:src/ch06/InvokeOtherActivity/src/mobile/android/invoke/other/activity/InvokeOtherActivity.java

public class InvokeOtherActivity extends Activity

{  

  private Context mContext;

  private Class mClass;

  // 由于有多处需要Context和Class对象,所以在onCreate方法中提前创建了这两个对象

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_invoke_other);

    try

    {

      // 创建指向WebBrowser程序的Context对象,其中mobile.android.web.browser就是

      // WebBrowser的Package Name

      mContext = createPackageContext(

          "mobile.android.web.browser", Context.CONTEXT_INCLUDE_CODE

             |Context.CONTEXT_IGNORE_SECURITY);      

      // 动态装载WebBrowserActivity类

      mClass = mContext.getClassLoader().loadClass(

          "mobile.android.web.browser.WebBrowserActivity");

    }

    catch(Exception e)

    {

      e.printStackTrace();

    }

  }

  // ①“浏览网页:Action”按钮的单击事件方法,使用Action方式浏览网页

  public void onClick_Action(View view)

  {

    // 指定了Action和要浏览的页面的Uri

    // Intent.ACTION_VIEW的值是android.intent.action.VIEW

    // Uri确定了Data(符合以http://开头的Uri的要求)

    Intent webIntent = new Intent(Intent.ACTION_VIEW,

        Uri.parse("http://blog.csdn.net/nokiaguy"));

    startActivity(webIntent);

    }

  // ②“浏览网页:setClassName”按钮单击事件方法,直接指定Package Name和Class Name

  public void onClick_SetClassName(View view)

  {

    Intent webIntent = new Intent();

    // 直接指定了WebBrowser应用的Package Name和WebBrowserActivity类的全名

    webIntent.setClassName("mobile.android.web.browser",

        "mobile.android.web.browser.WebBrowserActivity");

    // 指定Uri

    webIntent.setData(Uri.parse("http://nokiaguy.blogjava.net"));

    startActivity(webIntent);

  }

  // ③“浏览网页:setClassName_Context”按钮单击事件方法,指定Context对象和Class Name

  public void onClick_SetClassName_Context(View view)

  {

    if(mContext == null mClass == null) return;

    Intent webIntent = new Intent();

    // 指定了Context对象和WebBrowserActivity类的全名

    webIntent.setClassName(mContext,

        "mobile.android.web.browser.WebBrowserActivity");

    // 指定Uri

    webIntent.setData(Uri.parse("http://nokiaguy.cnblogs.com"));

    startActivity(webIntent);

  }

  // ④“浏览网页:setClass“按钮单击事件方法,指定Context和Class对象

  public void onClick_SetClass(View view)

  {

    if(mContext == null mClass == null) return;

    Intent webIntent = new Intent();

    // 指定Context和Class对象

    webIntent.setClass(mContext, mClass);

    // 指定Uri

    webIntent.setData(Uri.parse("http://nokiaguy.cnblogs.com"));

    startActivity(webIntent);    

  }

  // ⑤“浏览网页:setComponentName”按钮单击事件,指定了ComponentName对象

  public void onClick_SetComponentName(View view)

  {

    if(mContext == null mClass == null) return;

    // 通过ComponentName对象指定了Context和Class对象

    ComponentName cn = new ComponentName(mContext, mClass);

    Intent webIntent = new Intent();

    // 指定ComponentName对象

    webIntent.setComponent(cn);

    // 指定Uri

    webIntent.setData(Uri.parse("http://nokiaguy.cnblogs.com"));

    startActivity(webIntent);

  }

  // ⑥“显示计算器”按钮单击事件方法,通过指定Package Name和Class Name的方式显示系统计算器

  public void onClick_ShowCalculator(View view)

  {

    Intent intent = new Intent();

    // 指定计算器的Package Name和主窗口类的全名

    intent.setClassName("com.android.calculator2",

        "com.android.calculator2.Calculator");

    startActivity(intent);

  }

}

在①号方法中使用了Action调用浏览器显示网页,如果事先安装了WebBrowser,就会显示如图6-5所示的选择列表。而②至⑤号方法都直接指定了WebBrowser的Package Name和WebBrowserActivity的全名,只是指定的方式不同。这些方式中分别使用了setClassName、setClass和setComponentName方法。尽管在当前的Android系统中至少存在两个程序可以浏览网页(系统内置的浏览器和WebBrowser),但②至⑤号方法只会调用WebBrowser中的WebBrowserActivity(不会出现图6-5所示的选择列表),因为在这些方法中直接指定了WebBrowserActivity类(显式方式调用)。在⑥号方法中仍然通过显式的方式调用系统内置的计算器程序。

通过本例我们可以知道,如果想调用其他应用程序中的窗口,但又不想出现图6-5所示的选择列表(调用之前谁也不知道当前系统中有多少个窗口满足过滤条件),就可以使用显式调用窗口的方式。

答疑解惑:为什么显式调用其他应用程序的窗口时会失败

在显式调用其他应用程序窗口时会发现并不是都好使,有一些情况无法成功调用这些窗口。这其中原因很多,比较常见的原因就是Package Name和Activity Class Name指定错误。不过这也不是故意的,而是不知情所致。例如,如果要调用系统内置的浏览器程序。通过查询Android源代码的Browser程序可知。Package Name是com.android.browser,Activity Class Name(主窗口类名)是com.android.browser.BrowserActivity。这对于Android模拟器是没错的,不过对于真机上的Android系统,可能就会有变化(有的ROM内置的Browser仍然和Android模拟器中的Browser是一样的),至少在Google官方原生的Android系统已经将Package Name变成了com.google.android. browser,而Activity Class Name并未变化。

【注】①ROM就是Android系统的安装包,可以将特定版本的Android系统安装在Android硬件设备上,这一安装过程通常称为刷机。

既然系统自带程序的Package Name和Activity Class Name有可能发生变化,那么直接访问这些程序的包名和类名就会带来一定的风险。所以在调用系统程序的窗口时应尽量使用Action,而不要直接指定包名和类名。这样即使包名和类名变化了,只要Action不变,仍然可以成功调用窗口。当然,如果调用者和被调用者都是自己编写的程序(有源代码),显式和隐式调用窗口都没什么问题,因为包名和类名的变化在于自己,这一点很容易控制。尤其是在制作可扩展的系统时,往往不希望调用自己的扩展窗口(另一个自己编写的程序中的窗口)时显示一个选择列表(如果使用Action,有可能系统的其他程序会与当前程序的Action、Category和Data相同),而是想直接就显示指定的窗口,这种情况下使用显式调用的方式就比较好。在本例②至⑤号方法中尽管系统中有多个窗口可以打开网页,但由于使用了显式调用,所以就直接显示了WebBrowserActivity窗口。

那么除了包名和类名错误外,还有没有其他原因导致调用其他程序中窗口时失败呢?答案是肯定的。如果调用的窗口不在当前的应用程序中,在AndroidManifest.xml文件中声明窗口时必须允许该窗口被其他应用程序调用,也就是<activity>标签的exported属性值为“true”。现在比较下面两段窗口类(MyActivity)的声明代码。

不可以被其他应用程序通过显式方式调用。

【注】②当然更不能通过隐式方式调用了,因为没用指定Action。

<activity android:name=".MyActivity"/>

可以被其他应用程序通过显式方式调用。

<activity android:name=".MyActivity"android:exported="true"/>

不过我们看到有很多窗口类在声明时并没有设置android:exported属性(该属性的默认值是true),但仍然可以使用显式的方式调用这些窗口。不过好像还不对。既然android:exported属性的默认值为true,那么不设置android:exported属性不也是允许窗口被外部程序调用吗(按第1段代码声明的窗口调用失败)?这对于使用Action调用的方式是没问题的,但对于显式方式必须将android:exported属性设为true才可以。当然,如果为该窗口指定了至少一个Action,就可以不用设置android:exported属性了(当然也不能将该属性设为false,那样外部程序使用任何方式都无法访问该窗口了)。所以显式和隐式跨应用程序调用窗口必须满足如下规则才能成功调用。

显式调用:如果声明窗口时未指定任何Action,android:exported属性必须设置,而且属性值必须为true。如果指定了Action,则并不需要设置android:exported属性,或设置android:exported属性值true也可。

隐式调用:必须指定Action,而且android:exported属性值不需要设置,或设置该属性值为true。

扩展学习:如何得知系统程序的包名和类名

如果要显式调用其他程序的窗口,必须要知道应用的包名和类名。但如果没有APK程序或无法获得APK程序该如何做呢?例如,如果从Google Play安装某个程序,但自己的手机又没有root权限,无法提取该程序的APK文件。但我们还想查看该程序中是否有可以处理某一资源的窗口,如可以浏览网页。现在就拿系统内置的Browser程序为例。

【注】①有APK程序的情况会在本章后面的部分详细讨论。

【注】②root权限相当于Windows的Administrator用户。没有root权限会使得很多事都无法做,例如,无法访问手机内存中的系统目录。

Browser的包名很容易获取,只要在系统的正在运行的应用程序中找到Browser(或浏览器),单击进入如图6-6所示(白框中的就是Browser的包名)。如果没有找到浏览器,可以单击右上角的“显示缓存进程”,如图6-7所示。

 

▲图6-6 浏览器的相关信息

 

▲图6-7 显示所有正在运行的程序

获取Android应用中声明某个Action的窗口的类名也可以使用PackageManager.queryIntent Activities方法,代码如下:

// 查询是否有窗口指定了叫Intent.ACTION_VIEW的Action

Intent intent = new Intent(Intent.ACTION_VIEW);

// 任意指定一个Uri,如果窗口指定了叫http的scheme,系统就会匹配该窗口

intent.setData(Uri.parse("http://blog.csdn.net/nokiaguy"));

// 列出系统中所有这样的窗口信息

List<ResolveInfo> resolveInfos = packageManager

          .queryIntentActivities(intent,PackageManager.GET_INTENT_ FILTERS);

// 通常第一个程序就是Browser,当然,也可以枚举所有的窗口信息

// 在LogCat视图中输出第一个满足条件的窗口的类名(<activity>标签的android:name属性值)

Log.d("Activity Action", String.valueOf(resolveInfos.get(0).activityInfo.name));

前面的章节已多次提到过Android系统通过一些列过滤机制(Action、Category和Data)调用指定的应用程序组件。到现在为止,我们一直关注窗口(Activity),不过另外两种应用程序组件(服务和广播接收器)也同样可以使用这些过滤机制,这一点在后面的章节会详细介绍。本节将介绍Android系统是如何同时利用这些过滤机制定位应用程序组件的。以及在被调用者没有明确说明的情况下,如何按图索骥获取被调用者的相关信息,例如,哪些窗口可以被调用,数据如何传递等。

尽管现在已经知道了Android通过Action、Category和Data的组合来定位窗口,但这3种过滤机制涉及的规则还是很多的,如果不能充分理解它们,就可能设置了错误的Intent Filter,那么也就无法使用我们想要的方式访问这些窗口了。

【注】①由于还没有讲到服务和广播接收器,所以在介绍服务和广播接收器之前将只谈论窗口。

窗口过滤条件首先需要从调用者和被调用者两方面考虑。

1.调用者

调用者就是通过Action调用其他程序中窗口的一端。在这一端需要使用Intent的相应方法设置Action、Category和Data,也就是设置调用的窗口要满足的过滤条件。Action和Data只能指定一个(分别通过setAction和setData方法设置),而Category可以指定多个(通过addCategory方法设置)。设置完过滤条件后,通常会使用Activity.startActivity方法显示满足条件的窗口。例如,下面的代码同时指定了Action、Category和Data,并显示窗口。

// 通过Intent类的构造方法指定了Action,也可以通过Intent.setAction方法指定Action

Intent intent = new Intent("android.intent.action.MYACTION");

// 添加了一个Category

intent.addCategory("android.intent.category.MYCATEGORY5");

// 指定了Data。Data通常是指Uri,也可以只MIMEType

intent.setData(Uri.parse("http://192.168.17.100/work/test.jsp"));

// 显示窗口

startActivity(intent);

2.被调用者

被调用者就是如何声明被调用的窗口。在声明一个窗口类时可以不指定过滤器,也可以指定多个过滤器,每一个过滤器用<intent-filter>标签设置。例如,下面的代码就为一个窗口指定了3个过滤器(过滤器的具体代码已省略)。

<activity android:name="MyActivity">

<intent-filter>……</intent-filter>

   <intent-filter>……</intent-filter>

   <intent-filter>……</intent-filter>

</activity>

在<intent-filter>标签中可以使用<action>、<category>和<data>标签分别设置Action、Category和Data。但要记住<action>标签是必须的,如果不指定<action>,尽管程序可以成功编译和安装,但其他程序将无法通过隐式的方式显示该窗口(不能只指定Category和Data,而不指定Action)。

从常理推测<category>标签应该是可选的,不过实际上<category>也是必须加的,原因就是一个特殊的Category:android.intent.category.DEFAULT。如果调用者不添加Category,按常理会认为过滤条件中不包含Category,系统自然也就不考虑Category了。不过Android系统并不是这样认为的。不管调用者是否添加Category,系统都会认为有一个默认的Category已经被添加。相当于调用者执行如下的代码。

intent.addCategory(Intent.CATEGORY_DEFAULT);

既然调用者默认加入了一个Category,那么被调用这自然也需要在过滤器(<intent-filter>标签)中加入如下的<category>标签了。

<category android:name="android.intent.category.DEFAULT" />

那么有的读者会发现,程序的主窗口的过滤器如下:

<intent-filter>

  <action android:name="android.intent.action.MAIN" />

  <category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

在这个过滤器中并没有指定android.intent.category.DEFAULT,那么这是怎么回事呢?实际上,这是一个例外,如果android.intent.action.MAIN和android.intent.category.LAUNCHER成对出现,并不需要指定android.intent.category.DEFAULT,当然,也可以指定该Category。其他的Action都必须要指定android.intent.category.DEFAULT。

除了<action>和<category>外,过滤器还可以使用<data>指定更细微的过滤信息。<data>标签是可选的,用于指定Uri或MIMEType。关于<action>、<category>和<data>的具体设置方法会在下一节详细讨论。

窗口类的每一个过滤器(<intent-filter>标签)都会在系统中注册,然后系统再根据Intent对象设置的过滤条件筛选符合条件的窗口时会逐一扫描这些过滤器,并会分别根据Action、Category和Data筛选出所有符合条件的过滤器,如果这些被筛选出的过滤器属于多个窗口,系统就会显示一个选择列表,如果只属于一个窗口,就直接显示该窗口。

筛选规则主要应从两个方面考虑。首先应考虑Action、Category和Data在默认情况下代表什么含义。也就是在不设置这3个过滤条件的情况下,系统会如何处理。

不设置Action。由于Action是必选项,所以必须要设置Action,否则该窗口无法被其他的Android应用调用(同一个Android应用还是可以通过显式方式调用的)。

不设置Category。系统会添加一个默认的Category(android.intent.category.DEFAULT)。

不设置Data。系统会认为目标窗口不含有任何Data,也就是说如果不为Intent对象指定Data,在声明目标窗口时也不能在过滤器中使用<data>标签。否则即使Action和Category都匹配,该过滤器也不符合条件。

要考虑的第二个方面是Action、Category和Data如何进行匹配。系统会分别进行3次验证(分别验证这3个过滤条件),只有这3次验证都通过的过滤器才符合要求(该过滤器对应的窗口会成为候选窗口之一)。下面是这3个过滤条件的验证规则。

1.Action验证

如果某个过滤器指定了一个或多个<action>标签(如果没有<action>标签或Intent对象未设置Action,Action验证肯定失败)。系统会扫描所有的<action>标签,如果找到与指定Action相同的<action>标签,就认为Action验证通过。例如,调用者使用下面的代码设置了一个Action(MYACTION1)。

Intent intent = new Intent();

intent.setAction("MYACTION1");

系统中有一个过滤器的代码如下:

<intent-filter>

  <action android:name="MYACTION1"/>

  <action android:name="MYACTION2"/>

  <action android:name="MYACTION3"/>

  ……

</intent-filter>

在该过滤器中恰好有一个<action>标签的android:name属性值是MYACTION1,所以该过滤器通过了Action验证。

2.Category验证

Category验证采用了子集的方法,也就是说为Intent对象指定了N个Category,而在某一个过滤器中设置了M个Category,并且M >= N。那么如果N个Category是M个Category的子集,那么该过滤器通过Category验证。否则不管是M < N,还是其他任何情况,Category验证都失败。例如,下面的代码为Intent对象设置了3个Category。

Intent intent = new Intent();

intent.addCategory("MYCATEGORY1");

intent.addCategory("MYCATEGORY2");

intent.addCategory("MYCATEGORY3");

下面有两个过滤器,代码如下:

①号过滤器。

<intent-filter>

  <category android:name="MYCATEGORY1" />

  <category android:name="MYCATEGORY2" />

  <category android:name="MYCATEGORY3" />

  <category android:name="MYCATEGORY4" />

  ……

</intent-filter>

②号过滤器。

<intent-filter>

  <category android:name="MYCATEGORY1" />

  <category android:name="MYCATEGORY2" />

  <category android:name="MYCATEGORY4" />

  <category android:name="MYCATEGORY5" />

  ……

</intent-filter>

由于①号过滤器包含了Intent对象设置的3个Category,所以第一个过滤器通过了Category验证。但②号过滤器由于缺少MYCATEGORY3,所以没有通过Category验证。

3.Data验证

Data验证用于验证Uri和Mime Type。Uri比Url范围更广,除了可表示Web地址外,还可以表示如下格式的字符串。

scheme://host:port/path

从这个字符串看,将Uri分成了如下4部分。

scheme:协议标识,例如,http、https、content、ftp等。

host:域名、IP等。例如www.google.com、192.168.17.104等。

port:端口号,例如,80、8080等。

path:资源的具体路径。

下面是一个标准的Uri。

http://www.microsoft.com:8080/index.html

Uri的4个组成部分如下。

scheme:http。

host:www.microsoft.com。

port:8080。

path:/index.html。

这里的path指的是完整的路径,但除了可以设置完整的路径外,还可以使用如下两种路径匹配方式。

pathPrefix:路径前缀。所有以pathPrefix开头的path都满足条件。例如“/in”可以匹配“/index.html”,也可以匹配“/in.jsp”。

pathPattern:路径匹配模式。可以使用“*”或“.*”(星号前面加一个点)匹配相应的路径。“*”表示在“*”前面的字符会出现0个或任意多个。“.*”表示0个或任意多个字符。例如,“abcd*e”可以匹配“abce”、“abcde”、“abcdde”、“abcddde”,也就是说在“abc”和“e”之间可以有0或任意多个“d”。而“ab.*cd”可以匹配“abcd”、“abxyzcd”、“ab123cd”,也就是说在“ab”和“cd”之间可以有0或多个任意字符。

【注】①“*”匹配规则实际上就是正则表达式中的星封包,而“.”表示任意字符,所以“.*”可以表示0个或多个字符组成的字符串。

注意

不仅pathPattern属性可以使用通配符,host属性也可以使用“*”表示匹配所有的host(host属性值只能包含“*”,不能包含其他的字符)。

现在我们已经知道一个Uri实际上由6个属性确定,其中前3个是scheme、host和port,还有3个路径(path、pathPrefix和pathPattern)。在一个过滤器(<intent-filter>标签)中可以有多个Data(用<data>标签定义)。系统在匹配每一个Data时会同时考虑这6个属性,但需要遵循如下规则。

(1)scheme是必须的,所以无论是调用者,还是被调用者,如果设置了Uri,必须指定scheme,否则Data验证一定失败。

(2)属性不能跳跃设置,否则跳跃设置的属性将被忽略。例如,设置scheme后直接设置了port,而host并未设置,这被称为跳跃设置。这种情况下port将被忽略,相当于从未设置port属性。当然,如果未设置host,后面再设置path、pathPrefix、pathPattern属性也同样会被忽略。还有就是不管中间跳跃几个属性,跳跃设置的属性同样会被忽略。例如,设置scheme后,host和port都未设置,而直接设置了path,那么path将被忽略。

(3)系统在检测每一个<data>标签时会根据依次检测每一个属性。例如,Intent对象指定的Uri包含scheme、host、port和path。那么系统会在当前的<data>标签中依次检测这4个属性(scheme、host、port和path)是否符合要求(path会检测3个属性:path、pathPrefix和pathPattern,这3个属性只要有一个通过即可)。只有同时符合要求,才会通过Data验证,也就是说这4个属性是“与”的关系。

(4)系统会检索过滤器中的所有Data来查找相应的属性。例如,在一个过滤器中定义了3个Data,其中一个只指定了scheme和host,而另两个Data中只指定了port属性(两个Data指定了不同的属性)。而Uri是http://www.microsoft.com:8080。在Uri中包含了scheme、host和port。系统在验证第一个Data时自然要考虑这3个属性,但scheme和host都通过验证后,发现第一个Data中并没有设置port,这时系统并不认为验证失败,而是看看当前过滤器的其他Data中是否设置了port属性。恰好发现另外两个Data都设置了port属性,系统会分别拿这两个port属性值逐个与Uri中的端口号(8080)比较,只要有一个port是8080,就会通过Data验证。

(5)如果同时指定了path、pathPrefix和pathPattern属性,系统会依次处理这3个属性,直到一个属性的验证通过为止。也就是说这3个属性直接的关系是“或”,而不是“与”。

(6)设置path和pathPrefix属性时必须以“/”开头,例如“/index.html”、“/abc”等。pathPattern属性值如果以“.*”开头,则不需要加“/”,因为“.*”已经包含“/”了。但如果不以“.*”开头,则pathPattern属性值必须以“/”开头。

(7)如果Uri没有指定所有的过滤条件,例如,只指定了scheme和host,那么Data也不能设置Uri未指定的属性。但由于跳跃设置被忽略的属性除外(反正被忽略了,指不指定无所谓)。从这一点可以看出,在定义过滤器时,Data的过滤条件不能超过Uri中包含的信息,否则Data验证必然失败。

(8)如果Uri指定的过滤条件比Data多,并且Uri与Data都指定的过滤条件都通过了验证,则通过Data验证。例如,Uri指定了scheme、host和port,而Data只指定了scheme。如果两个scheme相同,系统则认为通过Data验证。

(9)如果同时设置了Uri与Mime Type(稍后会介绍Mime Type)。系统会分别验证Uri和Mime Type。如果这两个过滤条件都设置在了一个Data上,那么必须都满足条件才通过Data验证。如果Uri与Mime Type分属不同的两个Data,并且这两个Data分别只设置了Uri和Mime Type。系统会分别处理Uri和Mime Type。只要这两个Data的Uri和Mime Type分别满足条件就认为通过了Data认证。

(10)Uri是大小写敏感的,这一点与平常使用的Uri不同。

注意

要牢记上面的几条筛选规则,在下一节会给出一个筛选过滤器的例子,到时读者可以对照这几条以及前面介绍的Action和Category验证规则来研究这个例子。

特别要注意的是Data不仅指Uri,也指Mime Type。Uri和Mime Type可以同时使用,也可以单独使用一个。如果同时使用Uri和Mime Type,要使用Intent.setDataAndType方法进行设置,否则Uri和Mime Type会互相覆盖,这一点在6.2.5小节已经详细介绍过了。

Mime Type就是指可以处理的资源类型,例如“audio/*”用于处理所有的音频资源,“audio/mp3”只用于处理MP3音频格式,“video/*”用于处理所有的视频资源。在Data中使用mimeType属性设置Mime Type。下面所示一个标准的Data,在该Data中同时指定了Uri和Mime Type。这种情况下只有Uri和Mime Type同时通过验证,Data验证才算通过。

<data

  android:host="*"

  android:port="8080"

  android:path="/work/upload.jsp"

  android:scheme="http"

  android:mimeType="audio/*"

  />

综上所述,Data验证要想通过,需要同时通过如下3种验证。

scheme、host、port、path(包括pathPrefix和pathPattern)。这4个属性的关系是“与”,只有同时满足这4个属性,才算通过验证。

path、pathPrefix和pathPattern。这3个属性的关系是“或”,只要通过一个属性就算通过验证。

Uri(scheme、host、port、path、pathPrefix和pathPattern)和Mime Type。关系是“与”,如果同时设置了Uri和Mime Type,必须同时满足条件才算通过验证。当然,如果Intent未设置Uri或Mime Type,而Data设置了Uri或Mime Type,验证同样会失败,反之亦然。

</intent-filter>

源代码目录:src/ch06/FilterProducer、src/ch06/FilterConsumer

有可能很多读者看了前两节关于过滤条件和筛选规则的介绍会感到比较复杂,不过有些筛选规则并不经常使用。例如,path、pathPrefix和pathPattern中使用path会多一些,甚至大多数时候path都很少用。通常Uri只指定scheme,顶多与Mime Type一起使用。

为了使读者能更好地理解Android系统的过滤机制,本节会给出一个例子来看看系统是如何来匹配不同过滤器的。本例由两个Android应用组成,其中FilterProducer中的主窗口类(FilterProducerActivity)设置了多个过滤器,FilterConsumer会通过不同的过滤条件调用FilterProducer的主窗口。

在FilterProducer中为主窗口类设置了7个过滤器,其中第1个过滤器是用于启动窗口的,不需要考虑。后面6个过滤器将被FilterConsumer程序中的6个按钮调用。过滤器的定义代码如下:

源代码文件:src/ch06/FilterProducer/AndroidManifest.xml

<activity

android:name="mobile.android.filter.producer.FilterProducerActivity"

android:label="@string/app_name" >

<!-- ①号过滤器:用于启动窗口,在这里不需要考虑 -->

<intent-filter>

  <action android:name="android.intent.action.MAIN" />

  <category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

<!-- ②号过滤器-->

<intent-filter>

  <action android:name="android.intent.action.MYACTION1" />

  <action android:name="android.intent.action.MYACTION2" />

  <category android:name="android.intent.category.DEFAULT" />

  <category android:name="android.intent.category.MYCATEGORY1" />

  <category android:name="android.intent.category.MYCATEGORY2" />

</intent-filter>

<!-- ③号过滤器-->

<intent-filter>

  <action android:name="android.intent.action.MYACTION3" />

  <action android:name="android.intent.action.MYACTION4" />

  <category android:name="android.intent.category.DEFAULT" />

  <category android:name="android.intent.category.MYCATEGORY3" />

  <category android:name="android.intent.category.MYCATEGORY4" />

<!-- ④号过滤器-->

<intent-filter>

  <action android:name="android.intent.action.MYACTION5" />

  <category android:name="android.intent.category.DEFAULT" />

  <category android:name="android.intent.category.MYCATEGORY5" />

  <!--

      下面给出一个符合条件的Uri,只匹配scheme,Uri其他部分随意指定

      http://www.microsoft.com:8888/index.html

  -->

  <data android:scheme="http" />

  <data android:mimeType="audio/*" />

  <data android:mimeType="video/*" />

</intent-filter>

<!-- ⑤号过滤器-->

<intent-filter>

  <action android:name="android.intent.action.MYACTION6" />

  <category android:name="android.intent.category.DEFAULT" />

  <!--

      下面是一个符合条件的Uri

      http://www.google.com:8080/work/upload.jsp

  -->

  <data

    android:host="*"

    android:mimeType="audio/*"

    android:path="/work/upload.jsp"

    android:port="8080"

    android:scheme="http" />

</intent-filter>

<!-- ⑥号过滤器-->

<intent-filter>

  <action android:name="android.intent.action.MYACTION7" />

  <category android:name="android.intent.category.DEFAULT" />

  <!--

      下面是一个符合条件的Uri

      ftp://192.168.17.100:8080/work/upload.html

  -->

  <data

    android:host="192.168.17.100"

    android:mimeType="audio/*"

    android:port="8080"

    android:scheme="ftp" />

  <data android:pathPattern=".*.html" />

</intent-filter>

<!-- ⑦号过滤器-->

<intent-filter>

  <action android:name="android.intent.action.MYACTION7" />

  <category android:name="android.intent.category.DEFAULT" />

  <!--

      下面是3个符合条件的Uri

      path属性验证通过

      https://192.168.17.111:8888/work/test.up

      pathPattern属性验证通过

      https://192.168.17.111:8888/p/m/abc.html

      pathPrefix属性验证通过

      https://192.168.17.111:8888/test/up.aspx

  -->

  <data

    android:host="192.168.17.111"

    android:mimeType="audio/*"

    android:path="/work/test.up"

    android:pathPattern=".*.html"

     android:pathPrefix="/test"

    android:port="8888"

    android:scheme="https" />

  </intent-filter>

</activity>

下面看一下FilterConsumer类是如何为这6个过滤器的设置筛选条件的。

源代码文件:src/ch06/FilterConsumer/src/mobile/android/filter/consumer/FilterConsumerActivity.java

public class FilterConsumerActivity extends Activity

{  

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_filter_consumer);

  }

  // 第②号过滤器通过验证,利用了Action和Category验证规则,未使用Data

  public void onClick_IntentFilter2(View view)

  {

    Intent intent = new Intent("android.intent.action.MYACTION1");

    // 在这里可以不指定android.intent.category.DEFAULT

    intent.addCategory("android.intent.category.MYCATEGORY1");

    intent.addCategory("android.intent.category.MYCATEGORY2");

    startActivity(intent);

  }

  // ③号过滤器通过验证,未指定Category

  // 因为默认已经为Intent对象添加了android.intent.category.DEFAULT

  public void onClick_IntentFilter3(View view)

  {

    Intent intent = new Intent("android.intent.action.MYACTION4");

    startActivity(intent);

  }

  // ④号过滤器通过验证,Uri匹配了scheme(http),并且匹配了第1个Data(“audio/*”)

  // 在进行Data验证时利用了Data验证规则的第8条

  public void onClick_IntentFilter4(View view) 

  {

    Intent intent = new Intent("android.intent.action.MYACTION5");

    intent.setDataAndType(Uri.parse("http://www.microsoft.com:8888/index.html"), "audio/mp3");

    startActivity(intent); 

  }

  // ⑤号过滤器通过验证

  // 在进行Data验证时利用了Data验证规则的第3、9条

  public void onClick_IntentFilter5(View view) 

  {

    Intent intent = new Intent("android.intent.action.MYACTION6");

    intent.setDataAndType(Uri.parse("http://www.google.com:8080/work/upload.jsp"), "audio/*");

    startActivity(intent);

  }

  // ⑥号过滤器通过验证

  //在进行Data验证时利用了Data验证规则的第3、4、9条

  public void onClick_IntentFilter6(View view) 

  {

    Intent intent = new Intent("android.intent.action.MYACTION7");

    intent.setDataAndType(Uri.parse("ftp://192.168.17.100:8080/work/upload.html"), "audio/*");

    startActivity(intent); 

  } 

  // ⑦号过滤器通过验证

  // 在进行Data验证时利用了Data验证规则的第3、5、9条

  // 下面的代码调用了3次setDataAndType方法,分别设置了3个Uri,其中前两次加了注释,

  // 这3个Uri都符合要求,Uri的路径分别匹配path、pathPrefix和pahtPattern

  public void onClick_IntentFilter7(View view) 

  {

    Intent intent = new Intent("android.intent.action.MYACTION7");

    //intent.setDataAndType(Uri.parse("https://192.168.17.111:8888/work/test.up"), "audio/wav");

    //intent.setDataAndType(Uri.parse("https://192.168.17.111:8888/test/up.aspx"), "audio/wav");

    intent.setDataAndType(Uri.parse("https://192.168.17.111:8888/p/m/abc.html"), "audio/wav");

    startActivity(intent); 

  } 

} 

测试本例首先应安装FilterProducer,然后运行FilterConsumer,并单击主界面上的6个按钮来测试系统的过滤器筛选功能。

源代码目录:src/ch06/AndroidSrcFilter

只从Android官方文档中获取所有的过滤器、Action等信息是不可能的,因为这些文档的编写者未必参与了Android的设计,所以并不一定对Android相关方面的细节描述得特别清楚,而且还有不少错误。因此这就需要我们从其他地方来获取这些知识,例如Android源代码、反编译APK程序等。本节只讨论如何利用Android源代码获取与过滤器相关的信息,在下一节会介绍如何通过反编译的方式获取同样的信息。在本书的其他章节还会继续与读者一起通过分析Android源代码、反编译各种资源的方式获取更多、更权威的信息。

到现在为止已经可以使用多种方式调用另一个应用程序的窗口了,但可能很多读者一直有一个疑问,如果是调用其他应用程序(不是自己编写的程序)中的窗口,又是如何得知这些程序中哪些窗口提供了Action,哪些窗口可以通过显式方式调用呢?这些问题的答案将在本节和下一节为读者揭晓。

在Intent类中定义了一些Action常量,这些常量用来调用系统的一些窗口或接收系统广播。通常选中某一个常量后会在弹出的提示信息框中显示与该窗口相关的解释,如图6-8所示。

 

▲图6-8 Action常量与相应的解释

尽管Intent类中定义了用于调用系统窗口的常量,但并没有为全部的Action定义常量,而且可能有一些解释不太清楚,所以需要采用下面的方法获取更详细的信息。

由于任何Android应用程序的所有窗口都必须在AndroidManifest.xml文件中声明,所以只要获取相应内置程序的AndroidManifest.xml文件的源代码即可。虽然可以通过获取APK文件的方式得到AndroidManifest.xml文件的内容。但由于系统内置程序的APK文件都在系统的目录中(/system/app),如果没有root权限根本访问不了该目录,更别提获取APK文件了。尽管熟悉Linux的读者知道可以用su或sudo命令暂时将用户权限提升到root。但很多手机安装的Android系统根本就没有su或sudo命令,所以为了方法更通用,本节采用了直接查看Android源代码的方法(为了方便读者,在随书光盘中已经包含了最新的Android源代码)获取我们需要的信息。

【注】①APK文件就是zip压缩包,只要解压就可以得到APK文件中的资源。不过Java源代码和XML文件(包括AndroidManifest.xml)都是编译后的,需要一些工具反编译。在6.7节会介绍如何反编译AndroidManifest.xml文件。

【注】②Android模拟器默认就是有root权限的,所以可以从Android模拟器中获得大多数系统内置的APK程序(但有些程序没有包含在Android模拟器中,如Google Play),分析这些APK的方法会在6.7节详细讨论。

Android系统内置应用程序的源代码都放在了如下的目录:

<Android源代码根目录>/packages/apps

每一个应用以一个单独的目录存放,例如,下面是一些常用内置应用的目录。

Browser:浏览器。

Calculator:计算器。

Calendar:日历。

Camera:照相机。

Contacts:联系人。

PackageInstaller:APK安装器。有很多文件管理器在单击APK文件时可以进行安装,就是调用了PackageInstaller中的某个负责安装程序的窗口。

Phone:电话管理,包括拨号、来去电记录等窗口。

Settings:系统设置。

Launcher2:Android系统的启动程序。在系统启动后第一个运行的就是该程序,主要包括桌面、图标、程序列表等。经常提及的定制ROM,UI部分主要就是修改Launcher2。

下面几节会选一些典型的过滤器进行分析,并通过Java代码调用这些过滤器所在的窗口。

计算器是笔者最喜欢的一个程序,因为Calculator可能是唯一没有使用Android SDK内部API的程序,也就意味着Calculator可以单独提出来作为独立的第三方程序,并通过常规的方法安装到系统中。

对于很多初学者来说,由于并没有在Intent类中找到带Calculator的Activity Action,可能第一反应是系统不允许调用Calculator。不过在这里可以肯定地告诉大家,系统中所有带UI的内置程序都允许调用相应的窗口,只是有的Action或Category不太好找罢了。

现在先不考虑Intent类中定义的常量,首先来看看Calculator中AndroidManifest.xml文件的源代码。Calculator程序只有一个窗口类(Calculator),因此AndroidManifest.xml文件的内容也很简单,下面是声明Calculator类的代码。

源代码文件:<Android源代码根目录>/packages/apps/Calculator/AndroidManifest.xml

<activity android:name="Calculator"

     android:theme="@android:style/Theme.Holo.NoActionBar"

     android:windowSoftInputMode="stateAlwaysHidden">

  <intent-filter>

     <action android:name="android.intent.action.MAIN" />

     <category android:name="android.intent.category.DEFAULT" />

     <category android:name="android.intent.category.LAUNCHER" />

     <category android:name="android.intent.category.APP_CALCULATOR" />

  </intent-filter>

</activity>

从这些声明代码可以看出,Calculator只有一个过滤器,在该过滤器中有1个Action和3个Category。读者可以对照6.5节介绍的过滤机制,想一想如何调用Calculator,然后再看下面的内容。

可能通常认为android.intent.action.MAIN是给系统用的,与第三方程序无关。实际上,该Action不仅系统可以调用,第三方程序同样也可以调用。后面定义的3个Category指定一个就可以,不过指定前两个,系统中肯定有重复的定义。所以通常使用下面的代码调用Calculator。

源代码文件:src/ch06/AndroidSrcFilter/src/mobile/android/src/filter/AndroidSrcFilterActivity.java

public void onClick_Calculator(View view)

{

  Intent intent = new Intent("android.intent.action.MAIN");

  intent.addCategory("android.intent.category.APP_CALCULATOR");

  startActivity(intent);

}

当然,android.intent.action.MAIN 和android.intent.category.APP_CALCULATOR在Intent类中已经定义了,所以也可以使用下面的代码调用Calculator。

Intent intent = new Intent(Intent.ACTION_MAIN);

intent.addCategory(Intent.CATEGORY_APP_CALCULATOR);

startActivity(intent);

如果Intent类中已经为某些Action和Category定义了常量,应尽量使用这些常量,而不要直接使用字符串形式的Action和Category。因为如果应用程序中的相应Action和Category改变,可能会影响窗口的调用者,尽管发生这种情况的可能性并不大。在不知道具体的Action或Category定义的常量名的情况下,可以通过查看Android源代码获取相应的字符串形式的Action和Category,然后在Eclipse中跟踪进Intent类,并查找这些字符串,这样就可以很容易找到相应的常量。例如,Intent.ACTION_MAIN和Intent.CATEGORY_APP_CALCULATOR的定义代码如下:

public static final String ACTION_MAIN = "android.intent.action.MAIN";

public static final String CATEGORY_APP_CALCULATOR =

                "android.intent.category.APP_CALCULATOR";

在6.4节的例子中曾使用了显式的方式调用Calculator,这一点从本节给出的Calculator类的声明代码中更容易理解。不过笔者仍然建议尽可能使用隐式方式调用系统的窗口,除非别无选择,才使用显式的方式调用窗口。

注意

Calculator只能简单地显示主窗口,并不能向主窗口传递任何值。由于声明Calculator时未指定Data,所以自然无法传递Uri和Mime Type了。当然还可以通过Extra向主窗口传递数据,不过这就需要直接查看Calculator的源代码了。经过笔者查看,Calculator并没有处理Extra,所以自然也无法通过Extra传递数据了。

在5.7.4小节曾使用下面的代码调用浏览器显示指定的页面。

Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://blog.csdn.net/nokiaguy"));

startActivity(intent);

在代码中使用了Intent.ACTION_VIEW作为Activity Action。Intent.ACTION_VIEW常量的值是android.intent.action.VIEW,在系统中的很多窗口都包含该Action。例如,Browser程序在声明主窗口类(BrowserActivity)时就定义了很多过滤器,这些过滤器有很多都指定了该Action。下面的代码就是其中一个过滤器,也是上面的代码能够匹配的过滤器。

源代码文件:<Android源代码根目录>/packages/apps/Browser/AndroidManifest.xml

<intent-filter>

  <action android:name="android.intent.action.VIEW" />

  <category android:name="android.intent.category.DEFAULT" />

  <category android:name="android.intent.category.BROWSABLE" />

  <data android:scheme="http" />

  <data android:scheme="https" />

  <data android:scheme="about" />

  <data android:scheme="javascript" />

</intent-filter>

在过滤器中除了前面提到的android.intent.action.VIEW外,还指定了两个Category,以及4个Data。现在先说说android.intent.category.BROWSABLE。由于android.intent.action.VIEW在系统中已经被很多过滤器指定了,这些过滤器可能属于不同的窗口。所以只使用该Action可能会有多个窗口符合条件,这也会增大出现选择列表的可能性。因此,调用浏览器时应加上android.intent.category.BROWSABLE,以便尽可能减小显示选择列表的可能性(因为多一个条件,系统中符合所有条件的过滤器就越少),代码如下:

源代码文件:src/ch06/AndroidSrcFilter/src/mobile/android/src/filter/AndroidSrcFilterActivity.java

public void onClick_Calculator(View view)

{

  Intent intent = new Intent("android.intent.action.MAIN");

  // Intent.CATEGORY_BROWSABLE是android.intent.category.BROWSABLE对应的常量

  intent.addCategory(Intent.CATEGORY_BROWSABLE);

  startActivity(intent);

}

最后看一看过滤器中指定的4个Data。通过这4个Data指定了4个scheme:http、https、about和javascript。也就是说Uri必须以如下4个字符串中的一个作为前缀(scheme是以冒号结尾的)。

http:

https:

about:

javascript:

其中about:表示空的内容(什么也不显示),但浏览器地址栏中会显示“about:”,其他3个都需要跟实质性的内容,例如http://www.example.com/abc.html。

我们会发现,使用这个过滤器必须要指定Uri,但如果不想指定Uri该怎么办呢?可能有的读者会将上面代码中Intent类的构造方法第2个参数去掉,不过执行代码后会发现抛出异常,说明声明BrowserActivity类时根本就没有定义这样的过滤器。不过BrowserActivity的过滤器还有很多,经过仔细查找,还是找到了如下的过滤器。

源代码文件:<Android源代码根目录>/packages/apps/Browser/AndroidManifest.xml

<intent-filter>

  <action android:name="android.intent.action.MAIN" />

  <category android:name="android.intent.category.DEFAULT" />

  <category android:name="android.intent.category.LAUNCHER" />

  <category android:name="android.intent.category.BROWSABLE" />

  <category android:name="android.intent.category.APP_BROWSER" />

</intent-filter>

这个过滤器并没有指定Data,但却有用于启动程序的Action和Category。说明这个过滤器的功能之一是给系统用于单击浏览器程序图标时显示BrowserActivity窗口的。不过最后又定义了两个Category:android.intent.category.BROWSABLE和android.intent.category.APP_BROWSER。因此可以直接用android.intent.action.MAIN和这两个Category显示BrowserActivity,代码如下:

Intent intent = new Intent("android.intent.action.MAIN");

intent.addCategory(Intent.CATEGORY_BROWSABLE);

// Intent.CATEGORY_APP_BROWSER是android.intent.category.APP_BROWSER对应的常量

intent.addCategory(Intent.CATEGORY_APP_BROWSER);

startActivity(intent);

但要注意最好不要单独只用其中一个Category,否则可能会有多个过滤器满足过滤条件,这就有可能显示选择列表了。例如,只指定Intent.CATEGORY_BROWSABLE,可能会显示如图6-9所示的选择列表。可以看到,Browser、People和Phone程序中都有窗口满足过滤条件。

 

▲图6-9 多个窗口符合过滤条件下的选择列表

答疑解惑:为什么显式调用浏览器会导致Uri无效

在6.4节中曾在InvokeOtherActivity程序中使用显式的方式调用了WebBrowser中的主窗口用于显示指定的网页,在这个例子中程序可以完美地运行。但如果我们使用这种方法显式调用系统内置的浏览器会发现,尽管浏览器可以成功调用,但往里传入的Uri却无效了。究其原因是因为在我们实现的WebBrowser程序的主窗口类的onCreate方法中不管三七二十一都从Intent.getData方法中获取了Uri,所以无论何种情况,只要主窗口成功显示,就一定会获取传入的Uri。而系统内置的浏览器对Action进行了验证。只有在Action和Category符合要求的情况下才会继续读取Data中的数据。

系统浏览器在启动时会通过Intent.getAction方法返回一个Action,如果要让该方法返回非空值,必须使用下面两种方法调用窗口。

隐式调用窗口。

单击程序图标启动程序。

第1种情况就不需要多讲了,因为已经为窗口指定了一个Action,Intent.getAction方法返回的就是这个Action。而第2种情况Intent.getAction方法返回了android.intent.action.MAIN。读者可以在onCreate方法中加入如下的代码,看看在启动程序时是否会在LogCat视图输出这个Action。

if (getIntent().getAction() != null)

  Log.d("action", getIntent().getAction());

如果直接使用显式方法调用窗口,Intent.getAction方法会返回null。而从本节前面给出的过滤器代码可知,要处理Uri,Action必须是android.intent.action.VIEW。所以如果显式调用浏览器的BrowserActivity窗口,根本不可能通过Action检测,因此当然不会执行到接收Uri的代码了,这也是为什么显式调用系统浏览器显示网页后,Uri会被忽略的原因。

拨打电话在5.7.1小节已经介绍过了,现在再来回顾一下拨号的过程,代码如下:

Intent callIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:12345678"));

startActivity(callIntent);

其中Intent.ACTION_CALL的值是android.intent.action.CALL。现在来研究一下具体哪个程序中的哪个窗口来响应上面代码的请求。

由于本功能是拨打电话,所以很自然就会想到是调用了Phone程序中的某个窗口。打开该程序的AndroidManifest.xml文件后,根据android.intent.action.CALL会找到如下的过滤器。

源代码文件:<Android源代码根目录>/packages/apps/Phone/AndroidManifest.xml

<intent-filter>

  <action android:name="android.intent.action.CALL" />

  <category android:name="android.intent.category.DEFAULT" />

  <data android:scheme="tel" />

</intent-filter>

从这段过滤器的代码中会看到除了指定Action和Category外,还指定了一个叫tel的scheme。所以通过Uri指定电话号时一定要指定“tel:”前缀(由于Uri区分大小写,所以不能写成TEL、Tel等形式)。

到这里还不算完,我们会发现只执行前面两行代码会抛出异常,异常的大概意思是没有权限调用拨号窗口。出现这个问题的关键是拨号盘的权限设置,也就是上面过滤器所在的窗口类,该类的声明代码如下:

<activity android:name="OutgoingCallBroadcaster"

    android:theme="@style/OutgoingCallBroadcasterTheme"

    android:permission="android.permission.CALL_PHONE"

    android:screenOrientation="portrait"

    android:configChanges="orientation screenSize keyboardHidden">

  ……

</activity>

从拨号盘类(OutgoingCallBroadcaster)的声明代码可以看出,在<activity>标签中设置了android:permission属性,并指定了android.permission.CALL_PHONE权限,所以在调用者的AndroidManifest.xml文件中要加入如下的代码才可以成功进行拨号。

<uses-permission android:name="android.permission.CALL_PHONE"/>

注意

前面已经有很多代码涉及Uri。Uri的标准格式是“xxx://host:port/path”。但有的Uri并未指定“//”,例如“tel:12345678”。实际上这是由相应的程序进行处理的,因为如果不指定“//”,是无法通过Uri.getHost方法获取Host的,自然也就无法获取电话号的。不过处理来去电的程序(Phone)会从“tel:12345678”中解析出电话号。如果某些程序只能从Uri的host中获取数据,那就必须加“//”了。

源代码目录:src/ch06/InvokeSinaWeibo

现在很多程序中使用的功能并未包含在程序本身,都是通过各种插件方式获得的其他程序的资源。例如,经常会使用到的“分享”功能就是调用了系统中所有可以处理某类资源的窗口来实现的,如果这样的窗口多于一个,就会显示一个选择列表。

如果想利用其他程序中的资源,首先要了解程序对外的接口。但很多程序并没有公开相应的接口,在这种情况下,就需要通过该程序的APK文件获取我们需要的信息。本节就以新浪微博Android客户端分析如何获取第三方程序中可调用的窗口和调用方法。读者可从官方网站(http://weibo.com)下载最新的新浪微博客户端安装包(APK文件)。安装完后应先登录微博再测试本例。

由于APK文件就是ZIP压缩格式文件,所以读者可以随便找一个解压软件将APK文件解开。这里假设APK文件的文件名为sina_weibo.apk,解开sina_weibo.apk文件后,再解压根目录会找到一个AndroidManifest.xml文件,不过该文件是被编译过的,无法使用文本编辑器查看其内容。但可以使用一些工具将其反编译。本节介绍一个简单的工具AXMLPrinter2,该工具用Java编写,是一个jar文件,所以可以在任何支持JDK的操作系统平台上使用。该工具的下载地址如下:

http://android4me.googlecode.com/files/AXMLPrinter2.jar

读者也可以在随书光盘的tools目录中找到该工具。

现在将AXMLPrinter2.jar文件与AndroidManifest.xml文件放到一个目录,然后在控制台(终端)进入该目录,并执行如下的命令,将AndroidManifest.xml文件解压为AndroidManifest.txt。

java -jar ./AXMLPrinter.jar ./AndroidManifest.xml > ./AndroidManifest.txt

现在用任何一个文本编辑器打开AndroidManifest.txt,除了引用的资源都变成了十六进制的值外,其他的内容都恢复了原样。

本例会使用sina_weibo.apk的3个功能:显示微博主界面、发布微博(显示空的微博发布窗口)和分享微博(将要发布的文本和图像传入微博发布窗口)。在运行本例之前一定要安装sina_weibo.apk,否则程序会因为没找到相应的窗口而抛出异常。

首先来看如何显示微博主界面。既然显示的是主界面,那么一定是单击程序图标显示的窗口,也就是包含android.intent.action.MAIN的窗口,了解了这些后,马上在AndroidManifest.txt文件中搜索android.intent.action.MAIN,我们会找到一个SplashActivity窗口,这是显示程序封面的窗口,该窗口关闭后,就会显示微博主窗口(带微博列表的窗口),由于在SplashActivity类中需要做一些初始化工作,所以不能直接显示主窗口(MainTabActivity)。

现在搜素SplashActivity窗口的过滤器,会找到如下一个过滤器。我们会发现这个过滤器很眼熟。在6.6.3小节介绍的系统浏览器(Browser)也使用了类似的过滤器,只不过scheme不同,而且也没指定host。

<intent-filter>

  <action

    android:name="android.intent.action.VIEW">

  </action>

  <category

    android:name="android.intent.category.DEFAULT">

  </category>

  <category

    android:name="android.intent.category.BROWSABLE">

  </category>

  <data

    android:scheme="sinaweibo"

    android:host="splash">

  </data>

</intent-filter>

如果读者仔细阅读了6.5节的内容,应该很容易知道如何显示这个过滤器所在的窗口,代码如下。

源代码文件:src/ch06/InvokeSinaWeibo/src/mobile/android/invoke/sina/weibo/InvokeSinaWeibo Activity.java

public void onClick_StartSinaWeibo(View view)

{

  Intent intent = new Intent("android.intent.action.VIEW");

  intent.addCategory("android.intent.category.BROWSABLE");

  intent.setData(Uri.parse("sinaweibo://splash"));

  startActivity(intent);

}

现在要找到可以发布新微博的窗口,继续搜索AndroidManifest.txt文件的内容(主要搜索<action>标签),会找到一个叫com.sina.weibo.intent.action.NEW_BLOG的Action,对应的窗口类是EditActivity。现在用下面的代码测试一下,发现完全可以显示微博的发布界面。

源代码文件:src/ch06/InvokeSinaWeibo/src/mobile/android/invoke/sina/weibo/InvokeSinaWeibo Activity.java

public void onClick_PostWeibo(View view)

{

  Intent intent = new Intent("com.sina.weibo.intent.action.NEW_BLOG");

  startActivity(intent);

}

最后要实现“分享”功能,这个功能需要使用前面涉及的一个通用的Action,就是Intent.ACTION_SEND(android.intent.action.SEND),分享微博的代码如下:

public void onClick_Share(View view)

{

  Intent intent = new Intent("android.intent.action.SEND");

  // 设置要分享的文本

  intent.putExtra(Intent.EXTRA_TEXT, "今天气温很低,注意保暖哦,亲!");

  // 必须指定Mime Type为text/plain

  intent.setType("text/plain");

  startActivity(intent);

}

如果读者还想确定一下的话,可以在AndroidManifest.txt文件中搜索android.intent.action.SEND,会找到两个指定该Action的过滤器。一个设置了Mime Type为“text/plain”,用于处理文本数据;另一个Mime Type为“image/*”,用于处理文本和图像数据。如果想同时处理文本和图像数据(发布带图像的微博),可以使用下面的代码。

Intent intent = new Intent("android.intent.action.SEND");

intent.putExtra(Intent.EXTRA_TEXT, "今天气温很低,注意保暖哦,亲!");

// 需要指定图像文件的绝对路径

intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file:///sdcard/test.jpg"));

// 通常设为Mime Type为image/*

intent.setType("image/*");

startActivity(intent);

如果使用上面的代码可能会有多个窗口符合条件,这样就会显示选择列表。如果不想显示选择列表,直接调用新浪微博的分享微博界面,可以使用下面的代码。

public void onClick_ShareWithSinaWeibo(View view)

{

  Intent intent = new Intent("android.intent.action.SEND");

  // 直接指定Package Name和Activity Class Name

  intent.setClassName("com.sina.weibo", "com.sina.weibo.EditActivity");

  intent.putExtra(Intent.EXTRA_TEXT, "今天气温很低,注意保暖哦,亲!");

  intent.setType("text/plain");

  startActivity(intent);

}

注意

使用显式方式调用EditActivity,也需要指定android.intent.action.SEND,这是因为在微博程序内部检测了Action,如果不为Intent对象指定Action,则Action为null,系统就不会继续处理传递的数据了。如果想通过显式方式分享文本和图像,对Intent对象的设置与隐式方式完全一样。

如果读者想调用系统内置的程序,并且没有Android源代码,也可以从Android模拟器上使用adb pull命令下载相应的APK文件,并按着本节的方法进行分析。

在显示窗口时通常可以使用如下两种方式向窗口传递数据。

通过Data传递数据。

通过putExtra传递附加数据。

第1种方法在前面已经多次使用过了,通过Data传递数据实际上就是通过Uri将数据传递给窗口。例如,传递电话号可以用“tel:12345678”。第1种方式虽然传递数据比较方便,但只能传递有限的数据,如果要传递的数据量比较大,而且数据类型比较多(如int、boolean、byte[]等类型),就要使用第2种方式传递附加数据。

Intent类有多个重载的putExtra方法,这些方法用于向Intent对象写入不同类型的数据,putExtra方法有两个参数,第1个参数是key,第2个参数是value。也就是putExtra方法会将一个key-value,类型的数据存入Intent对象,然后Intent.getXxxExtra方法可以通过key获取value。其中Xxx表示Int、Char、String等。例如,getIntExtra、getCharExtra和getStringExtra都是这种类型的方法。getXxxExtra方法也有两个参数,第1个参数是key,第2个参数是defaultValue。当Intent对象中没有key时就返回defaultValue。

【注】①getStringExtra方法是个例外,该方法只有一个参数,没有默认值。

下面的代码使用putExtra方法写入了String、int和char类型的数据,并使用getXxxExtra方法获取了这些数据。

// 写入附加数据

Intent intent = new Intent(this, MyActivity.class);

intent.putExtra("key1", 20);

intent.putExtra("key2", 'c');

intent.putExtra("key3","value");

 

// 读取附加数据

// 0是默认值

int n = intent.getIntExtra("key1", 0);

// 'c'是默认值

char c = intent.getCharExtra("key2", 'c');

String s = intent.getStringExtra("key3");

除了可以使用Intent.putExtra设置附加数据外,还可以使用Bundle对象设置完附加数据后,使用Intent.putExtras方法设置一个Bundle对象,代码如下:

// 写入附加数据

Intent intent = new Intent(this, MyActivity.class);

Bundle bundle = new Bundle();

bundle.putInt("key1", 20);

bundle.putChar("key2", 'c');

bundle.putString("key3", "value");

intent.putExtras(bundle);

读取通过Bundle对象写入的数据时可以用Intent.getXxxExtra方法,也可以通过Intent.getExtras方法获取Bundle对象后,然后再通过Bundle.get或Bundle.getXxx方法(其中Xxx表示String、Int等字符串)获取相应的数据。其中Bundle.get方法只返回一个Object对象,可以封装任何类型的数据。而Bundle.getXxx方法通常有两个重载形式,一个没有默认值(只有一个参数用于传递key),另外一个有两个参数,一个是key,另一个是defaultValue。

实际上,Intent.putExtra和Bundle.putXxx方法本质上是一样的,如果不用Intent.putExtras方法设置Bundle对象,Intent会在内部创建一个新的Bundle对象。如果设置了Bundle对象,Intent就直接使用该Bundle对象。这一点从Intent.putExtra和Intent.putExtras方法的源代码就可以看出。

public Intent putExtra(String name, String value)

{

  if (mExtras == null)

{

    mExtras = new Bundle();

  }

  mExtras.putString(name, value);

  return this;

}

 

public Intent putExtras(Bundle extras)

{

  if (mExtras == null)

  {

    mExtras = new Bundle();

  }

  // 将extras中的key-value对都添加到内部的Bundle对象中(mExtras)

  mExtras.putAll(extras);

  return this;

}

要注意的是,如果通过key获取的value的数据类型与返回值数据类型不同时,并不会进行类型转换,而会返回默认值,或返回null(对于字符串类型的value)。例如,下面的代码中尽管key存在,但由于类型不匹配,所以仍然返回int的默认值。

Intent intent = new Intent();

// value的数据类型是String

intent.putExtra("key", "40");

// n的值为120(默认值)

int n = intent.getIntExtra("key", 120);

在显示窗口时,除了会通过Intent对象传递数据外,还需要在被调用窗口中获取这些数据。例如,向某个窗口传入了一个Uri。通常会在窗口类的onCreate方法中将Uri分解,获取Uri的各个部分,例如,scheme、host、port、path等。这些信息都可以从Intent类的相应方法获取。除了上一节介绍的获取附加数据的方法外,Intent类还有如下可以获取各种信息的方法。

获取Action:Intent.getAction方法

getAction方法用于获取调用者传递过来的Action,可用该方法的返回值判断是通过哪个Action调用的该窗口。getAction方法的原型如下:

public String getAction()

获取多个Category:Intent.getCategories方法

由于显示窗口时可以指定多个Category,而且这些Category与顺序无关,所以getCategories方法返回一个Set<String>类型的对象。GetCategories方法的原型如下:

public Set<String> getCategories()

获取Mime Type:Intent.getType方法

该方法可以获取字符串形式的MimeType。getType方法的原型如下:

public String getType()

获取与Data相关的信息

Data就是Uri,在Intent类中有多个方法可以获取与Uri相关的信息,这些方法的原型如下:

// 直接获取Uri对象

public Uri getData()

// 获取字符串形式的Uri

public String getDataString()

// 获取scheme

public String getScheme()

获取Flags:Intent.getFlags方法

在显示窗口时可以通过Intent.setFlags方法设置一个或多个Flag,以干预窗口创建和销毁的过程,通过Intent.getFlags方法也可以获取这些Flag。关于窗口Flags的详细内容会在下一章介绍。getFlags方法的原型如下:

public int getFlags()

本章非常翔实地讨论了Intent与Intent Filter各个方面的技术。尽管Intent和Intent Filter看上去不如Activity以及后面要介绍的Service、Broadcast Receiver和Content Provider复杂,也没有绚丽的效果。但Intent和Intent Filter却是整个Android系统的“神经中枢”和“血管网络”,负责交换数据和过滤不需要的数据。就像人体一样,尽管心脏、肺、肝等器官非常重要,但要没了神经中枢和血管网络连接每一个器官,后果就会很严重!所以这一章还是非常重要的,充分理解Intent和Intent Filter的原理也会更加容易学习后面章节的内容。

 

图书在版编目(CIP)数据

Android开发权威指南/李宁编著.--2版.--北京:人民邮电出版社,2013.9

ISBN 978-7-115-32033-9

Ⅰ.①A… Ⅱ.①李… Ⅲ.①移动终端—应用程序—程序设计—指南 Ⅳ.①TN929.53-62

中国版本图书馆CIP数据核字(2013)第109585号

内容提要

本书是畅销书《Android开发权威指南》的升级版,内容更新超过80%,是一本全面介绍Android应用开发的专著,拥有45章精彩内容供读者学习。

本书全面介绍了Android应用开发的各种技术,主要内容包括Android的四大应用程序组件(Activity、Service、Content Provider和Broadcast Receiver)、布局、菜单、控件、资源和本地化、可视化UI设计、对话框、Toast和通知栏(Notification)、Fragment、ActionBar、拖放技术、数据存储技术、Android支持的各种组件技术、通信技术(蓝牙、NFC、网络等)、异步装载、推送技术、多媒体、2D绘图技术、OpenGL ES、桌面小部件、动态壁纸、短信和彩信、传感器、GPS、谷歌地图、人机交互、输入法开发、Android NDK开发、测试驱动开发、性能优化等。

本书并不局限于介绍Android的各种理论知识,而是从“实战”的角度介绍开发技术。本书的所有示例都是精心设计的,足以使读者对当前讲解的理论知识驾轻就熟。这些示例中的很多例子都源于真实的项目,读者可以对这些示例的源程序稍加改动就可用于自己的项目中。本书除了为每一个知识点提供完整的示例外,最后还提供了两个完整的项目:笑脸连连看和NFC侠。这两个项目极具参考价值,尤其是NFC侠,是笔者维护的一个开源项目,用于在不支持NFC的手机或Android模拟器上开发和测试NFC应用。读者可以关注我的Blog(http://blog.csdn.net/nokiaguy)来获得NFC侠以及本书的最新信息。

本书适合有一定Java基础的读者阅读,如果读者已经掌握了Java的基本语法,那么本书将会成为您的首选和枕边书,您将从书中获得更多有价值的Android开发技术。

◆编著 李宁

责任编辑 张涛

责任印制 程彦红 杨林杰

◆人民邮电出版社出版发行  北京市崇文区夕照寺街14号

邮编 100061  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京中新伟业印刷有限公司印刷

◆开本:787×1092 1/16

印张:47.25

字数:1307千字  2013年9月第2版

印数:8501-12000册  2013年9月北京第1次印刷

定价:99.00元(附光盘)

读者服务热线:(010)67132692 印装质量热线:(010)67129223

反盗版热线:(010)67171154

广告经营许可证:京崇工商广字第0021号

相关图书

Android App开发入门与实战
Android App开发入门与实战
Kotlin入门与实战
Kotlin入门与实战
Android 并发开发
Android 并发开发
Android APP开发实战——从规划到上线全程详解
Android APP开发实战——从规划到上线全程详解
Android应用案例开发大全( 第4版)
Android应用案例开发大全( 第4版)
深入理解Android内核设计思想(第2版)(上下册)
深入理解Android内核设计思想(第2版)(上下册)

相关文章

相关课程