本文是 Android Widget(小部件) 系列的第四篇,主要从源码角度是对 Android widget option更新流程的分析 。
本系列的目的是通过对Android 小部件的梳理,了解小部件刷新流程、恢复流程、以及系统发生变化时,小部件是如何适配的,解决在开发小部件过程中遇到的问题。系列文章大部份来自源码的解读,内容非常多,也非常容易遗忘,因此记录分享。
系列文章
Android Widget (小部件)基础介绍以及常见问题
Android Widget (小部件)刷新源码解析一非列表
Android Widget (小部件) option 刷新源码解析一列表
小部件 option 更新
一、描述
option 通常用于host 与 应用widget 通信 。Host通过该通道将小部件最小宽高、最大宽高(OPTION_APPWIDGET_MIN_WIDTH、OPTION_APPWIDGET_MIN_HEIGHT、OPTION_APPWIDGET_MAX_WIDTH、OPTION_APPWIDGET_MAX_HEIGHT)等属性值传给小部件所在应用。当然host 也可以通过该通道定义其他的通信规则。理论上 widget 也是可以通过 option 和 host 通信,因为option 会存于widget中。
二、更新流程
1、Host 进程调用AppWidgetManager.updateAppWidgetOptions(),通过AIDL 回调到system_server
2、system_server对回调进行一系列的校验,找到对应的widget
2.1、SecurityPolicy.enforceCallFromPackage(),安全性校验,确定请求的包命和uid 是一致的
2.2、AppWidgetServiceImpl.ensureGroupStateLoadedLocked()
若已经加载过了则return,若没有加载过则根据uid 获取widgetProvider(过滤带刷新action 的广播),根据uid 获取相应的配置文件,根据配置文件设置widget,并绑定相应的host。放入mWidgets中。
2.3、AppWidgetServiceImpl.lookupWidgetLocked()
根据widgetId在mWidgets 找到对应的widget,通过uid验证权限
2.4、AppWidgetServiceImpl.sendOptionsChangedIntentLocked(),发送option change 的广播
3、system_server 通过广播通知应用widget
三、详细流程
1、Host 进程调用AppWidgetManager.updateAppWidgetOptions()
class AppWidgetManager {
public void updateAppWidgetOptions(int appWidgetId, Bundle options) {
if (mService == null) {
return;
}
try {
// 通过AIDL 回调到 AppWidgetServiceImpl
mService.updateAppWidgetOptions(mPackageName, appWidgetId, options);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
2、通过AIDL 回调到system_server,对回调进行一系列的校验,找到对应的widget
class AppWidgetServiceImpl {
@Override
public void updateAppWidgetOptions(String callingPackage, int appWidgetId, Bundle options) {
final int userId = UserHandle.getCallingUserId();
if (DEBUG) {
Slog.i(TAG, "updateAppWidgetOptions() " + userId);
}
// Make sure the package runs under the caller uid.
// AppWidgetServiceImpl 运行在system_process ,包名为字符串传入,
// 安全性校验,确定请求的包命和uid 是一致的,后面详讲
mSecurityPolicy.enforceCallFromPackage(callingPackage);
synchronized (mLock) {
// 是否解锁状态,处于解锁状态,若第一次加载则构建widget,后面会详细解析
ensureGroupStateLoadedLocked(userId);
// NOTE: The lookup is enforcing security across users by making
// sure the caller can only access widgets it hosts or provides.
//根据widgetId在mWidgets 找到对应的widget,通过uid验证权限,后面详讲
Widget widget = lookupWidgetLocked(appWidgetId,
Binder.getCallingUid(), callingPackage);
if (widget == null) {
return;
}
// Merge the options.
// 存储option , 因此option 同样可以用于widget 和host 的通信
widget.options.putAll(options);
// Send the broacast to notify the provider that options changed.
//发送option change 的广播,通知应用widget,后面详讲
sendOptionsChangedIntentLocked(widget);
// 将当前的状态进行存储
saveGroupStateAsync(userId);
}
}
}
2.1、SecurityPolicy.enforceCallFromPackage()安全性校验,确定请求的包命和uid 是一致的
public void enforceCallFromPackage(String packageName) {
mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
}
@Deprecated
public void checkPackage(int uid, @NonNull String packageName) {
try {
// 检查请求的 uid 和 packageName 是否一致
if (mService.checkPackage(uid, packageName) != MODE_ALLOWED) {
throw new SecurityException(
"Package " + packageName + " does not belong to " + uid);
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
2.2、 AppWidgetServiceImpl.ensureGroupStateLoadedLocked ()
若已经加载过了则return,若没有加载过则根据uid 获取widgetProvider(过滤带刷新action 的广播),根据uid 获取相应的配置文件,根据配置文件设置widget,并绑定相应的host。放入mWidgets中。
class AppWidgetServiceImpl{
private void ensureGroupStateLoadedLocked(int userId, boolean enforceUserUnlockingOrUnlocked) {
// 判断该应用是否处在解锁状态,设备锁
if (enforceUserUnlockingOrUnlocked && !isUserRunningAndUnlocked(userId)) {
throw new IllegalStateException(
"User " + userId + " must be unlocked for widgets to be available");
}
// 判断该应用文件配置是否处在解锁状态
if (enforceUserUnlockingOrUnlocked && isProfileWithLockedParent(userId)) {
throw new IllegalStateException(
"Profile " + userId + " must have unlocked parent");
}
// 获取能用的配置配置id
final int[] profileIds = mSecurityPolicy.getEnabledGroupProfileIds(userId);
// 查看是否有未加载的user
// Careful lad, we may have already loaded the state for some
// group members, so check before loading and read only the
// state for the new member(s).
int newMemberCount = 0;
final int profileIdCount = profileIds.length;
for (int i = 0; i < profileIdCount; i++) {
final int profileId = profileIds[i];
// >=0代表已经加载过,标记数组
if (mLoadedUserIds.indexOfKey(profileId) >= 0) {
profileIds[i] = LOADED_PROFILE_ID;
} else {
newMemberCount++;
}
}
// 没有新加的 便会return
if (newMemberCount <= 0) {
return;
}
// 构建新增加的ProfileId 数组,后续通常在第一次加载的时候执行
int newMemberIndex = 0;
final int[] newProfileIds = new int[newMemberCount];
for (int i = 0; i < profileIdCount; i++) {
final int profileId = profileIds[i];
if (profileId != LOADED_PROFILE_ID) {
mLoadedUserIds.put(profileId, profileId);
newProfileIds[newMemberIndex] = profileId;
newMemberIndex++;
}
}
// 清除provider 和 host 的tag 设置为 TAG_UNDEFINED
clearProvidersAndHostsTagsLocked();
// 根据加载ProfileId 获取系统 ResolveInfo 列表, 根据ResolveInfo 构建 provider;
loadGroupWidgetProvidersLocked(newProfileIds);
// 从系统配置文件/data/system/users/0/appwidgets.xml 加载状态、
loadGroupStateLocked(newProfileIds);
}
}
2.3、lookupWidgetLocked
根据widgetId在mWidgets 找到对应的widget,通过uid验证权限
class AppWidgetServiceImpl{
private Widget lookupWidgetLocked(int appWidgetId, int uid, String packageName) {
final int N = mWidgets.size();
for (int i = 0; i < N; i++) {
Widget widget = mWidgets.get(i);
if (widget.appWidgetId == appWidgetId
&& mSecurityPolicy.canAccessAppWidget(widget, uid, packageName)) {
return widget;
}
}
return null;
}
}
2.4、sendOptionsChangedIntentLocked
class AppWidgetServiceImpl{
public void sendOptionsChangedIntentLocked(Widget widget) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED);
intent.setComponent(widget.provider.id.componentName);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widget.appWidgetId);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, widget.options);
sendBroadcastAsUser(intent, widget.provider.id.getProfile());
}
}
到这里,option 的更新流程就分析完了,option 刷新流程相对比较简单,主要作用是host 给widget 所在应用传递信息。当然也可用于widget 所在应用给host传递信息,不过host 需要找一个时机去读取信息。另外需要注意一点updateAppWidgetOptions调用属于跨进程调用,因此最好在子线程中调用。