本文主要從系統層怎樣加載一個widget分析,不包含怎樣創建一個含有widget的app。
所謂widget,梗概流程就是App開發者傳給系統一個自定義的RemoteView,鎖屏或者桌面把這個RemotView解析出來放在自己的界面上,以widget的形式顯示出來。
文章有點長,沒耐心只想看大概結構可以翻到最后看小結中的圖。
widget相關類
相關的類主要有以下幾個
AppWidgetHostView
AppWodgetHostView是一個View,是容納App傳來的View的容器。
由于RemoteView并不是真正的View,只是一個View的描述,所以需要通過updateAppWidget(RemoteViews views)這個方法把View創建出來,然后加到AppWidgetView里。
public void updateAppWidget(RemoteViews remoteViews) {
if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
...//此處有省略,詳見源碼
// Try normal RemoteView inflation
if (content == null) {
try {
/// M: add for using customer view, migrated from GB to ICS to JB
remoteViews.setHasUsedCustomerView(mInfo.hasUsedCustomerView);
content = remoteViews.apply(mContext, this, mOnClickHandler);//重點,在這里apply"
if (LOGD) Log.d(TAG, "had to inflate new layout");
} catch (RuntimeException e) {
exception = e;
}
}
...//此處有省略,詳見源碼
if (!recycled) {
prepareView(content);
addView(content);
}
...//此處有省略,詳見源碼
}
- 以上代碼中的mView就是要加到AppWidgetHostView中的View,remoteViews.apply創建了實際的View.
AppWidgetHost
是容納appwidget的地方,它有以下功能:
- 監聽來自AppWidgetService的事件,這是主要處理update和provider_changed兩個事件,根據這兩個事件更新widget。(此處加代碼)
- 另外一個功能就是創建AppWidgetHostView。這里先創建AppWidgetHostView,然后通過AppWidgetService查詢 appWidgetId對應的RemoteViews,最后把RemoteViews傳遞給AppWidgetHostView去 updateAppWidget。(此處加代碼)
以上兩個類是基類,用戶可以根據這兩個類來定制自己的widget的。
AppWidgetProvider
AppWidgetProvider,其本質BroadcastReceiver。用戶用這個類去創建自己的widget,用戶可以通過繼承AppwidgetProvider的一個或者幾個方法來定義自己的widget以及控制widget的更新
主要的function以及作用可以參照開發者文檔
AppWidgetService
Widget的核心類,首先在系統啟動以后在systemReady做了以下工作:
- loadAppWidgetListLocked()通過PackageManager從Android系統中查找所有已經被安裝的AppWidget(包含"android.appwidget.action.APPWIDGET_UPDATE" 的Action和meta-data標簽),解析AppWidget的配置信息,封閉成對象,保存到List集合。
- addProviderLocked 從/data/system/appwidgets.xml文件讀取已經被添加到Launcher的AppWidget信息,封閉成對象,保存到List集合中。
- systemRunning(boolean safeMode) 注冊四個廣播接收器:第一. Android系統啟動完成,第二. Android配置信息改變,第三. 添加刪除應用,第四. sdcard的安裝與缷載。
AppWidgetService承擔著所有widget的管理工作。Widget安裝,刪除,更新等等都需要經過AppWidgetService,它是開機就啟動的.
AppWidgetManager
Updates AppWidget state; gets information about installed AppWidget providers and other AppWidget related state.
這是代碼中對這個類的注釋,可以看出,這個類的主要作用是更新AppWidget的狀態以及得到appwidget的信息。
-
這個類中定義了一些很重要的intent,例如
- ACTION_KEYGUARD_APPWIDGET_PICK:Setting中創建這個Activity,會列出當前系統中可以供添加到鎖屏的所有widget,用戶可以在列表中選擇一個widget添加到鎖屏上。具體實現參照Setting中的類KeyguardAppWidgetPickActivity。
- ACTION_APPWIDGET_BIND:允許綁定一個widget到系統中。參見Setting中的AllowBindAppWidgetActivity
等等。
-
另外這個類中有一些很重要的接口,定義了widget的加載。更新等操作,如
- updateAppWidget(int appWidgetId, RemoteViews views)//更新widget,在第一次加載widget的時候,加載widget
- notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId)//監聽widget的改變。
- bindAppWidgetId(int appWidgetId, ComponentName provider) //綁定widget到系統中。需要BIND_APPWIDGET權限
接口的實現都在AppWidgetService中,使用這個類的主要是鎖屏和桌面。
AppWidgetProviderInfo
每個AppWidget都有一個AppWidgetProviderInfo對象,該對象描述了每個AppWidget的基本數據(meta-data)信息 ,其定義在<appwidget-provider>節點信息。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="100dp"
android:minHeight="40dp"
//設置更新時間 毫秒單位
android:updatePeriodMillis="86400000"
//引用的布局文件
android:initialLayout="@layout/widget_layout"
>
</appwidget-provider>
不做贅述。
存儲位置:
上文可知,系統中所有的widget是通過packageManager從package里查找的(包含"android.appwidget.action.APPWIDGET_UPDATE" 的Action和meta-data標簽),那么已經被添加的鎖屏的widget的信息或者id存儲在哪里呢?
我們再來看KeyguardAppWidgetPickActivity
,在添加appwidget以后選擇一個widget的時候會調到
mLockPatternUtils.addAppWidget(appWidgetId, 0);
最終調到
private void writeAppWidgets(int[] appWidgetIds) {
Settings.Secure.putStringForUser(mContentResolver,
Settings.Secure.LOCK_SCREEN_APPWIDGET_IDS,
combineStrings(appWidgetIds, ","),
UserHandle.USER_CURRENT);
}
也就是說,我們鎖屏相關的widget是通過settings存儲在數據庫里。(具體存儲這塊內容在研究)
addWidget流程(綁定一個widget并最終存儲到數據庫)
以KeyguardHostView為例,添加一個View到數據庫的流程
在Keyguard中,添加一個widget是通過start一個Settings中的Activity來實現的,上文已經提及KeyguardAppWidgetPickActivity。
- 當用戶點擊添加widget的button的時候,會彈出Activity:KeyguardAppWidgetPickActivity,然后Activity會通過PackageManager得到所有的widget并顯示出來。
我們需要關注的是,當某個widget被點擊以后發生的事情。
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Item item = mItems.get(position);
Intent intent = item.getIntent();
int result;
if (item.extras != null) {
// If these extras are present it's because this entry is custom.
// Don't try to bind it, just pass it back to the app.
result = RESULT_OK;
setResultData(result, intent);
} else {
try {
if (mAddingToKeyguard && mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
// Found in KeyguardHostView.java
final int KEYGUARD_HOST_ID = 0x4B455947;
int userId = ActivityManager.getCurrentUser();
* mAppWidgetId = AppWidgetHost.allocateAppWidgetIdForPackage(KEYGUARD_HOST_ID,
userId, "com.android.keyguard");//重點:關鍵步驟1
}
*mAppWidgetManager.bindAppWidgetId(
mAppWidgetId, intent.getComponent(), mExtraConfigureOptions);//重點:關鍵步驟2
} catch (IllegalArgumentException e) {
// This is thrown if they're already bound, or otherwise somehow
// bogus. Set the result to canceled, and exit. The app *should*
// clean up at this point. We could pass the error along, but
// it's not clear that that's useful -- the widget will simply not
// appear.
result = RESULT_CANCELED;
}
setResultData(result, null);
}
if (mAddingToKeyguard) {
onActivityResult(REQUEST_PICK_APPWIDGET, result, mResultData);//重點
} else {
finish();
}
}
- allocateAppWidgetIdForPackage最終會調用到AppWidgetServiceImpl的allocateAppWidgetId。
public int allocateAppWidgetId(String packageName, int hostId) {
int callingUid = enforceSystemOrCallingUid(packageName);
Slog.d(TAG, "allocateAppWidgetId uid"+callingUid+" packageName "+packageName);
synchronized (mAppWidgetIds) {
if (!mHasFeature) {
return -1;
}
ensureStateLoadedLocked();
int appWidgetId = mNextAppWidgetId++;//1.創建一個id
Host host = lookupOrAddHostLocked(callingUid, packageName, hostId);
AppWidgetId id = new AppWidgetId();
id.appWidgetId = appWidgetId;
id.host = host;
host.instances.add(id);
mAppWidgetIds.add(id);2.//添加這個id
saveStateAsync();
if (DBG) log("Allocating AppWidgetId for " + packageName + " host=" + hostId
+ " id=" + appWidgetId);
return appWidgetId;//3.返回創建的id
}
}
然后就給這個widget分配了唯一的id。
2.bindAppWidgetId最終調用到AppWidgetServiceImpl的bindAppWidgetIdImp
private void bindAppWidgetIdImpl(int appWidgetId, ComponentName provider, Bundle options) {
if (DBG) log("bindAppWidgetIdImpl appwid=" + appWidgetId
+ " provider=" + provider);
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mAppWidgetIds) {
...//此處有省略,詳見源碼
id.provider = p;
if (options == null) {
options = new Bundle();
}
id.options = options;
// We need to provide a default value for the widget category if it is not specified
if (!options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
options.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY,
AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN);
}
p.instances.add(id);
int instancesSize = p.instances.size();
if (instancesSize == 1) {
// tell the provider that it's ready
sendEnableIntentLocked(p);
}
// send an update now -- We need this update now, and just for this appWidgetId.
// It's less critical when the next one happens, so when we schedule the next one,
// we add updatePeriodMillis to its start time. That time will have some slop,
// but that's okay.
sendUpdateIntentLocked(p, new int[] { appWidgetId });
// schedule the future updates
registerForBroadcastsLocked(p, getAppWidgetIds(p));
saveStateAsync();
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
這個方法主要就是把剛剛得到的id與這個widget的信息綁定在一起
注意,當id與widget綁定以后需要調用
sendUpdateIntentLocked(p, new int[] { appWidgetId })
來更新這個widget的內容,
void sendUpdateIntentLocked(Provider p, int[] appWidgetIds) {
log("sendUpdateIntentLocked");
if (appWidgetIds != null && appWidgetIds.length > 0) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
///M: added by mtk for debugging @{
Exception e = new Exception();
e.printStackTrace();
/// @}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
intent.setComponent(p.info.provider);
mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId));
}
}
這個方法主要是發送一個廣播,widget的開發者接收到廣播以后來更新這個widget的內容。發廣播的時間是比widget添加到界面上的時間稍微提前,不過開發者可以在接收到廣播以后再更新,這個時間差并不會影響widget接收廣播。
3.當獲取了id并且與widget綁定以后,再接著看onItemClick,
會調用到setResultData(int code, Intent intent)
,這個方法里有這兩句
mLockPatternUtils.addAppWidget(appWidgetId, 0);//前文已經提到這個方法的作用是把id添加到數據庫里。
finishDelayedAndShowLockScreen(appWidgetId);//重啟鎖屏
- 至此,把一個widget的加到數據庫的流程已經理清,主要有以下幾點:
- mAppWidgetId = AppWidgetHost.allocateAppWidgetIdForPackage//申請id
- mAppWidgetManager.bindAppWidgetId//綁定id
- mLockPatternUtils.addAppWidget(appWidgetId, 0);//添加到數據庫
getWidget流程(數據庫中的得到widget列表并顯示在鎖屏)
同樣以KeyguardHostView為起點。
上文可知,在添加完widget以后會重啟鎖屏,這個時候會走到KeyguardHostView的onFinishInflate() ——> updateAndAddWidgets();——>addWidgetsFromSettings()
private void addWidgetsFromSettings() {
if (mSafeModeEnabled || widgetsDisabled()) {
addDefaultStatusWidget(0);//添加一些默認的widget
return;
}
int insertionIndex = getInsertPageIndex();
// Add user-selected widget
final int[] widgets = mLockPatternUtils.getAppWidgets();
if (widgets == null) {
Log.d(TAG, "Problem reading widgets");
} else {
for (int i = widgets.length -1; i >= 0; i--) {
if (widgets[i] == LockPatternUtils.ID_DEFAULT_STATUS_WIDGET) {
addDefaultStatusWidget(insertionIndex);
} else {
// We add the widgets from left to right, starting after the first page after
// the add page. We count down, since the order will be persisted from right
// to left, starting after camera.
addWidget(widgets[i], insertionIndex, true);
}
}
}
}
由前文可知,添加widget到數據庫的操作是LockPatternUtils.addAppWidget(int widgetId, int index);
同理,取出數據庫中鎖屏的widgetid的方法是getAppWidgets
LockPatternUtils.java
private int[] getAppWidgets(int userId) {
String appWidgetIdString = Settings.Secure.getStringForUser(
mContentResolver, Settings.Secure.LOCK_SCREEN_APPWIDGET_IDS, userId);
String delims = ",";
if (appWidgetIdString != null && appWidgetIdString.length() > 0) {
String[] appWidgetStringIds = appWidgetIdString.split(delims);
int[] appWidgetIds = new int[appWidgetStringIds.length];
for (int i = 0; i < appWidgetStringIds.length; i++) {
String appWidget = appWidgetStringIds[i];
try {
appWidgetIds[i] = Integer.decode(appWidget);
} catch (NumberFormatException e) {
Log.d(TAG, "Error when parsing widget id " + appWidget);
return null;
}
}
return appWidgetIds;
}
return new int[0];
}
以上,取出了已經添加到鎖屏上的widget的列表,繼續看KeyguardHostView的addWidget(widgets[i], insertionIndex, true);
private boolean addWidget(int appId, int pageIndex, boolean updateDbIfFailed) {
AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appId);//通過取到widget對應的AppWidgetProviderInfo
if (appWidgetInfo != null) {
AppWidgetHostView view = mAppWidgetHost.createView(mContext, appId, appWidgetInfo);通過調用AppWidgetHost的CreateView方法將remoteview apply為view再添加到AppWidgetHostView中
addWidget(view, pageIndex);
return true;
} else {
if (updateDbIfFailed) {//widget信息為空或無效,刪除id
Log.w(TAG, "*** AppWidgetInfo for app widget id " + appId + " was null for user"
+ mUserId + ", deleting");
mAppWidgetHost.deleteAppWidgetId(appId);
mLockPatternUtils.removeAppWidget(appId);
}
return false;
}
}
AppWidgetHost是通過AppWidgetHostView的view.updateAppWidget(views);方法來獲取view的
public final AppWidgetHostView createView(Context context, int appWidgetId,
AppWidgetProviderInfo appWidget) {
Log.d(TAG, "createView appWidgetId "+appWidgetId);
final int userId = mContext.getUserId();
AppWidgetHostView view = onCreateView(mContext, appWidgetId, appWidget);
view.setUserId(userId);
view.setOnClickHandler(mOnClickHandler);
view.setAppWidget(appWidgetId, appWidget);
synchronized (mViews) {
Log.d(TAG, "createView mViews put "+this);
mViews.put(appWidgetId, view);
}
RemoteViews views;
try {
views = sService.getAppWidgetViews(appWidgetId, userId);
if (views != null) {
views.setUser(new UserHandle(mContext.getUserId()));
}
} catch (RemoteException e) {
throw new RuntimeException("system server dead?", e);
}
view.updateAppWidget(views);
return view;
}
具體請注意以下兩句
public void updateAppWidget(RemoteViews remoteViews) {
...//省略,詳細請查看源碼
// Try normal RemoteView inflation
if (content == null) {
try {
/// M: add for using customer view, migrated from GB to ICS to JB
remoteViews.setHasUsedCustomerView(mInfo.hasUsedCustomerView);
content = remoteViews.apply(mContext, this, mOnClickHandler);
if (LOGD) Log.d(TAG, "had to inflate new layout");
} catch (RuntimeException e) {
exception = e;
}
}
...//省略,詳細請查看源碼
if (mView != content) {
removeView(mView);
mView = content;//mView即AppWidgetHostView中添加的View
}
...//省略,詳細請查看源碼
}
經過以上步驟,則remoteView被通過id得到且創建為View放進AppWidgetHostView中。
繼續看KeyguardHostView的addWidget(view, pageIndex);
會調用到mAppWidgetContainer.addWidget(view, pageIndex),mAppWidgetContainer是一個KeyguardWidgetPager的實例,我們可以隨便定義一個父View,不一定要放在KeyguardWidgetPager里。
KeyguardWidgetPager.java
public void addWidget(View widget, int pageIndex) {
KeyguardWidgetFrame frame;
...//省略,詳細請看源碼
frame.addView(widget, lp);
...//省略,詳細請看源碼
if (pageIndex == -1) {
addView(frame, pageLp);
} else {
addView(frame, pageIndex, pageLp);
}
...//省略,詳細請看源碼
}
這里只是添加上,還不算完成,
注意,在KeyguardHostView中的onAttachedToWindow中還有這么一句
if (!KeyguardViewMediator.isKeyguardInActivity) {
mAppWidgetHost.startListening();
}
startListening一定要寫,widget才會被定時更新。
至此,我們的widget被成功添加到鎖屏的界面。
- 主要流程總結如下:
- widgets = mLockPatternUtils.getAppWidgets();//從數據庫中讀取
- appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appId);//通過id取到widget對應的AppWidgetProviderInfo
- AppWidgetHostView view = mAppWidgetHost.createView(mContext, appId, appWidgetInfo);通過調用AppWidgetHost的CreateView方法將remoteview apply為view再添加到AppWidgetHostView中
- addWidget(view, pageIndex);//將AppWidgetHostView添加到我們的父View中
注意在view加載完成以后mAppWidgetHost.startListening().
updatewidget流程(widget的更新)
上文我們提到兩個地方,有關widget的更新
- 在bindAppWidgetId完成以后,調用 sendUpdateIntentLocked(p, new int[] { appWidgetId });發送廣播來更新,這個流程比較清晰,不贅述。
-
在view加載以后mAppWidgetHost.startListening();來保持View一直會更新
。
AppWidgetHost的startListening會調用到AppWidgetService的startListening,AppWidgetService的startListening會返回一個需要更新的widgetid的數組。AppWidgetHost會在得到這個數組以后更新一次這個Host上的所有widget。
當然,要保持widget持續刷新,更新一次肯定是不夠的,所有我們繼續看
第一小步
AppWidgetService:
public int[] startListening(IAppWidgetHost callbacks, String packageName, int hostId,
List<RemoteViews> updatedViews) {
Slog.d(TAG, "startListening hostId " + hostId);
。。。//省略
Host host = lookupOrAddHostLocked(callingUid, packageName, hostId);
。。。//省略
return updatedIds;
}
}
再來看lookupOrAddHostLocked里做了如下操作
Host lookupOrAddHostLocked(int uid, String packageName, int hostId) {
final int N = mHosts.size();
for (int i = 0; i < N; i++) {
Host h = mHosts.get(i);
if (h.hostId == hostId && h.packageName.equals(packageName)) {
return h;
}
}
Host host = new Host();
host.packageName = packageName;
Slog.d(TAG, "lookupOrAddHostLocked h.uid"+uid);
host.uid = uid;
host.hostId = hostId;
mHosts.add(host);//重點
return host;
}
這里是把需要更新widget的host加入到AppWidgetService的mHost列表里。
第二小步
由于添加widget導致android配置信息改變會進入AppwidgetService的onConfiguartionchanged,這個方法中做了如下操作
void onConfigurationChanged() {
if (DBG) log("Got onConfigurationChanged()");
Locale revised = Locale.getDefault();
if (revised == null || mLocale == null || !(revised.equals(mLocale))) {
mLocale = revised;
synchronized (mAppWidgetIds) {
ensureStateLoadedLocked();
// Note: updateProvidersForPackageLocked() may remove providers, so we must copy the
// list of installed providers and skip providers that we don't need to update.
// Also note that remove the provider does not clear the Provider component data.
ArrayList<Provider> installedProviders =
new ArrayList<Provider>(mInstalledProviders);
HashSet<ComponentName> removedProviders = new HashSet<ComponentName>();
int N = installedProviders.size();
for (int i = N - 1; i >= 0; i--) {
Provider p = installedProviders.get(i);
ComponentName cn = p.info.provider;
if (!removedProviders.contains(cn)) {
updateProvidersForPackageLocked(cn.getPackageName(), removedProviders);//重點1
}
}
saveStateAsync();//重點2
}
}
}
繼續看源碼(這里不再粘貼)會發現
updateProvidersForPackageLocked(cn.getPackageName(), removedProviders)
做了一個操作就是根據配置的更新間隔定時發出更新廣播。
updateProvidersForPackageLocked——>getAppWidgetIds——>sendUpdateIntentLocked(p, appWidgetIds);
saveStateAsync();
會重新更新需要更新的widget的列表的xml。
saveStateAsync()——>writeStateToFileLocked——>parseProviderInfoXml(component, ri);
saveStateAsync就把上文startListening更新的mHost更新到xml里,下次定時更新widet的時候就遍歷到這里然后把對應的widget進行更新.
- 至此,widget的更新流程算是走完了,主要有一下兩個
- onconfiguarationchange的時候定時讀取xml——>向app發送更希widget的廣播——>更新xml,寫入需要update的widgetHost的列表。
- 2.startListening的時候把當前Host的Id通知給AppWidgetService保存在mHost列表里等待下次寫進xml。
小結:
所以添加一個插件的核心流程為