Android中AppWidget的分析與應用:AppWidgetProvider
2012-8-20
本文從開發AppWidgetProvider角度出發,看一個AppWidgetPrvodier在整個AppWidget體系中所扮演的角色。分析了AppWidgetProvider如何被AppWidget系統所識別;AppWidgetProvider何時/如何通過RemoteViews提供並更新資料;如何響應通過RemoteViews提供的PendingIntent的按鈕點選操作。
因為一般應用開發者並不關注AppWidget其他部分(比如,AppWidgetHost,或AppWidget內部元件)的開發,所以一般就直接把AppWidgetProvider開發稱為“AppWidget開發”。
一、實現一個AppWidgetProvider
要實現一個AppWidgetProvider,需要:
- 實現AppWidgetProvider的子類,並至少override onUpdate()方法[非必須,但是如果不這樣做,該AppWidgetProvider就沒有提供任何內容,也就不是AppWidgetProvider了];
- 在AndroidManifest.xml中,宣告上述的AppWidgetProvider的子類是一個Receiver,並且:
- 該Receiver的intent-filter的Action必須包含“android.appwidget.action.APPWIDGET_UPDATE”;
- 該Receiver的meta-data為“android.appwidget.provider”,並用一個xml檔案來描述佈局屬性。
以上幾點皆是AppWidget系統判斷是否是AppWidgetProvider的標誌。後面本文的3.2中詳述是如何被檢索並加入到系統中的。
二、AppWidgetProvider類分析
AppWidgetProvider是一個BroadcastReceiver,必須在AndroidManifest.xml中宣告該Receiver,並接收“android.appwidget.action.APPWIDGET_UPDATE”。
類AppWidgetProvider的實現是一個模板模式:
圖一、AppWidgetProvider
在AppWidgetProvider的onReceiver()實現中已經對接收到的ActionAppWidgetManager.ACTION_APPWIDGET_UPDATE / AppWidgetManager.ACTION_APPWIDGET_DELETED/ AppWidgetManager.ACTION_APPWIDGET_ENABLED以及AppWidgetManager.ACTION_APPWIDGET_DISABLED做了處理,分別執行onUpdate()/ onDeleted() / onEnabled() / onDisabled()。
所以,AppWidgetProvider的實現類,要overrideonReceive(),以及onXXX()[注:至少要實現onUpdate(),在這裡AppWidgetProvider通過RemoteViews提供內容給AppWidgetHost,否則,所謂的AppWidgetProvider什麼也沒提供]。
而在onReceive()的開始處就要執行super.onReceive()讓AppWidgetProvider來分發AppWidgetProvider所要處理的上述廣播訊息。
AppWidgetProvider處理AppWidget中的廣播:
- onUpdate() 處理AppWidgetManager.ACTION_APPWIDGET_UPDATE廣播。該廣播在需要AppWidgetProvider提供RemoteViews資料時,由AppWidgetService.sendUpdateIntentLocked()發出。
- onDeleted() 處理AppWidgetManager.ACTION_APPWIDGET_DELETED廣播。該廣播在有該AppWidgetProvider的例項被刪除時,由AppWidgetService.deleteAppWidgetLocked()發出。
- onEnabled() 處理AppWidgetManager.ACTION_APPWIDGET_ENABLED廣播。該廣播在該AppWidgetProvider被例項化時,由AppWidgetService.sendEnableIntentLocked()發出。
- onDisabled() 處理AppWidgetManager.ACTION_APPWIDGET_DISABLED廣播。該廣播在該AppWidgetProvider的所有例項中的最後一個例項被刪除時,由AppWidgetService.deleteAppWidgetLocked()發出。
一般地,AppWidgetProvider必須處理onUpdate();onEnabled()和onDisabled()最好也要處理;onDeleted()可以不處理。
三、AppWidgetProvider的如何被系統識別
3.1 AppWidgetProvider中配置AndroidManifest.xml
Android中“電量控制”這個AppWidget是由Settings中的SettingsAppWidgetProvider實現的,先來看它的AndroidManifest.xml。
下面是Settings中關於SettingsAppWidgetProvider這個AppWidgetProvider的描述資訊:
<receiver android:name=".widget.SettingsAppWidgetProvider"
android:label="@string/gadget_title"android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.net.wifi.WIFI_STATE_CHANGED"/>
<action android:name="android.net.conn.BACKGROUND_DATA_SETTING_CHANGED" />
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<action android:name="android.location.PROVIDERS_CHANGED"/>
<action android:name="com.android.sync.SYNC_CONN_STATUS_CHANGED" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"android:resource="@xml/appwidget_info" />
</receiver>
這其中滿足一中的1&2的要求,另外,這個AppWidget要處理設定Wifi、Bluetooth、GPS、資料同步和亮度,所以要處理這些相應的設定項變化時的廣播通知。
對3這點,還要要看res/xml/appwidget_info.xml檔案
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dip"
android:minHeight="72dip"
android:updatePeriodMillis="0"
android:initialLayout="@layout/widget"
>
</appwidget-provider>
這裡定義了最小寬度minWidth、最小高度minHeight和初始layoutinitialLayout,用來在AppWidgetProvider還未通過RemoteViews提供資料之前,AppWidgetHost就能夠獲知需要為該AppWidget預留大概的位置;
updatePeriodMillis指示是否需要週期性的更新AppWidget,0是不需要週期更新。
3.2 AppWidgetProvider的資訊被系統所識別
這部分是由AppWidgetService實現。
當包含AppWidgetProvider的apk被安裝到系統中的時候,AppWidgetService會監聽廣播,並處理相應的AppWidgetProvider:
- 監聽到有包被加入(Intent.ACTION_PACKAGE_ADDED或Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE)時,執行addProvidersForPackageLocked()/ updateProvidersForPackageLocked() 新增或更新其中的AppWidgetProvider;
- 監聽到有包被移除(Intent.ACTION_PACKAGE_REMOVED或Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE)時,執行removeProvidersForPackageLocked()移除其中的AppWidgetProvider。
下面重點關注如何加入AppWidgetProvider,看addProvidersForPackageLocked()的實現:
void addProvidersForPackageLocked(String pkgName) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.setPackage(pkgName);
List<ResolveInfo> broadcastReceivers =mPackageManager.queryBroadcastReceivers(intent,
PackageManager.GET_META_DATA);
final int N = broadcastReceivers == null ? 0 :broadcastReceivers.size();
for (int i=0; i<N; i++) {
ResolveInfo ri = broadcastReceivers.get(i);
ActivityInfo ai = ri.activityInfo;
if ((ai.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE)!= 0) {
continue;
}
if (pkgName.equals(ai.packageName)) {
addProviderLocked(ri);
}
}
}
- 檢索所加入包中的所有AppWidgetManager.ACTION_APPWIDGET_UPDATE的Receiver,並放入broadcastReceivers:List<ResolveInfo>;[Line#2~ 5]
- 對每一個這樣的Broadcast,滿足下列要求的加入已安裝AppWidgetProvider列表:
- 不是安裝在外儲存器上;
- Receiver的包名與安裝的包名相同;
- Meta-data的節點名必須是"appwidget-provider"[解析meta-data中android:resource指向的xml內容]
解析meta-data中android:resource指向的xml內容時,所要解析哪些內容是由frameworks/base/core/res/res/values/attrs.xml中的AppWidgetProviderInfo配置:
<declare-styleable name="AppWidgetProviderInfo">
<!-- Minimum width of the AppWidget. -->
<attr name="minWidth"/>
<!-- Minimum height of the AppWidget. -->
<attr name="minHeight"/>
<!-- Update period in milliseconds, or 0 if the AppWidget will updateitself. -->
<attr name="updatePeriodMillis" format="integer"/>
<!-- A resource id of a layout. -->
<attr name="initialLayout" format="reference"/>
<!-- A class name in the AppWidget's package to be launched toconfigure.
If not supplied, then no activity will be launched. -->
<attr name="configure" format="string" />
</declare-styleable>
這些值被解析出來之後,連同label、icon以及由ComponentName(packageName,className)構造的provider被賦值到AppWidgetProviderInfo中,並被記錄在AppWidgetService的mInstalledProviders:ArrayList<Provider>中。
圖二、AppWidgetProviderInfo
四、AppWidgetProvider的Enable與Disable
因為AppWidgetProvider只是提供顯示內容,具體顯示是顯示在AppWidgetHost中的。因為Android機制的關係,後臺的AppWidgetProvider很容易被系統殺掉。所以AppWidgetProvider在收到AppWidgetManager.ACTION_APPWIDGET_ENABLED和AppWidgetManager.ACTION_APPWIDGET_DISABLED而執行的onEnbaled()和onDisabled()中是恰當的設定實現AppWidgetProvider的包能不能被移除設定的恰當點。
在onEnbaled()中,該AppWidgetProvider正在被使用,不讓被殺掉:
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName("com.android.settings",".widget.SettingsAppWidgetProvider"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
在onDisabled()中,該AppWidgetProvider不再被使用,可以被殺掉了:
Class clazz = com.android.settings.widget.SettingsAppWidgetProvider.class;
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName("com.android.settings",".widget.SettingsAppWidgetProvider"),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
五、AppWidgetProvider通過RemoteViews提供內容
在需要AppWIdgetProvider提供RemoteViews時,AppWidget系統會發出AppWidgetManager.ACTION_APPWIDGET_UPDATE廣播,進而onUpdate()會被執行。
圖三、AppWidgetProvider提供RemoteViews
- 在onUpdate()中,建立RemoteViews的例項,傳入AppWidgteProvider所在的包名和該AppWidget所要用的Layout;[Seq#5]
- 如果要響應layoutId中某個viewId被點選操作,要建立本地的PendingIntent,並通過setOnClickPendingIntetn設定到RemoteViews中;[Seq#6~ #9]
- 為layoutId中要顯示的控制元件加上顯示元素,比如某個ImageView的ImageResource。[Seq#10]
- 用AppWidgetManager.updateAppWidget()更新RemoteViews到系統中,AppWidget系統會更新與之繫結的AppWidgetHost。[Seq#11]
六、通過PendingIntent設定按鈕響應
上面講到,可以通過給RemoteViews設定PendingIntent獲知感興趣的View被點選時的響應:
Intent launchIntent = new Intent();
launchIntent.setClass(context, SettingsAppWidgetProvider.class);
launchIntent.addCategory(Intent.CATEGORY_ALTERNATIVE);
launchIntent.setData(Uri.parse("custom:" + buttonId));
PendingIntent pi = PendingIntent.getBroadcast(context, 0 /* norequestCode */,
launchIntent, 0 /* no flags */);
buttonId是layout中的各個Button對應的自定義的Id,該Id只要在本程式中用來能夠區分出是哪個Button就可以,被指進”custom:”引數。
AppWidgetProvider本身就是個BroadcastReceiver,在其onReceive()中,就可以判斷出是哪個Button被點選了:
if(intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
Uri data = intent.getData();
int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
if (buttonId == BUTTON_WIFI) {
// 切換Wifi狀態
} else if (buttonId == BUTTON_BRIGHTNESS) {
// 切換亮度
} else if (buttonId == BUTTON_SYNC) {
// 切換資料同步設定
} else if (buttonId == BUTTON_GPS) {
// 切換GPS開啟開關
} else if (buttonId == BUTTON_BLUETOOTH) {
// 切換藍芽開啟狀態
}
}
總結
本文講述了:
- 實現一個AppWidgetProvider所需要的配置和實現;
- AppWidgetProvider如何被AppWidget識別和加入到已安裝列表;
- AppWidgetProvider如何生成RemoteViews物件,並更新到AppWidgetHost;
- AppWidgetProvider響應按鈕操作。
可進一步參考的文章
AppWidget系統框架。
看如何呼叫本文描述的已經獲取的AppWidgetProvider列表的。
本文。
可以看選取並繫結AppWidgetProvider之後,Launcher作為AppWidgetHost如何建立顯示RemoteViews裡AppWidgetProvider所提供的圖形元素。還可以看到資料模型的載入。
RemoteViews的內部如何實現設定進去的OnClickPendingIntent和ViewImageResource的。