当前位置: 代码迷 >> Android >> Pro Android 四 第五章 理解Intent
  详细解决方案

Pro Android 四 第五章 理解Intent

热度:116   发布时间:2016-04-28 08:12:30.0
Pro Android 4 第五章 理解Intent

     Android引入了一个名为Intent的概念用来唤醒各种组件。Android中的组件包括:activities(UI 组件),services(后台代码),broadcast receivers(用来接收广播消息的代码)和content providers(用来抽象数据的代码)。

     Android的Intent基础

     尽管将intent作为唤醒其他组件机制是很好理解的,不过Android还赋予了Intent这个概念更多的含义。你可以在你的应用中通过intent唤醒其他的应用,还可以唤醒应用内部及外部的各种组件。你可以通过intent发起事件,而其它人则通过一种类似发布与订阅的方式来做出相应。你可以通过intent唤醒闹钟提醒。

     注:什么是intent,简而言之,可以说intent就是一个动作,并且该动作上负载着数据。

     从最简单的水平来看,intent是一个让Android去做(唤醒)什么的动作。Android唤醒某个动作取决于为该动作注册了什么内容。假设你的Activity内容如下:

public class BasicViewActivity extends Activity 
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.some_view);
}
}//eof-class 

      some_view布局应该指向res/layout下的一个合法文件。Android然后允许你通过在该应用的manifest文件中注册这个activity,这样改activity就可以被唤醒了。注册方法如下:

<activity android:name=".BasicViewActivity"
            android:label="Basic View Tests">
<intent-filter>
      <action android:name="com.androidbook.intent.action.ShowBasicView"/>
      <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
     
     这种注册方法不仅仅包括注册一个activity,还包括一个可以唤醒该activity的动作(action)。Activity的设计者通常会为这个action取个名字,并且将这个action作为intent filter(意图过滤器)的一部分。本章后面会介绍更多关于intent的内容。

     现在你已经指定了一个activity并通过action对其进行了注册,这样就可以通过一个intent来唤醒这个BasicViewActivity了。

public static void invokeMyApplication(Activity parentActivity)
{
String actionName= "com.androidbook.intent.action.ShowBasicView";
Intent intent = new Intent(actionName);
parentActivity.startActivity(intent);
}     

     注:action名字的通常形式为:<你的包名>.intent.action.具体名称。

     一旦BasicViewActivity被唤醒,它就可以查看唤醒它的intent了。下面是重写的可以处理唤醒BasicViewActivity的intent的代码:

public class BasicViewActivity extends Activity 
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.some_view);
Intent intent = this.getIntent();
if (intent == null)
{
    Log.d("test tag", "This activity is invoked without an intent");
}
}
}//eof-class 

     
     Android中的Intent

     你可以通过测试来用intent唤醒Android自带的一些应用。http://developer.android.com/guide/appendix/g-appintents.html页面介绍了一些可以被唤醒的Android自带应用。

     注:这些名单可能根据Android发布版本不同而发生变化。

     剩下的可被唤醒的应用包括:

     浏览器应用,用来打开浏览器窗口。

     打电话应用。

     拨号键盘,用户通过其拨打号码。

     地图应用,用来显式给定经纬度的地址。给定经纬度的地址。

     谷歌街景应用。

     Listing5-1介绍如何根据这些应用发布的intents来唤醒它们:

Listing 5–1. Exercising Android’s Prefabricated Applications
public class IntentsUtils
{
public static void invokeWebBrowser(Activity activity)
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.google.com"));
activity.startActivity(intent);
}
public static void invokeWebSearch(Activity activity)
{
Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.setData(Uri.parse("http://www.google.com"));
activity.startActivity(intent);
}
public static void dial(Activity activity)
{
Intent intent = new Intent(Intent.ACTION_DIAL);
activity.startActivity(intent);
}
public static void call(Activity activity)
{
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:555–555–5555"));
activity.startActivity(intent);
}     
public static void showMapAtLatLong(Activity activity)
{
Intent intent = new Intent(Intent.ACTION_VIEW);
//geo:lat,long?z=zoomlevel&q=question-string
intent.setData(Uri.parse("geo:0,0?z=4&q=business+near+city"));
activity.startActivity(intent);
}
public static void tryOneOfThese(Activity activity)
{
IntentsUtils.invokeWebBrowser(activity);
}
}     

     只要你创建一个简单的带有菜单的activity,你就可以通过 tryOneOfThese(Activity activity)方法来测试上述代码。创建一个简单的菜单十分简单,见Listing5-2:

Listing 5–2. A Test Harness to Create a Simple Menu
public class MainActivity extends Activity
{
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.setText("Hello, Android. Say hello");
setContentView(tv);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
int base=Menu.FIRST; // value is 1
MenuItem item1 = menu.add(base,base,base,"Test");
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == 1) {
IntentUtils.tryOneOfThese(this);
          }
else {
      return super.onOptionsItemSelected(item);
}
            return true;
}
}     

     注:可以参考第二章来创建Android工程,编译并运行应用。你也可以通过第7章的前半部分来查看更多创建菜单的代码。或者你可以通过本章末尾关于这部分的eclipse工程代码链接下载代码。不过,当你下载完代码后,这个主要的activity可能有些不同,但是要表达的意思是相同的。在示例代码中,我们还通过XML文件来创建菜单。

     Intent的组成

     另一个确定的可以更好的理解intent的方法就是查看intent对象的组成结构。一个intent包括action、data(通过URI表示)、一个用来存储外部数据的键值对映射和一个显式的类名(称为组件名称component name)。只要intent中至少包含上述内容的一个,其它的内容都是可选的。

     注:如果一个intent包含一个组件名称,那么该intent被称为显式intent。如果intent不包含组件名称,而是依赖于其它部分,如action、data,那么该intent被称为隐式intent。随着我们进一步深入,你将会发现这两种intent是有着细微的差别的。

     Intents和数据URIs

     现在我们已经看到了最简单的intent,这种intent仅需要一个action名称。Listing5-1的ACTION_DIAL就是一例。要唤醒拨号器,我们除了设置intent的aciton之外,不需要其他任何设置。

public static void dial(Activity activity)
{
Intent intent = new Intent(Intent.ACTION_DIAL);
activity.startActivity(intent);
}     

     与ACTION_DIAL不同,ACTION_CALL用来通过给定的号码来进行呼叫,而给定的号码则存储在名为Data的参数中。这个参数指向一个URI,而这个URI又指向一个号码。
public static void call(Activity activity)
{
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:555–555–5555"));
activity.startActivity(intent);

     Intent的action是一个字符串或者字符串常量,通常将java的包名作为前缀。

     Intent的data部分通常并不是一个data数据,而是指向data的引用。data部分通过字符串来表示URI。Intent的URI可以包含参数,与网页URL类似。

     每个被action唤醒的activity都应该指定URI的格式。本例中,“call”方法决定了需要什么样的数据URI。通过该URI,可以解析出电话号码。

     注:被唤醒的activity也可以使用这个URI作为一个数据指针,从而解析出数据并加以使用。对于媒体,如音频、视频和图像来说正是如此。

     通用Actions

     Intent.ACTION_CALL和Intent.ACTION_DIAL会很容易误导我们,让我们认为action和其唤醒的对象之间存在着一对一的关系。为了反证这个观点,我们看一个相反的例子,如Listing5-1中的代码所示:

public static void invokeWebBrowser(Activity activity)
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.google.com"));
activity.startActivity(intent);
}     

     请注意其action仅仅是简单的定义为ACTION_VIEW。Android如何通过这个宽泛的action来确定要唤醒那个activity呢。这种情况下,Android就不仅仅依靠这个通用的action了,还需要依赖URI的特性。Android会查看URI的scheme部分,这部分恰好是http,然后查询所有注册的activities,看谁可以理解这个scheme。这样就可以查询哪个activity可以处理View动作并被唤醒。由于这个原因,browser activity就应该注册一个View intent,并且包含http作为数据scheme。在manifest文件中声明这种intent的方法如下:

<activity......>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http"/>
<data android:scheme="https"/>
</intent-filter>
</activity>     

     你可以通过http://developer.android.com/guide/topics/manifest/data-element.html 来学习更多的关于数据节点的属性。

     Intent 过滤器节点中数据xml子节点的子元素或特性包括下面内容:

     host
     mimeType
     path
     pathPattern
     pathPrefix
     port
     scheme

     mimeType将是你经常用到的属性。例如下面的例子表明展示notes列表的activity的intent filter的mimeType是一个包含多个notes的目录。
<intent-filter>
      <action android:name="android.intent.action.VIEW" />
      <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
</intent-filter> 

     该intent filter可以读作“唤醒该activity来浏览notes集合”。

     另一方法,如果只展示一个单独note,其intent filter使用的MIME类型为单条目类型:

<intent-filter>
      <action android:name="android.intent.action.VIEW" />
      <data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
</intent-filter>    

     该intent filter可以读作“唤醒该activity来浏览单个note”。 

     使用Extra信息
     
     Intent除了action和数据等主要属性外,还有一个名为extras的属性。Extra可以为接收该intent的组件提供更多的信息。Extra数据时以键值对的形式存在:键名通常以包名开始,而值可以是基本数据类型或其他任意实现了android.os.Parcelable的对象。这个extra信息在Android中被称为android.os.Bundle。

     Intent类提供了两种方法来访问extra Bundle:

     // 从intent获取bundle
     Bundle extraBundle = intent.getExtras();

     // 将一个bundle放进intent中
     Bundle anotherBundle = new Bundle();
     // 为bundle填充键值对
     。。。
     // 将bundle设置仅intent中
     intent.putExtras(anotherBundle);

     getExtras()很简单明了:就是返回intent所持有的bundle。putExtras()首先检查当前intent是否已持有bundle,如果有,则将新的bundle中的键值对转移到原有的bundle中,如果没有,则先创建一个bundle,然后将新的bundle中的键值对复制到新创建的bundle中。

     注:putExtra方法是复制原有bundle内容,而非引用。这样,如果你稍后修改传入的bundle也不会修改已经存入intnet的bundle。

     你可以用很多方法来存储基本数据类型到bundle中。下面是一些将简单数据类型存入bundle中的方法:

putExtra(String name, boolean value); 
putExtra(String name, int value);
putExtra(String name, double value);
putExtra(String name, String value); 

     还有一些不是太简单的数据类型的例子:

//simple array support
putExtra(String name, int[] values);
putExtra(String name, float[] values); 

//Serializable objects
putExtra(String name, Serializable value); 

//Parcelable support
putExtra(String name, Parcelable value); 

//Add another bundle at a given key
//Bundles in bundles
putExtra(String name, Bundle value); 

//Add bundles from another intent
//copy of bundles
putExtra(String name, Intent anotherIntent); 

//Explicit Array List support
putIntegerArrayListExtra(String name, ArrayList arrayList);
putParcelableArrayListExtra(String name, ArrayList arrayList);
putStringArrayListExtra(String name, ArrayList arrayList); 

     在接收侧,会有相应的获取数据的方法。这些方法将键的名字作为参数。在下面网址可以查询更多相关内容:
http://developer.android.com/reference/android/content/Intent.html#EXTRA_ALARM_COUNT.  

     下面我们看一下该网址列举的在发送email时涉及到的两个例子:

     EXTRA_EMAIL:你可以通过该键值获取emal地址集合。该键值为android.intent.extra.EMAIL。它应该指向一个包含email文本地址的数组。

     EXTRA_SUBJECT:你可以通过该键值获取邮件的主题。该键值为android.intent.extra.SUBJECT.它指向一个包含主题的字符串。

     使用组件直接唤醒Activity

     你已经看到一些用intent唤醒activity的方法了。你见到了用一个显式的action唤醒activity,也见到了用一个通用的action借助data URI的帮助来唤醒activity。Android还提供了一种更为直接的方法来唤醒activity:你可以直接通过组件名称来实现,而组件名就是对象的包名和类名的一种抽象。下面是Intent类指定组件名的方法:

setComponent(ComponentName name); 
setClassName(String packageName, String classNameInThatPackage);
setClassName(Context context, String classNameInThatContext);
setClass(Context context, Class classObjectInThatContext); 
     
     而最终这些方法都是setComponent(ComponentName name) 方法的简写。

     组件名称包含包名和类名。例如,下面就是一个唤醒模拟器自带的cotacts activity的方法:

Intent intent = new Intent();
intent.setComponent(new ComponentName(
     "com.android.contacts"
     ,"com.android.contacts.DialContactsEntryActivity");
startActivity(intent);

     请注意包名和类名是全称,且在传入intent之前首先用于构建ComponentName。

     你也可以直接使用类名,而无需用组件名来唤醒activity。再次看下面的activity:

public class BasicViewActivity extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.some_view);
}
}//eof-class               

     你可以通过下面方法来唤醒此activity:

Intent directIntent = new Intent(activity, BasicViewActivity.class);
activity.start(directIntent);

     不管你用什么方法来唤醒activity,你都有在AndroidManifest.xml文件中注册这个activity:

<activity android:name=".BasicViewActivity"
      android:label="Test Activity">          

     注:如果通过类名来唤醒activity,则不需要任何intent filter。正如前面所说,这种intent称为显式intent。由于这种显式的intent已经指明了要唤醒的组件的全称,则不需要其他额外的部分。

     理解intent category

     你可以将activities划分为多个类别,这样你可以根据类别名来搜索它们。例如,启动阶段,Andorid会寻找category为CATEGORY_LAUNCHER的activity,然后将该activity的名字及图表放在主界面上用于启动。

     还有一个例子:android会搜索一个category为CATEGORY_HOME的activity,在开始阶段作为主屏幕显示。类似的,如果activity的类别名为CATEGORY_GADGET,则其会作嵌入到其它activity中进行复用。

     以CATEGORY_LAUNCHER为例,类别名的格式如下:

     android.intent.category.LAUNCHER

     你需要知道这些具体的字符串,因为category将作为intent filter的一部分写入AndroidManifest.xml文件中。下面是一个例子:

<activity android:name=".HelloWorldActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>     

     注:activity可能有一些特定的属性来进行限制或者使用,例如你是否想将activity嵌入到父activity中。这种activity属性也是通过category来进行设定的。

     下面我们看一下android预定义的category,以及如何使用它们:

     Table 5–1.Activity Categories 及其描述

category名称描述
CATEGORY_DEFAULT如果activity想被一个隐式的intent唤醒,那么可以声明为CATEGORY_DEFAULT。如果不定义该属性,那么activity需要显式的intent进行唤醒。因此,你可以看到那些被通用的action或其他action唤醒的activity使用缺省的category说明
CATEGORY_BROWSABLE用来想浏览器声明当其被唤醒时不会影响到浏览器的安全需求
CATEGORY_TAB该activity被嵌在一个父tabbed activity中
CATEGORY_ALTERNATIVEactivity使用CATEGORY_ALTERNATIVE用来浏览特定类型的数据。当你查阅文档是,这些部分通常作为一个可选菜单。例如打印界面相对于其他界面可以称之为alternative
CATEGORY_SELECTED_ALTERNATIVEactivity使用CATEGORY_ALTERNATIVE用来浏览特定类型的数据。这与列出一系列的文本文件或html文件编辑器很类似。
CATEGORY_LAUNCHER activity使用CATEGORY_LAUNCHER属性可以使其在launcher(桌面)中显示
CATEGORY_HOMEactivity使用CATEGORY_HOME后可以作为主屏幕。特别的,应该只有一个activity具有该属性,如果有多个,系统会让你做出选择。
CATEGORY_PREFERENCEactivity使用CATEGORY_PREFERENCE属性表明其为preference activity。这样该activity将作为preference screen的一部分进行显示
CATEGORY_GADGETactivity使用CATEGORY_GADGET就可以嵌入到父activity中
CATEGORY_TEST 表明这是一个测试activity
CATEGORY_EMBED该属性已被CATEGORY_GADGET取代,保留只为后向兼容


     你可以在下面网址阅读更多关于category的介绍:
      http://developer.android.com/android/reference/android/content/Intent.html#CATEGORY_ALTERNATIVE. 
     
     当你唤醒一个activity时,你可以通过设置category来确定要唤醒什么类型的activity。或者你可以搜索到满足特定category的activity。下面是一个获取与CATEGORY_LAUNCHER相匹配的activity的方法:

Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
PackageManager pm = getPackageManager();
List<ResolveInfo> list = pm.queryIntentActivities(mainIntent, 0);

     PackageManager是一个可以让你获取到与特定category匹配的activity而又不用将其唤醒的关键类。你可以遍历返回的activities,当遇到合适的activity可以再将其唤醒。下面是一个如何遍历activities,以及如何唤醒其中相匹配的一个activity的例子,我们用了一个随机的名字来进行测试:

for(ResolveInfo ri: list)
{
//ri.activityInfo.
Log.d("test",ri.toString());
String packagename = ri.activityInfo.packageName;
String classname = ri.activityInfo.name;
Log.d("test", packagename + ":" + classname);
if (classname.equals("com.ai.androidbook.resources.TestActivity"))
{
Intent ni = new Intent();
ni.setClassName(packagename,classname);
activity.startActivity(ni);
}
}          
     
     你也可以仅仅依靠category来唤醒一个activity,如CATEGORY_LAUNCHER.

public static void invokeAMainApp(Activity activity)
{
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
activity.startActivity(mainIntent);
     
     会有不止一个activity与之匹配,那么android会选择哪一个呢?为了解决这个问题,Android弹出一个相匹配的activity列表(complete action using)对话框,这样你就可以选择其中一个运行。

     下面是另一个去往home主页的例子:

//Go to home screen
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_HOME);
startActivity(mainIntent);

     如果你不喜欢android默认的主页,那么你可以自己写一个,并与category HOME进行标记。这样,再使用前面的代码,就会弹出选项,让你选择主页。这是因为现在已经注册有不止一个主页了。

//Replace the home screen with yours
<intent-filter>
<action android:value="android.intent.action.MAIN" />
<category android:value="android.intent.category.HOME"/>
<category android:value="android.intent.category.DEFAULT" />
</intent-filter>          

     Intent唤醒组件的规则

     到目前为止,我们已经讨论了intent的许多方面。简单总结一下就是:actions、data URIs、extra data和category。有了这些,android通过intent filter,依据多种策略来匹配最合适的activity。

     最顶层是与intent相关联的组件名。如果设置了这个,那么intent就是显式intent。对于的intent,只有组件名是重要的,其他的所有属性都可以忽略。如果一个intent没有指明组件名,那么该intent被称为隐式intent。而隐式intent的处理规则则有很多。intent。对于显式的intent,只有组件名是重要的,其他的所有属性都可以忽略。如果一个intent没有指明组件名,那么该intent被称为隐式intent。而隐式intent的处理规则则有很多。

     最基本的规则就是传入的intent的action、category和data属性必须与intent filter中声明的属性相匹配。Intent filter与intent不同,可以声明多个actions、categories和data属性。这表明,同一个intent filter可以匹配多个intent,也就是说一个activity可以对多个intent做出反应。不过,“匹配”这个概念在action、category和data中并不相同。下面我们看一下对于不同的属性匹配规则有何不同。

     Action

     如果一个intent设定了action属性,那么intent的filter中必须有相同的action或者不包含任何action。所以,对于不定义任何action的intent filter可以与任何action相匹配。

     如果一个intent filter定义了多个actions,那么至少需要含有一个action与传入intent的action相匹配。

     Data

     如果intent filter中没有定义data属性,那么它将不会与任何定义了data属性的intent相匹配。这说明该intent filter只寻找没有定义data属性的intent。

     Filter中缺少data属性和缺少action属性是截然相反的。如果没有action属性,那么所有的action都会匹配。如果没有data属性,那么即使data只有1个bit,也不会匹配。

     Data类型

     为了匹配data的类型,传入的intent的data的类型必须与intent filter定义的数据类型相同。Intent中的data类型必须在intent filter中列出。

     出入的数据类型由两种方式决定。第一种:如果data URI是一个content或者file URI,那么content provider或者Android本身将会识别出其类型。第二种:检查intent中显式定义的数据类型。对于第二章,传入的intent不应该设置data URI,因为当intent的setType方法调用时会自动处理data URI。

     Android也允许其MIME类型的子类型用星号(*)来代替所有的子类型。

     另外,data类型时大小写敏感的。

     Data Scheme

     为了匹配data scheme,传入的intente的data scheme必须与intent filter中的scheme属性相匹配。也就是说传入的data scheme必须存在于intent filter中。

     传入的data scheme是intent的data URI的第一部分。Intent中没有设置scheme的方法。其仅能从传入的data URI中(如http://www.somesite.com/somepath. )解析出来。

     如果传入的intent的data URI是content:或者file:,那么就认为其与intent filter相匹配,而不必考虑scheme、domain和path。根据Android SDK,这样的原因是所有的组件都被设计为知道如何从content或者file URLs中读取数据。换句话说,所有的组件都被设计为支持这两种类型的URLs。

     Scheme也是大小写敏感的。

     如果intent filter中没有指明authority属性,那么传入的任何URI的authority(域名)都与之匹配。如果在filter中指定了authority,例如www.somesite.com,那么其中一个scheme和一个authoriy必须与传入的intent中的data URI相匹配。

     例如,我们在intent filter中指定authority是www.somesite.com,scheme是https。那么intent中的http://www.somesite.com将不会与之匹配。因为http并没有被filter指定为支持的scheme。

     Authority也是大小写敏感的。

     Data Path

     如果intent filter中没有定义data path,表示与任意的传入的intent的data path相匹配。如果filter中指定了一个data path,例如somepath,那么一个scheme,一个authority和一个path就应该与传入的intent的data URI相匹配。

     换而言之,scheme、authority和path一起来确定某个传入的intent是否合法,例如http://www.somesite.com/somepath.所以,scheme、authority和path并不是独立工作,而是一起工作。

     Path也是大小写敏感。

     Intent Category

     所有传入的intent的category必须在intent filter中列举出来。在filter中可以包含多个categories。如果filter中没有设置category,那么也只能匹配没有设置category的intent。

     不过,这里有一个忠告。Android这样处理传入到startActivity中的隐式intent:默认intent至少包含一个category,也就是android.intent.category.DEFAULT。startActivity()中的代码会寻找filter中包含DEFAULT Category的activities。所以,任何想要被隐式intent唤醒的activity都必须在intent filter中设定DEFAULT Category。

     即使一个activity的intent filter中不包含default category,如果你知道其显式的组件名称,你也可以向laucher那样启动该activity。如果你不考虑default category而显式的搜索与intent匹配的activity,你也可以用那种方法启动这些activities。

     这样,DEFAULT category是根据startActivity()的实现的产物,而不是filter中固有的行为。

     还有一个利好的消息:如果activity仅仅想被laucher唤醒,那么default category并不是必须的。所以这些activities仅仅设置了MAIN和LAUCHER category。不过这些activities中也可以设置DEFAULT category。

     以ACTION_PICK为例

     到现在我们已经介绍了一些用来唤醒activity而不需要返回数据的intents或者actions。下面我们再介绍一种稍微复杂点的能够在唤醒activity后返回数据的action。ACTION_PICK就是其中一个。

     ACTION_PICK的目的是唤醒一个activity,该activity列出一系列条目,然后运行你选择其中的某个条目。一旦用户选中了某个条目,则该activity应该返回选中条目的URI给调用者。这样就可以复用UI的功能来进行选择了。

     你应该通过MIME类型指明要选择的条目集合,该MIME类型指向Android的content cursor。其URI的MIME类型应该类似于下面的形式:

     vnd.android.cursor.dir/vnd.google.note

     Activity负责从基于URI的content provider中提取数据。这也是为什么需要尽可能的把数据封装到content provider中的原因。

     对于返回数据的action,我们不能使用startActivity(),因为startActivity()并不返回数据。startActivity()不返回数据的原因是该方法通过一个独立的线程来启动activity,而将主线程继续用来处理事务。换句话说,startActivity()是一个异步的方法,且没有回调函数,这样就无法得知被唤醒的activity的状态。如果你想要返回数据,那么可以使用startActivity()的一个变形startActivityForResult(),该方法带有一个回调函数。

     让我们看一下startActivityForResult()的定义:

     public void startActivityForResult(Intent intent, int requestCode)

     这个方法启动一个activity,并且你想从该activity中获取返回数据。如果这个activity退出后,原来的activity中的onActivityResult()方法将被调用,并且返回之前传入的requestCode。该方法定义如下:

     protected void onActivityResult(int requestCode, int resultCode, Intent data)

     其中requestCode就是你传入startActivityForResult()的参数。而resultCode可以是RESULT_OK, RESULT_CANCELLED或者用户定义的数字。用户自定义数字应该从RESULT_FIRST_USER开始。Intent参数包含activity返回的其他数据。在ACTION_PICK例子中,该intent返回指向某个条目的data URI。

     Listing5-3是一个返回结果的activity的例子。

     注:Listing5-3中的例子认为你已经安装了android sdk包中的NotePad应用。我们后面会给出链接来告诉你如何下载该应用。

Listing 5–3. Returning Data After Invoking an Action
public class SomeActivity extends Activity
{
.....
.....
public static void invokePick(Activity activity)
{
Intent pickIntent = new Intent(Intent.ACTION_PICK);
int requestCode = 1;
pickIntent.setData(Uri.parse(
"content://com.google.provider.NotePad/notes"));
activity.startActivityForResult(pickIntent, requestCode);
}
protected void onActivityResult(int requestCode
,int resultCode
,Intent outputIntent)
{
//This is to inform the parent class (Activity)
//that the called activity has finished and the baseclass
//can do the necessary clean up
super.onActivityResult(requestCode, resultCode, outputIntent);
parseResult(this, requestCode, resultCode, outputIntent);
}
public static void parseResult(Activity activity
, int requestCode     
, int resultCode
, Intent outputIntent)
{
if (requestCode != 1)
{
Log.d("Test", "Some one else called this. not us");
return;
}
if (resultCode != Activity.RESULT_OK)
{
Log.d(Test, "Result code is not ok:" + resultCode);
return;
          }
Log.d("Test", "Result code is ok:" + resultCode);
Uri selectedUri = outputIntent.getData();
Log.d("Test", "The output uri:" + selectedUri.toString());
//Proceed to display the note
outputIntent.setAction(Intent.ACTION_VIEW);
startActivity(outputIntent);

      RESULT_OK, RESULT_CANCELED,和 RESULT_FIRST_USER常量都在Activity类中定义。其对应的数值为:

     RESULT_OK = -1; 
     RESULT_CANCELED = 0;
     RESULT_FIRST_USER = 1; 

     为了保证PICK操作能够成功,实现者必须显式的指明PICK需要什么。我们看一下在google的NotePad例子中是如何实现的。当列表中的条目被选中时,会检查传入的用来唤醒activity的intent的action是否是ACTION_PICK.如果是,则选中项目的URI将被放入一个新的intent中并通过setResult()方法传回。

@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
Uri uri = ContentUris.withAppendedId(getIntent().getData(), id);
String action = getIntent().getAction();
if (Intent.ACTION_PICK.equals(action) ||
Intent.ACTION_GET_CONTENT.equals(action))
{
// The caller is waiting for us to return a note selected by
// the user. They have clicked on one, so return it now.
setResult(RESULT_OK, new Intent().setData(uri));
} else {
// Launch activity to view/edit the currently selected item
startActivity(new Intent(Intent.ACTION_EDIT, uri));
}

     以GET_CONTENT Action为例

     ACTION_GET_CONTENT与ACTION_PICK类似。在ACTION_PICK的例子中,你指定一个URI,使其指向多个条目的集合,例如多个notes的集合。你期待着选取一个条目然后将其返回给调用者。而在ACTION_GET_CONTENT例子中,你告诉Android你想要一个特定MIME类型的条目。Android会寻找那些能够创建该MIME类型条目的activities或者已经存在该MIME类型的条目可以从中选择的activities。

     使用ACTION_GET_CONTENT,你可以通过下面的代码从notes集合中选取一个NotePad应用所支持的note:

public static void invokeGetContent(Activity activity)
{
Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT);
int requestCode = 2;
pickIntent.setType("vnd.android.cursor.item/vnd.google.note");
activity.startActivityForResult(pickIntent, requestCode);
}     

     请注意如何设置一个单条目的MIME类型。与ACTION_PICK不同,其输入的是一个data URI:

public static void invokePick(Activity activity)
{
Intent pickIntent = new Intent(Intent.ACTION_PICK);
int requestCode = 1;
pickIntent.setData(Uri.parse(
"content://com.google.provider.NotePad/notes"));
activity.startActivityForResult(pickIntent, requestCode);

     对于负责响应ACTION_GET_CONTENT的activity,应该在intent  filter中注册相应的MIME类型。下面是sdk中NotePad应用如何实现这一点的方法:

<activity android:name="NotesList" android:label="@string/title_notes_list">
......
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
</intent-filter>
......
</activity>     

     至于如何处理onActivityResult,与ACTION_PICK是完全相同的。如果有多个activities都可以返回相同的MIME类型,android会弹出选择菜单供你选择。      

      Pending Intents介绍 

     Android有一个intent的变种称为Pending intent。该intent运行android的组件在某个位置将intent存储起来比便在将来使用,这样该组件可以被再次唤醒。例如,在闹钟管理器中,你想要在闹钟关闭后再开启一个服务。Android先创建一个pending intent将对应的普通intent包装起来,然后存储在某个地方,这样即使调用的进程被杀死后,这个intent也可以被传递给目标。在pending intent创建时,android会存储足够的原始进程的信息,这样在分配或唤醒时,可以查看其安全证书。

     我们看一下如何创建pending intent:

Intent regularIntent;
PendingIntent pi = PendingIntent.getActivity(context, 0, regularIntent,...);

     注:  PendingIntent.getActivity()方法中的第二个参数叫做requestCode,本例中我们设置为0.如果两个pending intent所包装的intent一样,这个参数就可以用来区分这两个pending intent。这方面内容我们会在第20章具体讨论pending intent和闹钟时具体介绍。

     对于 PendingIntent.getActivity()的名字有很多奇怪之处。这里的activity到底扮演什么角色?为什么我们在创建pending intent的时候不使用create这个词,而是使用get?

     为了理解第一点,我们需要在进一步了解一下普通的intent的用途。一个普通的intent可以用来唤醒一个activity,service或broadcast receiver。(后面你会学到service和broadcast receiver)用intent来唤醒不同种类的组件本质是不同的,为了实现这个目的,android context(activity的父类)提供三种不同的方法:

startActivty(intent)
startService(intent)
sendBroadcast(intent)

     由于有这几个变形,那么如果我们要想存储一个intent以后使用,android如何知道这个intent是用来唤醒activity、service还是broadcast receiver呢?这也就是为什么我们要在创建pending intent之前先指定其用途,这样就解释了下面三个方法为何如此命名:

PendingIntent.getActivity(context, 0, intent, ...)
PendingIntent.getService(context, 0, intent, ...)
PendingIntent.getBroadcast(context, 0, intent, ...)

     现在我们解释一下为什么用“get”。Android存储intent并进行复用。如果你用同一个intent请求pending intent两次,你也只能得到一个pending intent。

     这样,你看到PendingIntent.getActivity()的全部定义后就会稍微清晰一些了。

PendingIntent.getActivity(Context context, //originating context
int requestCode, //1,2, 3, etc
Intent intent, //original intent
int flags ) //flags 

     如果你的目的是获取一个不同的pending intent复本,你就要提供一个不同的requestCode。我们在第20章介绍alarm时会更详细的介绍这方面内容。如果两个intents中,处理extra bundle部分,其它都相同,那么会认为是同一个intent。如果你必须要对这样的两个其它部分一样的intents进行区分,那么就提供不同的requestCode。这样,创建的pending intent就会不同,及时其底层intent是相同的。

     flag参数表明当存在一个pending intent时需要做些什么,是否是返回一个null,还是重写extras,等等。下面网址可以获得更多flag的含义:

     http://developer.android.com/reference/android/app/PendingIntent.html

     通常情况下,你可以为requestCode何flag传入0来获取默认的属性。

     资源

     下面是一些帮助你更好理解本章内容的有用的链接:

http://developer.android.com/reference/android/content/Intent.html:
介绍intents相关内容,包括常用的的actions, extras等内容介绍.

http://developer.android.com/guide/appendix/g-app-intents.html: Lists
Google应用中intents集合。这里你可以看到如何唤醒Browser, Map, Dialer和 Google Street View.

http://developer.android.com/reference/android/content/IntentFilter.html:
介绍intent filters,当你需要注册filters时非常有用。

http://developer.android.com/guide/topics/intents/intents-filters.html:
Intent filters的关键规则。

http://developer.android.com/resources/samples/get.html: 
NotePad应用下载网址,你需要该应用测试一些intents.

http://developer.android.com/resources/samples/NotePad/index.html:
NotePad应用线上代码。

www.openintents.org/: 
一个尝试集合不同第三方厂商提供的intents的网站。

 www.androidbook.com/proandroid4/projects: 
本章示例工程下载网址。其zip文件名称为ProAndroid4_ch05_TestIntents.zip. 

     总结

     本章涵盖下面内容:

     一个隐式的intent就是actions、data URIs和extras传入的显式数据的集合。

     显式的intent就是直接绑定组件名称,而忽略其他所有隐式内容。

     在Android你通过itent来唤醒Activity或其他组件。

     activity等组件通过intent filter来声明其对哪些intents做出响应。

     intents和intent filters的区分规则。

     如何用intent来启动activity。

     如何启动可以返回数据的activity。

     intent category扮演的角色。

     default category的细微差别。

     什么是pending intent?如何使用?

     pending intent的不同之处?

     如何使用PICK和GET_CONTENT这两个actions?


     复习问题

     1、你如何通过一个intent来唤醒activity?

     2、什么是显式、隐式的intents?

     3、intent的组成?

     4、你如何通过intent想接收intent的组件传入数据。

     5、你能说出android应用中的主要组件吗?

     6、intent中的data部分是直接包含数据吗?

     7、intent中的action部分可以直接引用activity或其他组件吗?

     8、当指明intent中类名时,其它部分如何处理?

     9、action.MAIN有什么含义?

     10、如果你在intent filters中不指定任何action,是否意味着其可以与所有action相匹配?

     11、如果在intent filter中不指定data,那么什么样的intents可以与之匹配?

     12、在你的intent filter中加入一个default category为何很有必要?

     13、你的laucher activity需要default category吗?

     14、你如何唤醒一个可以返回给调用者数据的activity?

     15、唤醒一个activity最快的方法是什么?

     16、action_pick和action_get_content的区别是什么?
  相关解决方案