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_ALTERNATIVE activity使用CATEGORY_ALTERNATIVE用來浏覽特定類型的數據。當你查閱文檔是,這些部分通常作為一個可選菜單。例如打印界面相對於其他界面可以稱之為alternative
CATEGORY_SELECTED_ALTERNATIVE activity使用CATEGORY_ALTERNATIVE用來浏覽特定類型的數據。這與列出一系列的文本文件或html文件編輯器很類似。
CATEGORY_LAUNCHER activity使用CATEGORY_LAUNCHER屬性可以使其在launcher(桌面)中顯示
CATEGORY_HOME activity使用CATEGORY_HOME後可以作為主屏幕。特別的,應該只有一個activity具有該屬性,如果有多個,系統會讓你做出選擇。
CATEGORY_PREFERENCE activity使用CATEGORY_PREFERENCE屬性表明其為preference activity。這樣該activity將作為preference screen的一部分進行顯示
CATEGORY_GADGET activity使用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是相同的。