第4章 Intents和Intent Filters
一个Android应用程序的三个核心组件-activities,services,boradcast receivers都是通过调用intents消息来激活的。Intent消息传递是在最近运行的组件之间的一种设施,它能用于相同的或不同的应用程序中。intent它本身是一个Intent对象,是一种把想要执行的操作抽象化的数据结构,或者使用广播描述发生或者宣布一些事情。传递每一种组件类型的intent都有一个单独的机制:
1. 一个Intent对象通过Context.startActivity()或者Activity.startActivityForResult()来启动一个Activity或者获取一个存在的activity来做些事情。(也能通过调用Activity.setResult()来返回调用startActivityForResult()的activity的信息)
2. 一个Intent对象通过Context.startService()来初始化一个service或传递新的指令到一个正在进行中的service。同样地,一个intent能通过调用Context.bindService()来在组件和一个目标service之间建立一个连接。如果service没有运行,它能根据情况初始化service。
3. 一个Intent对象通过任意的广播方法(如
Context.sendBroadcast(),Context.sendOrderedBroadcast(),Context.sendStickyBroadcast())传递到相关的广播接收者(broadcast receivers)。许多种广播的起源于系统代码。
在不同情况下,Android系统找到适当的activity,service,或者一组广播接收者来响应intent,如果必要的话还会实例化它们。在这些消息传递系统内不会有重叠:Broadcast(广播)intent只用于在broadcast receivers中传递消息,绝对不会牵扯到activity和service。一个intent通过startActivity()调用的仅用于activity传递,service类似。本章内容开始讲述Intent对象。然后讲述intent与组件映射的规则等
4.1 Intent对象
一个Intent对象是一个Bundle(Android中的一个类,类似于Hashmap)信息。它能包含以下主要内容:
1. 组件名
这是intent将要处理的组件名字。这个属性是一个ComponentName对象,它是一个目标组件完整名字的组合。(如com.example.project.app.FreneticActivity)并且在应用程序里manifest文件中设置的包名是组件名的一个部分(如com.example.project)。组件名是可选的。如果它被设置了,Intent对象传递一个指定类的实例。如果没有设置,Android使用intent对象中的其他信息来查找合适的目标。设置组件名可以通过setComponent(),setClass(),setClassName(),读取组件名可以通过getComponent()。
2. Action(动作)
一个执行动作的字符串名字,或者在广播中有一个动作发生并报告给系统。Intent类中定义了一些action常量,如表格4-1所示:
常量 | 目标组件 | Action(动作) |
| activity | 拨打一个电话 |
| activity | 显示用户编辑的数据 |
| activity | 把这个activity当成“main activity”在任务中启动,没有数据输入也不需要返回输出 |
| activity | 同步服务器和移动设备的数据 |
| broadcast receiver | 电量低的警告 |
| broadcast receiver | 一个耳机插入或拔出设备 |
| broadcast receiver | 屏幕被打开. |
| broadcast receiver | 设置时区被更改后的广播 |
表格4-1 action的概述
除了系统自带的Action之外,你也可以定义自己的Actiong字符。定义的字符串应该包含你应用的包名,例如:“com.example.project.SHOW_COLOR”。你可以使用setAction()和getAction(),或者直接在new Intent(String action)。
3. Data(数据)
描述数据的URI及数据类型。不同的动作匹配不同种类的数据描述。比如,ACTION_EDIT,那么数据域应该包含一个待编辑的文档URI。数据类型也是非常重要的描述,因为当你在操作一个图片时,不可能去使用音频的数据类型来操作。大部分情况下,数据类型可以从URI识别出来,比如content:uri,指明了数据是在本地查找并通过ContentProvider控制。如果动作是ACTION_VIEW并且数据字段是一个http:URI,接收的activity将被要求下载并显示URI引用的数据。当然,也可以通过Intent对象的方法setData()来指定Uri,setType()来指定数据类型。同过setDataAndType()方法竟可以指定uri也可以指定数据类型。通过getData()及getType()方法读取数据uri及数据类型。
4. Category(范畴)
一个Intent可以包含多个Category。Intent本身定义的Category如表格4-2所示:
常量 | 含义 |
| 目标Activity可以安全地被浏览器调用,并通过链接在浏览器中显示数据。比如图片或者Emai消息 |
| Activity被当成一个小部件可以被嵌入到另一个宿主activit中 |
| 设备开启或按Home键时,显示的activity。这个可以让你装一个自己开发的应用来代替目前的home屏幕 |
| 大家非常熟悉的,最通俗的解释就是“main activity” |
| 这个activity是一个preference面板 |
表格4-2 category的概述
addCategory()方法放置一个category到Intent中,removeCategory()方法删除一个category,getCategory()方法获取Intent设置的categoies。
5. Extras(额外部分)
键值对格式的额外信息。例如一个 ACTION_TIMEZONE_CHANGED intent有一个“time-zone”的额外数据用来指定新时区,一个 ACTION_HEADSET_PLUG 有一个“state”的额外数据来表示耳机是插入还是拔出。你可以通过一个Bundle 的putExtras()和getExtras()方法。Intent提供了put…()和get…()方法。
6. Flags(额外部分)
各种各样的Flags。关于Android系统如何启动一个activity有许多指令(例如,这个activity属于哪个任务),还有在activity启动后如何处理它(如是否属于最近的activity列表)。所有的Flags在Intent中都有定义。
你还可以通过Intent使用系统自带的一些组件。请访问
4.2 Intent解决方案
Intent有两组:
1. 明确的intents 通过名字指定目标组件(具体名字可以参考4.1中的组件名)。由于开发者通常不知道组件的名字,所以明确的intents通常会用在应用内部的信息传递中,例如一个activity调用一个从属的service或者是启动一个同级别的activity。
2. 隐式的intents 没有指明目标组件的名字。这种Intent通常是用来激活其他应用里的组件。
Android把明确的intent传递给指定的目标类。在Intent中没有什么比指定目标组件的名字更重要的了,因为它决定了是哪个组件来接收intent。
对于隐式的intent就要使用不同的策略了。在没有指定目标类的情况下,Android System必须找到最合适的组件去处理intent—一个单独的activity或者是service又或者是一组的broadcast receivers。Android会把intent的内容拿来与intent filters(过滤器)和相关联的结构相比较。Filters对外声明了组件的作用,对内限定了intent能处理的事情。intent filters的目的就是让组件可以接收隐式的intents。如果一个组件没有intent filters,那么他只能接收明确的intent。拥有intent filters的组件可以接收明确的intent也能接收隐式的intent。
Intent filters要对intent的三个内容进行校验:
action
data(URI和数据类型)
category
Extra和Flag这一方面并不发挥作用。
4.2.1Intent filters
Activity,service 和 broadcast receiver为了通知系统他们可以处理哪些隐式的intent,通常他们都有一个或是更多的intent filters。每个filters描述了组件的一个能力也限定一些组件可以接收的intent。实际上filters是筛选组件想要的intent,而不是剔除不需要的intent。明确的intent总是被传递给指定的目标,无论filters里设置了什么条件。
一个组件会把filters分开,让他们各自去做自己的事情。例如,NoteEditor activity拥有两个filters,一个是为了启动用于查看和编辑的note,另一个是为了启动一个新的,空白的note用于填写和保存。(所有的Note Pad's filters在API Demo中的 都有描述)。关于filters和安全,请注意一个Intent filters并不安全。因为他不能阻止明确的intent来干扰。即使它阻止了,别人依然可以使用不同的数据源绑定到一个明确的intent上,并且名字和目标组件相同。一个intent filters就是一个IntentFilter类的实例。然后由于android系统在加载组件前必须知道这个组件的功能,所以intent filters通常不在java代码中设置,而在Andrid Manifest.xml中的<intent-filters>节点中设置。(有一个例外就是broadcast receivers的filters是通过Context.registerReceiver()动态注册的,它就可以使用IntentFilter类)Intent中的filter字段与action,data,category它们是平行关系。一个隐式的intent要想通过intent filter必须要在action,data,category三个方面通过校验。如果其中一个没有通过,Android 系统就不会把intent传递给组件。然而,由于一个组件拥有很多的intent过滤器,所以只要通过其中一个就可以了。下面是一些细节测试:
1. Action test
manifest文件中的<intent-filter>节点下,列出了<action>子节点。例如代码清单4-1所示:
. . .
代码清单4-1
如范例所展示,当一个Intent对象只命名了一个动作,filter可能会列举多个。列表不能为空;一个filter必须至少包含一个<action>元素,否则它将包括所有的intents。
为了通过这个测试,Intent对象中所指定的动作必须与filter中所列举的其中一个动作匹配。如果这个intent对象或filter没有指定一个动作,其结果如下:
◆如果filter没有列举任何动作,那么就没有动作能够用来与intent相匹配,所以,所有的intents都不能通过测试。没有intents可以通过filter。
◆另一方面,一个Intent对象没有指定任何动作,将自动通过测试——只要过滤器含有至少一个动作。
2. Category test
一个<intent-filter>节点,同样也把分类(Category)当作子元素列举,如代码清单4-2所示:
. . .
代码清单4-2
注意,表格4-2中讨论的动作(action)和分类(Category)常量并不适用于manifest文件,在manifest中要使用完全字符串值。例如,上面范例中的android.intent.category.BROWSABLE"字符串对应了表格4-2中讨论到的CATEGORY_BROWSABLE常量。相似地,"android.intent.action.EDIT"字符串对应了ACTION_EDIT常量。对于一个intent,想要通过Category测试,intent对象中的每一个Category都必须匹配filter 中的一个Category。filter 可以列举额外的Category,但不能忽略intent中的任何Category。因此从理论上来说,不考虑filter中的值,一个没有Category的intent对象应该始终能通过这个测试。大部分情况下是对的。然而,有一个例外,Android把所有传递给startActivity()隐式的intents都看作是至少有一个Category:"android.intent.category.DEFAULT"(即CATEGORY_DEFAULT常量)。所以,想要接收隐式的intents对象的activity必须在intent filters中包含"android.intent.category.DEFAULT"(filters中含有"android.intent.action.MAIN"和"android.intent.category.LAUNCHER"的是一个例外。它们标记了activity开始新任务,并显示在启动屏幕上。虽然它可以包括"android.intent.category.DEFAULT",但不需要这么做。)
3. Data test
如同action与Category,intent filters中的data指定也包含在了一个子元素当中。并且,在这种情况下,子元素可以多次出现,或不出现。如代码清单4-3所示:
. . .
代码清单4-3
每一个<data>节点可以指定一个URI和一个数据类型(MIME媒体类型)。URI由scheme,host,port和path组成:
scheme://host:port/path
例如,在以下的URI中,
content://com.example.project:200/folder/subfolder/etc
其中,scheme为"content",host为"com.example.project",port是"200",path为"folder/subfolder/etc"。host与port一起,组成了URI权限,如果没有指定host,那么port也将被无视。这些属性都是可选的,但它们之间并不相互独立:为了有意义的授权,scheme必须被指定。为了使路径(path)有意义,scheme和权限(authority)都必须被指定。当Intent对象中的URI与filters中指定的URI对比时,只对比在filters中提及的部分。例如,如果filters中听指定了一个scheme,所有拥有这个scheme的URIs都能匹配。如果filters指定了一个scheme和权限(authority),但没有路径(path),所有拥有相同scheme和权限(authority)的URIs将匹配,不考虑其路径(path)。如果authority指定了scheme,权限(authority)和路径(path),那么,只有在URIs中这三个属性相同的时才匹配。然而,相同的中的路径指定可以包含通用符,来要求路径的部分匹配。
<data>元素中的type属性指定了数据的MIME类型。在filters中,它比URI更常见。Intent对象和filters都可以使用"*"通配符匹配子类型域——如,"text/*"或"audio/*"——指定任意一个匹配的子类型。
Data测试与Intent对象中和filters中所指定的URI和数据类型相比。规则如下:
(1)一个既不包含URI又没有指定数据类型的Intent对象只有在filters也同样什么都没指定的情况下才能通过测试。
(2)任意一个只包含了URI但没有数据类型(并且无法从URI中推断出其类型)只有在这个URI与filters中的一个URI相匹配,并且同样没有指定数据类型的情况下通过测试。当URI为mailto:和tel:时即为这种情况,并不能确切的知道数据类开。
(3)Intent对象中包含了一个数据类型但没有URI时,只有在filters列举了相同的数据类型,并且类似有没有指定URI时通过测试。
(4)Intent对象同时包含了一个URI和一个数据类型(如数据类型可以由URI推断出)只有在该类型与filters中所列类型匹配时才能通过测试。它将通过URI部分的测试,要么URI与filters的中某个匹配,要么其包含了一个content:或file:URI并且filters没有指定一个URI。换句话说,如果过滤器中听指定了数据类型,那么一个组件默认的支持content:和file:数据。
如果一个intent可以通过多个activity和service的filters,那么用户将需要选择激活哪个组件。如果找不到目标,刚会抛出一个异常。
4.2.2常见情况
在上面的规则(4)中,反应了一种情况:就是组件可以从文件或Content provider中获取数据。因此,他们的filters只需列出数据的类型而不需要给content:schemes和file:schemes明确命名。下面是一个经典的例子。一个<data>节点,告诉Android,组件可以从content provider获得图片数据并且显示:
因为大多数可用的数据是由content provider分配的,常见的情况就是filters只指定数据类型而不指定URI。
另一种通用的配置是filters指定scheme和数据类型。例如,一个<data>节点,告诉Android,组件可以从网络获得视频数据并显示:
考虑一下当点击一个链接时,浏览器会做什么。首先,它会尝试去显示数据(如果他能够连接到网页)。如果它不能展示数据,它会把隐式的intent与scheme和数据类型放在一起并且尝试启动一个可以完成任务的activity。如果没有合适的activity,它会要求下载器下载数据。把应用放在content provider的控制下,这样会有一个潜在的activities池来响应调度。
大多数应用还可以刚启动时,不需要引入任何指定的数据。初始化应用的Activities拥有一个action为"android.intent.action.MAIN"的filters。如果他们出现在应用的启动器中(通常指Home屏幕上看到的图标),那么他们还必须声明为"android.intent.category.LAUNCHER",如代码清单4-4所示:
代码清单4-4
4.2.3Intent的匹配
Intents与intent filters匹配,不仅仅是为了找到能激活的目标组件,而且还能找到关于设备上的组件集。例如,Android系统内置应用程序启动器,在屏幕最上层显示了用户可以启动的应用程序,通过寻找所有在intent filters中指定了"android.intent.action.MAIN"动作和"android.intent.category.LAUNCHER"分类的activity。然后在启动器中显示activities的图标和标签。同样的,通过寻找filter中的"android.intent.category.HOME"来发现home屏幕程序。
你的应用程序也可以使用同样的方式。PackageManager拥有一系列的query...()方法,这些方法可以用来返回所有可以接收一个特定intent的组件,和相似的一系列resolve...()方法用来决定响应这个intent的最佳组件。例如,queryIntentActivities()返回了一个所有能通过intent的activity的列表,queryIntentService()类似地返回了一个service列表。但这两个方法都不激活组件;他们只是列表了所有可以响应的组件。对于广播接收器也有类似的方法,queryBroadcastReceivers()。
4.3 Note Pad实例
记事本实例(Note Pad Example)应用程序使用户能够浏览一个记事本列表,查看列表中的个别item的细节,编辑,和添加新的item到列表中。这里主要强调的是manifest文档中intent filters的声明。(你可以在<sdk>/samples/NotePad中找到关于这个范例应用程序,包括manifest文件的原代码。)
在manifest文件中,记事本应用程序声明了三个activity,每一个至少含有一个intent filter。它同时还声明了一个content provider来管理记事本数据。Manifest中详细内容如代码清单4-5所示:
代码清单4-5
第一个activity,NoteList是一个可操作的note列表而不是某个单独的note。通常情况下,对于用户来说它是应用程序之中首选的服务接口。通过它定义的三个intent filters,它可以完成以下三件事:
代码清单4-6
如代码清单4-6所示的是第一件事情,这个filter申明了记事本应用程序的主入口点。标准的MAIN动作是一个不需要任何intent信息的入口点,LAUNCHER描述了这个入口点应该被显示到home屏幕中(即应用程序启动器之中)。
代码清单4-7
如代码清单4-7所示的是第二件事,这个filter申明activity可以在记事本目录上可做的事情。它允许用户查看并编辑这个目录(通过VIEW和EDIT动作),或是从目录中选取一个指定的note(通过PICK动作)。<data>元素中的mimeType属性指定了这些动作所操作的数据类型。它指明了这个activity可以从content provider中获得有一个或多个items的Cursor(vnd.android.cursor.dir),并且这个这个content provider可以保留记事本的数据(vnd.google.note)。启动activity的Intent对象会包含一个content:URI指定了这种类型的activity应该打开的额外数据。
在filter中也含有DEFAULT分类。其存在的原因为Context.startActivity()和Activity.startActivityForResult()方法视为所有intents看都包含了DEFAULT的——除了这两个例外:
1. Intents明确的指出了目标activity
2. MAIN和LAUNCHER组合的Intents
因此,DEFAULT是所有filter所要求的——除了那些包含了MAIN和LAUNCHER的filter。(在有明确的intents时,Intent filter不被用到)。
代码清单4-8
如代码清单4-8所示的是第三件事,这个filters描述了activity可以返回一个用户所选择的note而不需要指定用户选择的笔记的目录。GET_CONTENT动作类似于PICK动作。在两个案例中,activity都返回了用户选择的笔记的URI。(在每一个情况下,它被返回给调用startActivityForResult()的activity来启动NoteList actvity)。这种情况下调用者指定了想要的数据类型而不是用户将要选择的目录数据。
数据类型,vnd.android.cursor.item/vnd.google.note,表示了activity可以返回的数据类型——每个单独的note对应一个URI。通过返回的URI,调用者可以从持有note数据的content provider中(vnd.google.note)得到一个对应的Cursor。
换句话说,对于前文中提到的PICK动作,数据类型指示了activity能够给用户显示的数据类型。对于GET_CONTENT filters,它表明了activity可以返回给调用者的数据类型是什么。
拥有了这些能力的intents能够帮NotesList解决:
◆action:android.intent.action.MAIN
启动一个没有指明数据的活动
◆action:android.intent.action.MAIN
◆category:<androidintent.category.LAUNCHER>
启动一个没有数据的activity并添加到home屏幕中。
◆action:android.intent.action.VIEW
◆data:content://com.google.provider.NotePad/notes
请求activity显示在content://com.google.provider.NotePad/notes目录下的所有note的列表。用户可以从列表中选择一个note,并且该activity将把此item的URI返回给启动NoteList activity的activity。
◆action:android.intent.action.GET_CONTENT
◆data type:vnd.android.cursor.item/vnd.google.note
请求activity支持note数据的单个item。
第二个activity,NoteEditor,向用户使展示了单独一个note的入口并允许其被编辑。它有两个intent filters,它可以做两个事情:
代码清单4-9
如代码清单4-9所示的是第一件事,也是唯一的目的,这个activity是想让用户和note进行互动--用户要么查看,要么编辑(EDIT_NOTE是EDIT的同义词)。Intent将包含匹配类型的数据的URIvnd.android.cursor.item/vnd.google.note --他是指向一个单独的,具体的记事本的URI。他通常是被PICK或者GET_CONTENT返回的URI。像以前一样,上面的filters列出了DEFAULT,使这个activity能够被没有具体指明NoteEditor类的intent所调用。
代码清单4-10
如代码清单4-10所示的是第二件事,是让用户创建一个新的note。Intent将包含匹配类型的数据的URI vnd.android.cursor.dir/vnd.google.note --他是指向note存在的目录的URI。
拥有了这些能力的intents能够帮NotesList解决:
◆action: android.intent.action.VIEW
◆data: content://com.google.provider.NotePad/notes/ID根据ID查看一个具体的note中的内容
◆action: android.intent.action.EDIT
◆data: content://com.google.provider.NotePad/notes/ID根据ID让用户编辑它,如果用户想要保存更改,那么数据会更新到content provider中。
◆action: android.intent.action.INSERT
◆data: content://com.google.provider.NotePad/notes创建一个新的note,并可以编辑
最后的activity,TitleEditor,让用户可以编辑note的标题。这个可以通过直接引入activity来实现,而不需要使用intent filters。但是在这里我们正好可以借此机会展示在存在的数据上如何替代操作
代码清单4-10
这个intent filters使用了自定义的action,"com.android.notepad.action.EDIT_TITLE"。它必须是一个具体的note引用(data type是vnd.android.cursor.item/vnd.google.note),就像是之前的View和EDIT。然而,这里的activity显示的是记事本的题目而不是内容。
除了支持常用的DEFAULT ,TitleEditor还支持两种标准的categories: ALTERNATIVE和SELECTED_ALTERNATIVE。这些categories指定了那些以菜单形式展示给用户的activities(很像LAUNCHER category 指定了应用启动器一样)。注意filters还要提供明确的标签(通过android:label="@string/resolve_title")以便能更好的控制用户所能看到的一切。
拥有了这些能力的intents能够帮TitleEditor解决:
◆action: com.android.notepad.action.EDIT_TITLE
◆data: content://com.google.provider.NotePad/notes/ID请求activity显示与note ID相关的标题,并允许用户编辑标题。
FAQ:QQ群213821767